feat(ledger): the consumer map surfaces — every css row carries used_by on list_shapes, flag="unused-css" (the map's negative space, surfaced never deleted), derive groups and the write-path family carry consumers, the derive line says "used by N template(s)", coverage payload unused_css + the standing block; docs, SKILL, plugin 0.1.44; the two step-2 tests expected 5 edges where fan-out makes 6 (milestone 302 step 3, #2936)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Failing after 44s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 31s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Failing after 44s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 31s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,7 +173,7 @@ endpoint at `/mcp`, not these REST routes.
|
|||||||
| GET | `/api/plugin/retrieve` | Title-first knowledge-injection candidates |
|
| GET | `/api/plugin/retrieve` | Title-first knowledge-injection candidates |
|
||||||
| GET | `/api/plugin/processes` | Stored Processes for skill-stub sync |
|
| GET | `/api/plugin/processes` | Stored Processes for skill-stub sync |
|
||||||
| GET | `/api/plugin/prior-art` | Write-path hint for the plugin hooks (params: `path`, `code`, `repo`, `shapes`, `exclude_ids`, `exclude_sync_ids`, `exclude_derive`); returns `context`, `note_ids`, `sync_note_ids`, `stamped`, `divergence`, `derive`, `derive_keys` |
|
| GET | `/api/plugin/prior-art` | Write-path hint for the plugin hooks (params: `path`, `code`, `repo`, `shapes`, `exclude_ids`, `exclude_sync_ids`, `exclude_derive`); returns `context`, `note_ids`, `sync_note_ids`, `stamped`, `divergence`, `derive`, `derive_keys` |
|
||||||
| GET / POST | `/api/projects/<id>/coverage`, `…/coverage/refresh` | Shape-ledger accounting (`pattern_coverage` line, counts, `derive_groups`, `derive_new`, `divergence`, `recheck`) |
|
| GET / POST | `/api/projects/<id>/coverage`, `…/coverage/refresh` | Shape-ledger accounting (`pattern_coverage` line, counts, `derive_groups` — css groups carry `consumers`, `derive_new`, `unused_css`, `divergence`, `recheck`) |
|
||||||
| GET / PUT | `/api/plugin/marketplace-url` | Read / set the plugin marketplace URL |
|
| GET / PUT | `/api/plugin/marketplace-url` | Read / set the plugin marketplace URL |
|
||||||
|
|
||||||
## Dashboard, Export, Trash, Users
|
## Dashboard, Export, Trash, Users
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scribe",
|
"name": "scribe",
|
||||||
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
|
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"author": { "name": "Bryan Van Deusen" },
|
"author": { "name": "Bryan Van Deusen" },
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"scribe": {
|
"scribe": {
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ the last sweep left it. Three surfaces say so without anyone running an audit
|
|||||||
under different names are never a family. Derive a CSS family by moving
|
under different names are never a family. Derive a CSS family by moving
|
||||||
the recipe to the shared sheet and recording it; a class name reused for
|
the recipe to the shared sheet and recording it; a class name reused for
|
||||||
genuinely different things is dismissed with `reason_code="scoped-css"`.
|
genuinely different things is dismissed with `reason_code="scoped-css"`.
|
||||||
|
The datum that decides between the two is **what renders it**: every css
|
||||||
|
row carries `used_by` (the files whose markup names the class — the CSS
|
||||||
|
consumer map, milestone 302), a derive group carries the family's
|
||||||
|
`consumers`, and the write-path line says "used by N template(s)". Many
|
||||||
|
templates, one recipe → derive; one template each, different purposes →
|
||||||
|
dismiss. `list_shapes(flag="unused-css")` is the map's negative space —
|
||||||
|
css rules no template names, a deletion candidate to look at, never
|
||||||
|
auto-deleted (a class built at runtime is invisible to the map).
|
||||||
|
|
||||||
After the one-time pay-down the derive queue reads empty; anything in it
|
After the one-time pay-down the derive queue reads empty; anything in it
|
||||||
afterwards is drift of the moment, and the hint already said so at the write.
|
afterwards is drift of the moment, and the hint already said so at the write.
|
||||||
|
|||||||
@@ -116,10 +116,17 @@ async def list_shapes(
|
|||||||
classify it: instance if it should use the canon, variant with
|
classify it: instance if it should use the canon, variant with
|
||||||
the why if deliberate); "recheck": judged instances/variants
|
the why if deliberate); "recheck": judged instances/variants
|
||||||
whose body changed since judged (the judgment stands; confirm
|
whose body changed since judged (the judgment stands; confirm
|
||||||
it again with classify_shapes, or re-judge).
|
it again with classify_shapes, or re-judge); "unused-css"
|
||||||
|
(milestone 302): live css rules no file's markup names — a
|
||||||
|
deletion candidate to look at, never auto-deleted (the map reads
|
||||||
|
templates only; a class built at runtime is invisible to it).
|
||||||
|
|
||||||
Returns {"shapes": [...], "total": N} — total counts every match, not
|
Returns {"shapes": [...], "total": N} — total counts every match, not
|
||||||
just this page. Each row's `classified_by` says who judged: agent /
|
just this page. Every css row carries `used_by` {count, paths} — the
|
||||||
|
files whose markup names its class (milestone 302, the CSS consumer
|
||||||
|
map: a scoped rule is used by its own template; a shared recipe by
|
||||||
|
many; a count of 0 is "no template names it"). Each row's
|
||||||
|
`classified_by` says who judged: agent /
|
||||||
audit / import are judgments; `mechanical` is the canonical stamp the
|
audit / import are judgments; `mechanical` is the canonical stamp the
|
||||||
sync applies; `hook` is write-path EVIDENCE (#2791) — the session pulled
|
sync applies; `hook` is write-path EVIDENCE (#2791) — the session pulled
|
||||||
a snippet and then wrote code referencing/resembling it, so the shape
|
a snippet and then wrote code referencing/resembling it, so the shape
|
||||||
@@ -145,10 +152,12 @@ async def list_shapes(
|
|||||||
include_vanished=include_vanished, limit=limit, offset=offset,
|
include_vanished=include_vanished, limit=limit, offset=offset,
|
||||||
proposal=proposal, flag=flag, uses=uses,
|
proposal=proposal, flag=flag, uses=uses,
|
||||||
)
|
)
|
||||||
return {
|
shapes = [r.to_compact() if compact else r.to_dict() for r in rows]
|
||||||
"shapes": [r.to_compact() if compact else r.to_dict() for r in rows],
|
used_by = await shape_ledger_svc.used_by_map(rows)
|
||||||
"total": total,
|
for row, out in zip(rows, shapes):
|
||||||
}
|
if row.id in used_by:
|
||||||
|
out["used_by"] = used_by[row.id]
|
||||||
|
return {"shapes": shapes, "total": total}
|
||||||
|
|
||||||
|
|
||||||
async def classify_shapes_by_rule(
|
async def classify_shapes_by_rule(
|
||||||
@@ -295,7 +304,10 @@ async def refresh_pattern_coverage(project_id: int) -> dict:
|
|||||||
Returns the accounting payload — total, accounted, counts by status,
|
Returns the accounting payload — total, accounted, counts by status,
|
||||||
unclassified, repos, largest_gaps, `proposed` (canon proposals awaiting
|
unclassified, repos, largest_gaps, `proposed` (canon proposals awaiting
|
||||||
confirmation), `derive_groups` (the biggest repeats-with-no-canon
|
confirmation), `derive_groups` (the biggest repeats-with-no-canon
|
||||||
families), `derive_new` (copies that joined a family since the previous
|
families, each css one with `consumers` — the files whose markup
|
||||||
|
render it, milestone 302), `unused_css` (css rules no template names;
|
||||||
|
None where the map has no evidence of templates), `derive_new` (copies
|
||||||
|
that joined a family since the previous
|
||||||
refresh — the drift to act on now: derive the canon, don't queue an
|
refresh — the drift to act on now: derive the canon, don't queue an
|
||||||
audit), `proposer` (what this refresh examined) — plus
|
audit), `proposer` (what this refresh examined) — plus
|
||||||
`pattern_coverage`, the same one-line summary enter_project carries.
|
`pattern_coverage`, the same one-line summary enter_project carries.
|
||||||
|
|||||||
@@ -629,7 +629,23 @@ async def compute_coverage(
|
|||||||
agg["accounted"] += row.status != "unclassified"
|
agg["accounted"] += row.status != "unclassified"
|
||||||
|
|
||||||
unclassified = counts.pop("unclassified")
|
unclassified = counts.pop("unclassified")
|
||||||
proposals = shape_ledger.proposal_summary(rows)
|
# The CSS consumer map's readout (milestone 302): which files render each
|
||||||
|
# css row — on the derive groups (a shared recipe vs a scoped one is a
|
||||||
|
# count), and the negative space: css rules no template names. "Unused"
|
||||||
|
# is measured only where the map has evidence of templates at all (one
|
||||||
|
# edge somewhere); a repo of bare stylesheets is "not measured", not
|
||||||
|
# "all unused".
|
||||||
|
css_rows = [r for r in rows if r.kind == "css"]
|
||||||
|
consumer_paths: dict[int, list[str]] = {}
|
||||||
|
unused_css = None
|
||||||
|
try:
|
||||||
|
edges = await shape_ledger.consumers_of([r.id for r in css_rows])
|
||||||
|
consumer_paths = {sid: [e.path for e in es] for sid, es in edges.items()}
|
||||||
|
if consumer_paths:
|
||||||
|
unused_css = sum(1 for r in css_rows if r.id not in consumer_paths)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("consumer map read failed", exc_info=True)
|
||||||
|
proposals = shape_ledger.proposal_summary(rows, consumer_paths=consumer_paths)
|
||||||
divergence = shape_ledger.divergence_summary(rows)
|
divergence = shape_ledger.divergence_summary(rows)
|
||||||
derive_new = shape_ledger.derive_new_summary(rows, since=since)
|
derive_new = shape_ledger.derive_new_summary(rows, since=since)
|
||||||
return {
|
return {
|
||||||
@@ -646,6 +662,9 @@ async def compute_coverage(
|
|||||||
# duplicate family — what the arrival line names so drift is noticed
|
# duplicate family — what the arrival line names so drift is noticed
|
||||||
# on entering, not found by an audit.
|
# on entering, not found by an audit.
|
||||||
"derive_new": derive_new,
|
"derive_new": derive_new,
|
||||||
|
# The consumer map's negative space (milestone 302): live css rules
|
||||||
|
# no template names — None when the map has no evidence of templates.
|
||||||
|
"unused_css": unused_css,
|
||||||
"proposer": proposer_stats,
|
"proposer": proposer_stats,
|
||||||
# The divergence readout (#2793): button B where button A is canon,
|
# The divergence readout (#2793): button B where button A is canon,
|
||||||
# and judged shapes whose bodies moved since they were judged.
|
# and judged shapes whose bodies moved since they were judged.
|
||||||
@@ -823,7 +842,16 @@ def coverage_line(coverage: dict) -> str:
|
|||||||
standing.append(f"top canon #{top['snippet_id']} ×{top.get('count', 0)}")
|
standing.append(f"top canon #{top['snippet_id']} ×{top.get('count', 0)}")
|
||||||
first = (coverage.get("derive_groups") or [{}])[0]
|
first = (coverage.get("derive_groups") or [{}])[0]
|
||||||
if first.get("label") and first.get("files"):
|
if first.get("label") and first.get("files"):
|
||||||
standing.append(f"top copy {first['label']} ×{first['files']} files")
|
top_copy = f"top copy {first['label']} ×{first['files']} files"
|
||||||
|
# A css family says what renders it (milestone 302): the count that
|
||||||
|
# tells a shared recipe from a scoped convention.
|
||||||
|
if "consumers" in first:
|
||||||
|
n_t = (first.get("consumers") or {}).get("count", 0)
|
||||||
|
top_copy += f" · used by {n_t} template{'s' if n_t != 1 else ''}"
|
||||||
|
standing.append(top_copy)
|
||||||
|
if coverage.get("unused_css"):
|
||||||
|
n_u = coverage["unused_css"]
|
||||||
|
standing.append(f"{n_u} unused class{'es' if n_u != 1 else ''}")
|
||||||
if unclassified:
|
if unclassified:
|
||||||
line += f"; {unclassified} unclassified"
|
line += f"; {unclassified} unclassified"
|
||||||
if standing:
|
if standing:
|
||||||
|
|||||||
@@ -1073,6 +1073,18 @@ def _derive_line(path: str, derive: list[dict]) -> str:
|
|||||||
# files. CSS is only ever grouped this way (note 2917) — a class
|
# files. CSS is only ever grouped this way (note 2917) — a class
|
||||||
# is a recipe, and the recipe is what gets derived or dismissed.
|
# is a recipe, and the recipe is what gets derived or dismissed.
|
||||||
what = f"is a repeated name with no canon — defined in {n} other file(s)"
|
what = f"is a repeated name with no canon — defined in {n} other file(s)"
|
||||||
|
# What renders a css family (milestone 302): the consumer count is
|
||||||
|
# the datum that separates a shared recipe from a scoped convention.
|
||||||
|
cons = f.get("consumers")
|
||||||
|
if cons is not None:
|
||||||
|
n_t = cons.get("count", 0)
|
||||||
|
used = f"; used by {n_t} template{'s' if n_t != 1 else ''}"
|
||||||
|
if cons.get("paths"):
|
||||||
|
used += ": " + ", ".join(f"`{x}`" for x in cons["paths"])
|
||||||
|
extra = n_t - len(cons["paths"])
|
||||||
|
if extra > 0:
|
||||||
|
used += f" +{extra} more"
|
||||||
|
files += used
|
||||||
# The dismissal reason the family most likely earns: a class name
|
# The dismissal reason the family most likely earns: a class name
|
||||||
# reused for different purposes is scoped styling; a code name reused
|
# reused for different purposes is scoped styling; a code name reused
|
||||||
# across modules is convention plumbing.
|
# across modules is convention plumbing.
|
||||||
|
|||||||
@@ -354,6 +354,28 @@ async def consumers_of(shape_ids) -> dict[int, list[CodeShapeConsumer]]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# How many consumer files a readout names before "+N more".
|
||||||
|
_CONSUMERS_SHOWN = 4
|
||||||
|
|
||||||
|
|
||||||
|
def consumer_summary(paths: Iterable[str]) -> dict:
|
||||||
|
"""{"count", "paths"} — distinct consumer files, sorted, the first few
|
||||||
|
named. The one shape every surface uses for "used by N template(s)"."""
|
||||||
|
files = sorted(set(paths))
|
||||||
|
return {"count": len(files), "paths": files[:_CONSUMERS_SHOWN]}
|
||||||
|
|
||||||
|
|
||||||
|
async def used_by_map(rows: Iterable[CodeShape]) -> dict[int, dict]:
|
||||||
|
"""{shape_id: consumer_summary} for every css row given — a row with no
|
||||||
|
consumer gets {"count": 0, "paths": []}: "no template names it" is a
|
||||||
|
finding, not an absence."""
|
||||||
|
css = [r for r in rows if r.kind == "css"]
|
||||||
|
if not css:
|
||||||
|
return {}
|
||||||
|
edges = await consumers_of([r.id for r in css])
|
||||||
|
return {r.id: consumer_summary(e.path for e in edges.get(r.id, [])) for r in css}
|
||||||
|
|
||||||
|
|
||||||
async def mark_canonicals(
|
async def mark_canonicals(
|
||||||
project_id: int, recorded: list[tuple[int, str, str]]
|
project_id: int, recorded: list[tuple[int, str, str]]
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -666,7 +688,8 @@ async def list_project_shapes(
|
|||||||
suggestion), "derive" (a repeats-with-no-canon group), or one basis
|
suggestion), "derive" (a repeats-with-no-canon group), or one basis
|
||||||
name (symbol/reference/text/signature/semantic). ``flag`` narrows to
|
name (symbol/reference/text/signature/semantic). ``flag`` narrows to
|
||||||
the readout's asks (#2793): "divergence" (new where a canon dominates,
|
the readout's asks (#2793): "divergence" (new where a canon dominates,
|
||||||
`diverges_from` names it) or "recheck" (a judged shape whose body moved).
|
`diverges_from` names it), "recheck" (a judged shape whose body moved),
|
||||||
|
or "unused-css" (milestone 302: a css rule no template names).
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
|
|
||||||
@@ -701,6 +724,13 @@ async def list_project_shapes(
|
|||||||
conds.append(CodeShape.diverges_from.isnot(None))
|
conds.append(CodeShape.diverges_from.isnot(None))
|
||||||
elif flag == "recheck":
|
elif flag == "recheck":
|
||||||
conds.append(CodeShape.recheck_at.isnot(None))
|
conds.append(CodeShape.recheck_at.isnot(None))
|
||||||
|
elif flag == "unused-css":
|
||||||
|
# The consumer map's negative space (milestone 302): a live css rule
|
||||||
|
# no file's markup names. A candidate for deletion, surfaced — never
|
||||||
|
# deleted — because the map reads templates only (a class built at
|
||||||
|
# runtime, or used from a script, is invisible to it).
|
||||||
|
conds.append(CodeShape.kind == "css")
|
||||||
|
conds.append(~CodeShape.id.in_(select(CodeShapeConsumer.shape_id)))
|
||||||
if uses:
|
if uses:
|
||||||
# Consumers of a canon (#2870): rows with a uses edge to it, whatever
|
# Consumers of a canon (#2870): rows with a uses edge to it, whatever
|
||||||
# shape they themselves are.
|
# shape they themselves are.
|
||||||
@@ -1461,13 +1491,21 @@ async def apply_derive_groups(project_id: int) -> int:
|
|||||||
return grouped
|
return grouped
|
||||||
|
|
||||||
|
|
||||||
def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
def proposal_summary(
|
||||||
|
rows: Iterable[CodeShape], *, top: int = 8,
|
||||||
|
consumer_paths: dict[int, list[str]] | None = None,
|
||||||
|
) -> dict:
|
||||||
"""The readout's view of the proposer's standing: how many canon
|
"""The readout's view of the proposer's standing: how many canon
|
||||||
proposals await confirmation, and the largest derive-first groups."""
|
proposals await confirmation, and the largest derive-first groups.
|
||||||
|
``consumer_paths`` (shape_id → files whose markup names it, milestone
|
||||||
|
302) puts `consumers` on each group — the family's distinct consumer
|
||||||
|
files across its members, the datum that separates a shared recipe
|
||||||
|
from a scoped convention."""
|
||||||
proposed = 0
|
proposed = 0
|
||||||
by_canon: dict[int, int] = {}
|
by_canon: dict[int, int] = {}
|
||||||
groups: dict[str, dict] = {}
|
groups: dict[str, dict] = {}
|
||||||
files: dict[str, set[str]] = {}
|
files: dict[str, set[str]] = {}
|
||||||
|
consumers: dict[str, set[str]] = {}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if row.status not in _MECHANICAL_TODO:
|
if row.status not in _MECHANICAL_TODO:
|
||||||
continue
|
continue
|
||||||
@@ -1488,8 +1526,14 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
|||||||
files.setdefault(row.proposal_group, set()).add(row.path)
|
files.setdefault(row.proposal_group, set()).add(row.path)
|
||||||
if len(g["paths"]) < 3:
|
if len(g["paths"]) < 3:
|
||||||
g["paths"].append(row.path)
|
g["paths"].append(row.path)
|
||||||
|
if consumer_paths is not None and row.kind == "css":
|
||||||
|
consumers.setdefault(row.proposal_group, set()).update(
|
||||||
|
consumer_paths.get(row.id) or ()
|
||||||
|
)
|
||||||
for key, g in groups.items():
|
for key, g in groups.items():
|
||||||
g["files"] = len(files[key])
|
g["files"] = len(files[key])
|
||||||
|
if key in consumers:
|
||||||
|
g["consumers"] = consumer_summary(consumers[key])
|
||||||
# Body-identical groups first (#2872): the things an audit actually
|
# Body-identical groups first (#2872): the things an audit actually
|
||||||
# consolidated were identical bodies under different names/files; a
|
# consolidated were identical bodies under different names/files; a
|
||||||
# name repeated across modules is usually convention. Within a tier,
|
# name repeated across modules is usually convention. Within a tier,
|
||||||
@@ -1753,15 +1797,21 @@ async def write_time_derive(
|
|||||||
group = grouped[0].proposal_group
|
group = grouped[0].proposal_group
|
||||||
members = [r for r in grouped if r.proposal_group == group]
|
members = [r for r in grouped if r.proposal_group == group]
|
||||||
files = sorted({r.path for r in members})
|
files = sorted({r.path for r in members})
|
||||||
out.append({
|
family = {
|
||||||
"symbol": name, "kind": kind, "key": group,
|
"group": group, "label": label,
|
||||||
"family": {
|
"identical": not group.startswith("name:"),
|
||||||
"group": group, "label": label,
|
"files": files[:_DERIVE_FILES_SHOWN], "file_count": len(files),
|
||||||
"identical": not group.startswith("name:"),
|
"size": len(members) + (1 if here is not None else 0),
|
||||||
"files": files[:_DERIVE_FILES_SHOWN], "file_count": len(files),
|
}
|
||||||
"size": len(members) + (1 if here is not None else 0),
|
if kind == "css":
|
||||||
},
|
# What renders the family (milestone 302): the members' consumer
|
||||||
})
|
# files, the row at `path` included when it already exists.
|
||||||
|
ids = [r.id for r in members] + ([here.id] if here is not None else [])
|
||||||
|
edges = await consumers_of(ids)
|
||||||
|
family["consumers"] = consumer_summary(
|
||||||
|
e.path for es in edges.values() for e in es
|
||||||
|
)
|
||||||
|
out.append({"symbol": name, "kind": kind, "key": group, "family": family})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -703,11 +703,13 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
|||||||
"v/B.vue": {"error-msg": 1},
|
"v/B.vue": {"error-msg": 1},
|
||||||
"v/C.vue": {"error-msg": 1, "btn-primary": 4},
|
"v/C.vue": {"error-msg": 1, "btn-primary": 4},
|
||||||
}
|
}
|
||||||
assert await sync_repo_consumers(pid, REPO, refs) == 5
|
# A and B consume their OWN error-msg; C defines none, so its use fans
|
||||||
|
# out to both rows; btn-primary resolves to the shared sheet from A and C.
|
||||||
|
assert await sync_repo_consumers(pid, REPO, refs) == 6
|
||||||
rows = {(r.path, r.symbol): r.id for r in await live_rows(pid) if r.kind == "css"}
|
rows = {(r.path, r.symbol): r.id for r in await live_rows(pid) if r.kind == "css"}
|
||||||
edges = await consumers_of(rows.values())
|
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()}
|
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/A.vue", "error-msg")] == [("v/A.vue", 2), ("v/C.vue", 1)]
|
||||||
assert view[("v/B.vue", "error-msg")] == [("v/B.vue", 1), ("v/C.vue", 1)]
|
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[("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
|
assert view[("web/button.css", "btn")] == [] # the seeded row: no template names it
|
||||||
@@ -719,6 +721,22 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
|||||||
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[("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)]
|
assert [(e.path, e.count) for e in edges[rows[("assets/components.css", "btn-primary")]]] == [("v/A.vue", 2)]
|
||||||
|
|
||||||
|
# The readout side: used_by per css row, the unused-css flag, and the
|
||||||
|
# family's consumers on the write-path check.
|
||||||
|
from scribe.services.shape_ledger import (
|
||||||
|
apply_derive_groups, used_by_map, write_time_derive,
|
||||||
|
)
|
||||||
|
owner = seeded["owner"]
|
||||||
|
live = [r for r in await live_rows(pid) if r.kind == "css"]
|
||||||
|
used = await used_by_map(live)
|
||||||
|
assert used[rows[("v/B.vue", "error-msg")]] == {"count": 1, "paths": ["v/B.vue"]}
|
||||||
|
assert used[rows[("web/button.css", "btn")]] == {"count": 0, "paths": []}
|
||||||
|
unused, n = await list_project_shapes(owner, pid, flag="unused-css")
|
||||||
|
assert n == 1 and [(r.path, r.symbol) for r in unused] == [("web/button.css", "btn")]
|
||||||
|
await apply_derive_groups(pid)
|
||||||
|
out = await write_time_derive(pid, "v/New.vue", [("css", "error-msg")])
|
||||||
|
assert out and out[0]["family"]["consumers"] == {"count": 2, "paths": ["v/A.vue", "v/B.vue"]}
|
||||||
|
|
||||||
# B's rule vanishes from the tree → its edges go with the pass.
|
# 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_shapes(pid, REPO, [d for d in defs if d[0] != "v/B.vue"], seen_marker="m2")
|
||||||
await sync_repo_consumers(pid, REPO, refs2)
|
await sync_repo_consumers(pid, REPO, refs2)
|
||||||
|
|||||||
@@ -604,6 +604,17 @@ def test_coverage_line_names_the_proposers_standing():
|
|||||||
assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
|
assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
|
||||||
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
|
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
|
||||||
assert "(1 derive group)" in line
|
assert "(1 derive group)" in line
|
||||||
|
# Milestone 302: a css top copy says what renders it; unused classes
|
||||||
|
# join the standing block only when measured (None = no evidence).
|
||||||
|
line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [
|
||||||
|
{"group": "name:css:error-msg", "label": ".error-msg", "files": 6,
|
||||||
|
"consumers": {"count": 6, "paths": ["a.vue"]}}], "unused_css": 3})
|
||||||
|
assert "top copy .error-msg ×6 files · used by 6 templates" in line
|
||||||
|
assert "3 unused classes" in line
|
||||||
|
line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [
|
||||||
|
{"group": "name:css:x", "label": ".x", "files": 2,
|
||||||
|
"consumers": {"count": 1, "paths": ["a.vue"]}}], "unused_css": None})
|
||||||
|
assert "top copy .x ×2 files · used by 1 template" in line and "unused" not in line
|
||||||
# #2874: the next action on the line — biggest canon queue, widest copy.
|
# #2874: the next action on the line — biggest canon queue, widest copy.
|
||||||
line = coverage_line({
|
line = coverage_line({
|
||||||
**base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78},
|
**base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78},
|
||||||
|
|||||||
@@ -380,6 +380,17 @@ def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows
|
|||||||
row("v/J.vue", "closed-msg", "dup:abc", status="exempt"),
|
row("v/J.vue", "closed-msg", "dup:abc", status="exempt"),
|
||||||
]
|
]
|
||||||
out = proposal_summary(rows)
|
out = proposal_summary(rows)
|
||||||
|
# Milestone 302: with consumer paths in hand, each css group says what
|
||||||
|
# renders it — distinct files across the members; absent otherwise.
|
||||||
|
assert "consumers" not in out["derive_groups"][0]
|
||||||
|
for i, r in enumerate(rows): # unsaved rows have no id; give them one
|
||||||
|
r.id = i + 1
|
||||||
|
cpaths = {rows[0].id: ["v/0.vue", "v/Z.vue"], rows[1].id: ["v/1.vue"], rows[2].id: ["v/0.vue"]}
|
||||||
|
with_c = proposal_summary(rows, consumer_paths=cpaths)
|
||||||
|
badge = next(g for g in with_c["derive_groups"] if g["group"] == "name:css:status-badge")
|
||||||
|
assert badge["consumers"] == {"count": 3, "paths": ["v/0.vue", "v/1.vue", "v/Z.vue"]}
|
||||||
|
dup = next(g for g in with_c["derive_groups"] if g["group"] == "dup:abc")
|
||||||
|
assert dup["consumers"] == {"count": 0, "paths": []}
|
||||||
assert [g["group"] for g in out["derive_groups"]] == ["dup:abc", "dup:def", "name:css:status-badge"]
|
assert [g["group"] for g in out["derive_groups"]] == ["dup:abc", "dup:def", "name:css:status-badge"]
|
||||||
assert out["derive_groups"][0]["files"] == 3 and out["derive_groups"][0]["size"] == 3
|
assert out["derive_groups"][0]["files"] == 3 and out["derive_groups"][0]["size"] == 3
|
||||||
assert out["derive_groups"][0]["label"] == "closed-msg (identical body)"
|
assert out["derive_groups"][0]["label"] == "closed-msg (identical body)"
|
||||||
@@ -455,7 +466,7 @@ def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
|
|||||||
(1, "v/A.vue"): 2, # own row, not B's
|
(1, "v/A.vue"): 2, # own row, not B's
|
||||||
(3, "v/A.vue"): 1, # the shared sheet
|
(3, "v/A.vue"): 1, # the shared sheet
|
||||||
(2, "v/B.vue"): 1, # own row
|
(2, "v/B.vue"): 1, # own row
|
||||||
(2, "v/C.vue"): 1, # C defines nothing → the other file's row
|
(1, "v/C.vue"): 1, (2, "v/C.vue"): 1, # C defines none → every other definition
|
||||||
(4, "v/C.vue"): 3, (5, "v/C.vue"): 3, # ambiguous: both, not a guess
|
(4, "v/C.vue"): 3, (5, "v/C.vue"): 3, # ambiguous: both, not a guess
|
||||||
}
|
}
|
||||||
# Unknown tokens and an unreferenced row leave no trace.
|
# Unknown tokens and an unreferenced row leave no trace.
|
||||||
|
|||||||
@@ -1306,7 +1306,8 @@ async def test_the_write_time_derive_check_names_a_family_or_a_canon_in_band():
|
|||||||
{"symbol": "log-empty", "kind": "css", "key": "name:css:log-empty",
|
{"symbol": "log-empty", "kind": "css", "key": "name:css:log-empty",
|
||||||
"family": {"group": "name:css:log-empty", "label": ".log-empty", "identical": False,
|
"family": {"group": "name:css:log-empty", "label": ".log-empty", "identical": False,
|
||||||
"files": ["a/TaskLogSection.vue", "a/WorkspaceTaskPanel.vue"],
|
"files": ["a/TaskLogSection.vue", "a/WorkspaceTaskPanel.vue"],
|
||||||
"file_count": 5, "size": 6}},
|
"file_count": 5, "size": 6,
|
||||||
|
"consumers": {"count": 6, "paths": ["a/TaskLogSection.vue", "a/V.vue"]}}},
|
||||||
{"symbol": "btn-primary", "kind": "css", "key": "canon:2855",
|
{"symbol": "btn-primary", "kind": "css", "key": "canon:2855",
|
||||||
"canon": {"snippet_id": 2855, "path": "frontend/src/assets/components.css",
|
"canon": {"snippet_id": 2855, "path": "frontend/src/assets/components.css",
|
||||||
"label": ".btn-primary"}},
|
"label": ".btn-primary"}},
|
||||||
@@ -1344,8 +1345,10 @@ async def test_the_write_time_derive_check_names_a_family_or_a_canon_in_band():
|
|||||||
assert "Shape ledger at `frontend/src/components/New.vue`" in ctx
|
assert "Shape ledger at `frontend/src/components/New.vue`" in ctx
|
||||||
# A CSS family is a repeated NAME (note 2917) and its dismissal is scoped-css;
|
# A CSS family is a repeated NAME (note 2917) and its dismissal is scoped-css;
|
||||||
# a code dup family is an identical body and dismisses as convention-plumbing.
|
# a code dup family is an identical body and dismisses as convention-plumbing.
|
||||||
|
# A css family says what renders it (milestone 302) before the ask.
|
||||||
assert "`.log-empty` is a repeated name with no canon — defined in 5 other file(s): " \
|
assert "`.log-empty` is a repeated name with no canon — defined in 5 other file(s): " \
|
||||||
"`a/TaskLogSection.vue`, `a/WorkspaceTaskPanel.vue` +3 more; derive it now" in ctx
|
"`a/TaskLogSection.vue`, `a/WorkspaceTaskPanel.vue` +3 more; used by 6 templates: " \
|
||||||
|
"`a/TaskLogSection.vue`, `a/V.vue` +4 more; derive it now" in ctx
|
||||||
assert "`slugify` is a duplicate family with no canon — identical body in 2 other file(s): " \
|
assert "`slugify` is a duplicate family with no canon — identical body in 2 other file(s): " \
|
||||||
"`a/x.py`, `a/y.py`; derive it now" in ctx
|
"`a/x.py`, `a/y.py`; derive it now" in ctx
|
||||||
assert "`.btn-primary` is canon — snippet #2855 at `frontend/src/assets/components.css`" in ctx
|
assert "`.btn-primary` is canon — snippet #2855 at `frontend/src/assets/components.css`" in ctx
|
||||||
|
|||||||
Reference in New Issue
Block a user