feat(ledger): the CSS consumer map — code_shape_consumers edges (shape → file whose markup names the class, count), migration 0086, resolve_consumers (own-file row when the template defines the class, else every other definition), sync_repo_consumers rebuilt from the archive on every refresh, consumers_of; derived, so not backed up (milestone 302 step 2, #2935)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 52s
CI & Build / Build & push image (push) Skipped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 13:59:55 -04:00
co-authored by Claude Fable 5
parent dffbf43d84
commit ffbdf19116
8 changed files with 269 additions and 3 deletions
+43
View File
@@ -683,6 +683,49 @@ async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(se
assert derive_new_summary(rows, since=None)["count"] == 0
@pytest.mark.integration
async def test_consumer_map_syncs_edges_from_template_references(seeded):
"""Milestone 302: the consumer edges follow the archive — own-file
resolution for a scoped class, fan-out to the shared sheet for a class a
template does not define, counts refreshed and stale edges removed on
the next sync, and a vanished row's edges gone with it."""
from scribe.services.shape_ledger import consumers_of, live_rows, sync_repo_consumers
pid = seeded["pid"]
defs = _defs(
("v/A.vue", "css", "error-msg", ".error-msg {", ".error-msg { color: red }"),
("v/B.vue", "css", "error-msg", ".error-msg {", ".error-msg { color: blue }"),
("assets/components.css", "css", "btn-primary", ".btn-primary {", ".btn-primary { x: 1 }"),
)
await sync_repo_shapes(pid, REPO, defs, seen_marker="m1")
refs = {
"v/A.vue": {"error-msg": 2, "btn-primary": 1},
"v/B.vue": {"error-msg": 1},
"v/C.vue": {"error-msg": 1, "btn-primary": 4},
}
assert await sync_repo_consumers(pid, REPO, refs) == 5
rows = {(r.path, r.symbol): r.id for r in await live_rows(pid) if r.kind == "css"}
edges = await consumers_of(rows.values())
view = {(p, s): [(e.path, e.count) for e in edges.get(i, [])] for (p, s), i in rows.items()}
assert view[("v/A.vue", "error-msg")] == [("v/A.vue", 2)]
assert view[("v/B.vue", "error-msg")] == [("v/B.vue", 1), ("v/C.vue", 1)]
assert view[("assets/components.css", "btn-primary")] == [("v/A.vue", 1), ("v/C.vue", 4)]
assert view[("web/button.css", "btn")] == [] # the seeded row: no template names it
# The next tree: C stops using error-msg, A uses btn-primary twice now.
refs2 = {"v/A.vue": {"error-msg": 2, "btn-primary": 2}, "v/B.vue": {"error-msg": 1}}
assert await sync_repo_consumers(pid, REPO, refs2) == 3
edges = await consumers_of(rows.values())
assert [(e.path, e.count) for e in edges[rows[("v/B.vue", "error-msg")]]] == [("v/B.vue", 1)]
assert [(e.path, e.count) for e in edges[rows[("assets/components.css", "btn-primary")]]] == [("v/A.vue", 2)]
# B's rule vanishes from the tree → its edges go with the pass.
await sync_repo_shapes(pid, REPO, [d for d in defs if d[0] != "v/B.vue"], seen_marker="m2")
await sync_repo_consumers(pid, REPO, refs2)
edges = await consumers_of(rows.values())
assert rows[("v/B.vue", "error-msg")] not in edges
# --- #2793: the divergence readout against real rows -------------------------
+38
View File
@@ -433,6 +433,44 @@ def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
assert noisy not in compact
def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
"""Milestone 302: a class named in a template resolves to that file's
OWN row when it defines the class (a scoped rule, consumed by its own
markup); otherwise to every other definition of the name — one shared
sheet, or all of several (the map fans out rather than guessing)."""
from scribe.services.shape_ledger import resolve_consumers
css_rows = [
(1, "v/A.vue", "error-msg"), # scoped, defined + used in A
(2, "v/B.vue", "error-msg"), # scoped, defined in B, used in B and C
(3, "assets/components.css", "btn-primary"), # the shared sheet
(4, "assets/a.css", "pill"), (5, "assets/b.css", "pill"), # two shared defs
(6, "assets/c.css", "unused"),
]
refs = {
"v/A.vue": {"error-msg": 2, "btn-primary": 1, "nothing-defined": 1},
"v/B.vue": {"error-msg": 1},
"v/C.vue": {"error-msg": 1, "pill": 3},
}
assert resolve_consumers(css_rows, refs) == {
(1, "v/A.vue"): 2, # own row, not B's
(3, "v/A.vue"): 1, # the shared sheet
(2, "v/B.vue"): 1, # own row
(2, "v/C.vue"): 1, # C defines nothing → the other file's row
(4, "v/C.vue"): 3, (5, "v/C.vue"): 3, # ambiguous: both, not a guess
}
# Unknown tokens and an unreferenced row leave no trace.
assert all(sid != 6 for sid, _ in resolve_consumers(css_rows, refs))
def test_consumer_edges_table_cascades_with_the_shape():
from scribe.models import Base
from scribe.models.code_shape import CONSUMER_BASES, CodeShapeConsumer
assert "code_shape_consumers" in Base.metadata.tables
cols = CodeShapeConsumer.__table__.c
assert next(iter(cols.shape_id.foreign_keys)).ondelete == "CASCADE"
assert CONSUMER_BASES == ("template",)
def test_uses_edges_table_and_validation():
"""#2870: consumption is its own relation — a table that cascades with
both ends, and `uses` on a classification must be a list of ids."""