feat(ledger): CSS derive families are names, never bodies — name floor 2 for css, dup: grouping sym-only; derive line says "repeated name" and dismisses scoped-css; plugin 0.1.41 (note #2917)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 00:21:17 -04:00
co-authored by Claude Fable 5
parent a2b377b74d
commit 85111442a6
9 changed files with 104 additions and 43 deletions
+10 -9
View File
@@ -198,11 +198,13 @@ def extract_definitions(text: str) -> list[Definition]:
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.
# (#2872): the row's identity already carries the selector. Since
# note 2917 the derive grouping no longer reads CSS bodies at all (a
# class is grouped by name only), so for CSS the fingerprint is the
# recheck identity — "did this rule's body change since it was
# judged?" — and nothing more. The shape of the hash is kept as-is on
# purpose: changing it would flip every judged CSS row to recheck on
# the next sync. Sym blocks keep their signature line in the hash.
if kind == "css":
# One-line rules (`.x { color: red; }`) carry their declarations on
# the selector line itself; a block that is only the selector plus
@@ -217,10 +219,9 @@ def extract_definitions(text: str) -> list[Definition]:
# A SINGLE declaration is not a shape (#2903): `color: var(--fs-
# text-tertiary)` under .text-muted, .task-mark and .pin-badge-auto
# is three meanings sharing one line, not three copies of one
# rule — the first pay-down found 5-file "families" of exactly
# this and nobody would consolidate them. Keep the selector in the
# hash for one-liners, so they group only with same-name copies;
# two declarations and up stay selector-agnostic.
# rule. Keep the selector in the hash for one-liners; two
# declarations and up stay selector-agnostic. (Moot for grouping
# since note 2917, kept for fingerprint stability — see above.)
elif _declaration_count(hashed) < 2:
hashed = block
else:
+14 -4
View File
@@ -1061,17 +1061,27 @@ def _derive_line(path: str, derive: list[dict]) -> str:
)
continue
f = d["family"]
how = "identical body" if f.get("identical") else "same name defined"
files = ", ".join(f"`{x}`" for x in f.get("files") or [])
more = f.get("file_count", 0) - len(f.get("files") or [])
if more > 0:
files += f" +{more} more"
n = f.get("file_count", 0)
if f.get("identical"):
what = f"is a duplicate family with no canon — identical body in {n} other file(s)"
else:
# A name family: the same definition name living in several
# files. CSS is only ever grouped this way (note 2917) — a class
# 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)"
# The dismissal reason the family most likely earns: a class name
# reused for different purposes is scoped styling; a code name reused
# across modules is convention plumbing.
dismiss = "scoped-css" if d.get("kind") == "css" else "convention-plumbing"
parts.append(
f"`{f['label']}` is a duplicate family with no canon — {how} in "
f"{f.get('file_count', 0)} other file(s): {files}; derive it now: "
f"`{f['label']}` {what}: {files}; derive it now: "
"record the canon (create_snippet) and make the copies instances "
"(classify_shapes) — or, if these are convention not copies, "
"`classify_shapes(..., status=\"exempt\", reason_code=\"convention-plumbing\")` "
f"`classify_shapes(..., status=\"exempt\", reason_code=\"{dismiss}\")` "
"dismisses the family — rather than adding another copy"
)
return f"> Shape ledger at `{path}`: " + "; ".join(parts) + "."
+19 -9
View File
@@ -923,6 +923,13 @@ async def stamp_write_path_instances(
# recur by convention, not by duplication).
_DERIVE_MIN_DUP = 2
_DERIVE_MIN_NAME = 3
# CSS is never grouped by body (note 2917): classes for different purposes
# share declarations because the style system makes them alike — `.text-muted`
# and `.pin-badge-auto` carrying the same `color: var(--fs-text-tertiary)` are
# two meanings, not two copies. A CSS family is a NAME defined in more than
# one file: that is a recipe living in several places, and two is already
# the signal (a class name is deliberate in a way `setup`/`load` are not).
_DERIVE_MIN_NAME_CSS = 2
# Semantic checks per repo per refresh — an embedding each (local fastembed),
# bounded so a 4,000-row ledger is worked through over refreshes, not in one.
_SEMANTIC_CAP = 150
@@ -1298,15 +1305,16 @@ def derive_groups(
rows: Iterable[tuple[str, str, str, str]]
) -> dict[tuple[str, str, str], str]:
"""The derive-first grouping over (path, kind, symbol, body_sha) rows
that matched no canon: {(path, kind, symbol): group_key}. Identical
bodies in ≥2 places group as `dup:<sha>`; the same name defined in ≥3
files groups as `name:<kind>:<symbol>`; a row joins at most one group,
the copy before the name."""
that matched no canon: {(path, kind, symbol): group_key}. For code
(kind `sym`) identical bodies in ≥2 places group as `dup:<sha>` and the
same name defined in ≥3 files groups as `name:sym:<symbol>`, the copy
before the name. CSS groups by name only — the same class defined in
≥2 files is `name:css:<symbol>`; its body never groups it (note 2917)."""
by_sha: dict[str, list[tuple[str, str, str]]] = {}
by_name: dict[tuple[str, str], list[tuple[str, str, str]]] = {}
for path, kind, symbol, sha in rows:
key = (path, kind, symbol)
if sha:
if sha and kind != "css":
by_sha.setdefault(sha, []).append(key)
by_name.setdefault((kind, _norm_symbol(symbol)), []).append(key)
out: dict[tuple[str, str, str], str] = {}
@@ -1315,7 +1323,8 @@ def derive_groups(
for key in keys:
out.setdefault(key, f"dup:{sha}")
for (kind, symbol), keys in by_name.items():
if len({k[0] for k in keys}) >= _DERIVE_MIN_NAME:
floor = _DERIVE_MIN_NAME_CSS if kind == "css" else _DERIVE_MIN_NAME
if len({k[0] for k in keys}) >= floor:
for key in keys:
out.setdefault(key, f"name:{kind}:{symbol}")
return out
@@ -1606,9 +1615,10 @@ async def write_time_derive(
named at ``path``, what the ledger already knows about that name
elsewhere in the project —
family the name sits in a derive-first group (identical body in N
files, or the same name in ≥3): "this is a known duplicate
family with no canon — derive it now, don't add a copy";
family the name sits in a derive-first group (code: identical body
in N files or the same name in ≥3; CSS: the same class in
≥2 files, note 2917): "this is a known family with no canon
— derive it now, don't add a copy";
canon a `canonical` row of that name at another path: "this is
canon #N at <path> — reuse, don't redefine".