feat(ledger): the consumer map reads transition names and concatenated prefixes (#2970)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 10s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 23s

Paying down #2962 measured `flag="unused-css"` against a hand audit and
found it had a permanent false-positive floor: roughly sixty classes it
called unused are alive and always would be, because two ordinary authoring
forms produce names no reader of `class=` attributes can see. A flag whose
list you cannot act on line by line is worse than no flag — act on it and
you delete live UI.

- A transition `name=` IS a class reference. `<Transition name="toast">`
  makes Vue apply `.toast-enter-active` and its siblings at runtime, and
  React's `<CSSTransition classNames="fade">` does the same with a different
  suffix set. Every spelling of the tag is read (`Transition`,
  `TransitionGroup`, `transition-group`), and the emitted suffix set is the
  union of Vue 3, Vue 2 and React: naming a class no rule defines costs
  nothing, since it resolves to no row. A bound `:name` stays unknowable.
- A concatenated name contributes its static head as a PREFIX reference.
  `` `status-${s}` ``, `'pri-' + p` and `class="card-{{ v }}"` all leave a
  head behind once the hole is blanked — and `_CLASS_TOKEN_RE` accepts a
  trailing hyphen, so until now the extractor emitted a junk token
  `"status-"` that matched nothing. It is now `status-*`, and
  resolve_consumers credits every row whose symbol starts with that head,
  each under the same own-file-else-fan-out rule as an exact token. `*`
  cannot occur in a class token, so the marker rides the existing
  dict[str, int] with no schema change. A head shorter than two characters
  says nothing and is dropped.

Crediting every candidate row is the honest reading: the template genuinely
does not say which one it built, and the alternative is reporting live rules
as dead. What the map still cannot see is a name assembled in a script —
`classList.add` — which stays deliberately out of scope; the skill, the
`flag=` docstring and the refresh payload docs all say so now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 21:27:27 -04:00
co-authored by Claude Fable 5
parent 4179f3e560
commit df18e897af
6 changed files with 164 additions and 15 deletions
+34
View File
@@ -473,6 +473,40 @@ def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
assert all(sid != 6 for sid, _ in resolve_consumers(css_rows, refs))
def test_resolve_consumers_credits_every_row_a_prefix_could_have_built():
"""#2970: `status-${s}` names a class the map cannot pin down, so the
prefix reference `status-*` credits every row whose symbol starts with
the head — each under the same own-file-else-fan-out rule. Crediting all
of them is the honest reading: the alternative is calling live rules
unused, which is what the flag existed to avoid."""
from scribe.services.shape_ledger import resolve_consumers
css_rows = [
(1, "assets/app.css", "status-done"),
(2, "assets/app.css", "status-todo"),
(3, "v/Board.vue", "status-done"), # a scoped copy of one of them
(4, "assets/app.css", "btn-primary"),
(5, "assets/anim.css", "toast-enter-active"),
]
# A file that defines none of them fans out across every match.
assert resolve_consumers(css_rows, {"v/List.vue": {"status-*": 2}}) == {
(1, "v/List.vue"): 2, (2, "v/List.vue"): 2, (3, "v/List.vue"): 2,
}
# A file that DOES define one keeps the own-file rule, per symbol: its own
# status-done row, and the shared status-todo it does not define.
assert resolve_consumers(css_rows, {"v/Board.vue": {"status-*": 1}}) == {
(3, "v/Board.vue"): 1, (2, "v/Board.vue"): 1,
}
# A prefix that matches nothing is silent, and exact tokens are untouched.
assert resolve_consumers(css_rows, {"v/X.vue": {"zz-*": 1}}) == {}
assert resolve_consumers(css_rows, {"v/X.vue": {"btn-primary": 3}}) == {
(4, "v/X.vue"): 3,
}
# A transition class resolves exactly, like any other name.
assert resolve_consumers(css_rows, {"c/Toast.vue": {"toast-enter-active": 1}}) == {
(5, "c/Toast.vue"): 1,
}
def test_consumer_edges_table_cascades_with_the_shape():
from scribe.models import Base
from scribe.models.code_shape import CONSUMER_BASES, CodeShapeConsumer