From 57d68c935569cc1d1b78be606c3810bd2df4d7b9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 21 Aug 2026 15:08:57 -0400 Subject: [PATCH] feat(ledger): derive readout ranks body-identical groups first; CSS fingerprints are declarations, not selectors (#2872, milestone 294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 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 --- src/scribe/services/coverage.py | 9 ++++++++- src/scribe/services/shape_ledger.py | 23 +++++++++++++++++------ tests/test_pattern_coverage.py | 5 +++++ tests/test_shape_ledger.py | 26 ++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/scribe/services/coverage.py b/src/scribe/services/coverage.py index a4ee20a..a1e44fd 100644 --- a/src/scribe/services/coverage.py +++ b/src/scribe/services/coverage.py @@ -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 diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index e37a363..e0dc983 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -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]} diff --git a/tests/test_pattern_coverage.py b/tests/test_pattern_coverage.py index c8e7997..5bdd4fd 100644 --- a/tests/test_pattern_coverage.py +++ b/tests/test_pattern_coverage.py @@ -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(): diff --git a/tests/test_shape_ledger.py b/tests/test_shape_ledger.py index f63c86e..c0f923f 100644 --- a/tests/test_shape_ledger.py +++ b/tests/test_shape_ledger.py @@ -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