feat(ledger): derive readout ranks body-identical groups first; CSS fingerprints are declarations, not selectors (#2872, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 32s

The 2026-08 audit consolidated identical bodies under different names and
files (five auth views' CSS, two Workspace formatDate()s, four modal blocks)
while the derive readout led with name groups (to_dict ×25, main ×5, load ×6)
that were convention or coincidence.

- proposal_summary ranks dup:<sha> groups above name groups, wider file spread
  first, and carries `files`; scoped rows (#2869) are in the readout, since
  that is where view-level copies live.
- extract_definitions fingerprints a CSS rule by its declarations — the row
  identity already carries the selector — so .closed-msg / .error-block /
  .success-msg with one body are one dup group. One-time effect: judged css
  rows whose stored fingerprint predates this may show a recheck on the next
  refresh (the judgment stands; re-confirm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:08:57 -04:00
co-authored by Claude Fable 5
parent 1126bbe84f
commit 57d68c9355
4 changed files with 56 additions and 7 deletions
+8 -1
View File
@@ -185,8 +185,15 @@ def extract_definitions(text: str) -> list[Definition]:
end = j
break
block = lines[i:end]
# A CSS rule's fingerprint is its DECLARATIONS, not its selector
# (#2872): the row's identity already carries the selector, and the
# question the fingerprint answers for derive grouping is "is this the
# same rule under another name?" — .closed-msg / .error-block /
# .success-msg with identical bodies are one dup group, not three
# lonely rows. Sym blocks keep their signature line in the hash.
hashed = block[1:] if kind == "css" and len(block) > 1 else block
out.append(Definition(
kind, name, lines[i].strip()[:_SIGNATURE_CAP], _block_sha(block),
kind, name, lines[i].strip()[:_SIGNATURE_CAP], _block_sha(hashed),
"\n".join(block), i,
))
return out
+17 -6
View File
@@ -1215,25 +1215,36 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
proposals await confirmation, and the largest derive-first groups."""
proposed = 0
groups: dict[str, dict] = {}
files: dict[str, set[str]] = {}
for row in rows:
if row.status != "unclassified":
if row.status not in _MECHANICAL_TODO:
continue
if row.proposed_snippet_id is not None:
proposed += 1
elif row.proposal_group:
dup = not row.proposal_group.startswith("name:")
g = groups.setdefault(row.proposal_group, {
"group": row.proposal_group, "kind": row.kind,
"label": (
("." if row.kind == "css" else "") + row.symbol
if row.proposal_group.startswith("name:")
else f"{row.symbol} (identical body)"
f"{row.symbol} (identical body)" if dup
else ("." if row.kind == "css" else "") + row.symbol
),
"size": 0, "paths": [],
"size": 0, "files": 0, "paths": [],
})
g["size"] += 1
files.setdefault(row.proposal_group, set()).add(row.path)
if len(g["paths"]) < 3:
g["paths"].append(row.path)
ranked = sorted(groups.values(), key=lambda g: (-g["size"], g["group"]))
for key, g in groups.items():
g["files"] = len(files[key])
# Body-identical groups first (#2872): the things an audit actually
# consolidated were identical bodies under different names/files; a
# name repeated across modules is usually convention. Within a tier,
# the group spread over more files is the bigger copy.
ranked = sorted(
groups.values(),
key=lambda g: (g["group"].startswith("name:"), -g["files"], -g["size"], g["group"]),
)
return {"proposed": proposed, "derive_groups": ranked[:top]}
+5
View File
@@ -468,6 +468,11 @@ def test_extract_definitions_fingerprints_each_block():
# And the identity view is unchanged for the hook mirror.
from scribe.services.coverage import extract_shapes
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)]
# A CSS rule's fingerprint is its declarations (#2872): the same body
# under another selector is the same shape to the derive grouping.
css = ".closed-msg {\n text-align: center;\n padding: 0.5rem 0;\n}\n.error-block {\n text-align: center;\n padding: 0.5rem 0;\n}\n.other {\n text-align: left;\n}\n"
d = {x.name: x for x in extract_definitions(css)}
assert d["closed-msg"].body_sha == d["error-block"].body_sha != d["other"].body_sha
def test_coverage_line_names_the_proposers_standing():
+26
View File
@@ -317,6 +317,32 @@ def test_derive_groups_copy_before_name_with_floors():
assert ("i.py", "sym", "one") not in g
def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows():
"""#2872: dup groups (the real copies) outrank name groups (usually
convention), wider spread first; #2869: scoped rows are in the readout."""
from scribe.models.code_shape import CodeShape
from scribe.services.shape_ledger import proposal_summary
def row(path, symbol, group, kind="css", status="scoped"):
return CodeShape(project_id=2, repo_key="r", path=path, symbol=symbol, kind=kind,
status=status, proposal_basis="derive", proposal_group=group)
rows = [
# a name group of 6 across 6 files
*[row(f"v/{i}.vue", "status-badge", "name:css:status-badge") for i in range(6)],
# a dup group of 3 across 3 files (different selector names, one body)
row("v/Login.vue", "closed-msg", "dup:abc"), row("v/Reset.vue", "error-block", "dup:abc"),
row("v/Forgot.vue", "success-msg", "dup:abc"),
# a dup group of 2 in ONE file — a copy, but not across files
row("v/A.vue", "x", "dup:def"), row("v/A.vue", "y", "dup:def"),
# judged rows never count
row("v/J.vue", "closed-msg", "dup:abc", status="exempt"),
]
out = proposal_summary(rows)
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]["label"] == "closed-msg (identical body)"
def test_confirm_requires_a_named_scope():
import asyncio