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
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:
@@ -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 -------------------------
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user