fix(ledger): one-line CSS rules fingerprint their own declarations; the proposer writes uses edges for judged rows too (#2872, #2870)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 22s

First deploy of v26.08.21.1 showed two gaps:
- Every one-line CSS rule followed by a blank line hashed to sha1("") — the
  declarations live on the selector line, which the #2872 "declarations only"
  fingerprint dropped — so 68 unrelated one-liners across 17 files read as
  one body-identical copy at the top of the derive readout. The selector
  line's tail after "{" is now part of the hash; an all-blank remainder falls
  back to the whole block.
- The proposer only examined unjudged rows, so consumers that were already
  classified (auth.create_invitation → hash_token) never got a uses edge: 3
  edges for hash_token after the first refresh. Judged rows are now scanned
  for references (once per body), no proposal is made on them.
- 0084 migration docstring reworded: "function that …" at a line start parsed
  as a definition (extractor false positive).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:34:12 -04:00
co-authored by Claude Fable 5
parent f0c915a6bc
commit aba16583ab
5 changed files with 48 additions and 5 deletions
+3 -3
View File
@@ -5,9 +5,9 @@ Revises: 0083
Create Date: 2026-08-21
A ledger row carries ONE snippet_id: what shape this is (instance/variant of
a canon). But a shape can also CALL several canonical helpers — a service
function that is an instance of the service-function convention and a
consumer of hash_token. The 2026-08 audit had to pick one; hook evidence
a canon). But a shape can also CALL several canonical helpers — e.g. a
service function both conforming to the service-function convention and
consuming hash_token. The 2026-08 audit had to pick one; hook evidence
("pulled #N then wrote code referencing it") was stamped as instance when it
is a uses fact. This table holds the many-valued relation: shape → snippet,
with the basis and the evidence. Cascades with the shape and the snippet.
+13 -1
View File
@@ -191,7 +191,19 @@ def extract_definitions(text: str) -> list[Definition]:
# 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
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
# trailing blanks must not hash to the empty string (which grouped
# 68 unrelated one-liners as one "copy" on first deploy, #2872).
first = lines[i]
brace = first.find("{")
head = [first[brace + 1:]] if brace >= 0 and first[brace + 1:].strip() else []
hashed = head + block[1:]
if not any(x.strip() for x in hashed):
hashed = block
else:
hashed = block
out.append(Definition(
kind, name, lines[i].strip()[:_SIGNATURE_CAP], _block_sha(hashed),
"\n".join(block), i,
+12 -1
View File
@@ -1225,7 +1225,6 @@ async def propose_for_repo(
select(CodeShape).where(
CodeShape.project_id == project_id,
CodeShape.repo_key == repo_key,
CodeShape.status.in_(_MECHANICAL_TODO),
CodeShape.vanished_at.is_(None),
)
)
@@ -1239,6 +1238,18 @@ async def propose_for_repo(
examined_as = f"{body_sha}@{_PROPOSER_VERSION}"
if row.proposed_at is not None and row.proposed_sha == examined_as:
continue
if row.status not in _MECHANICAL_TODO:
# A judged row gets no proposal — but its uses edges (#2870)
# are a fact about the body, judged or not: the consumer map
# of a canon must include the call sites someone already
# classified. Mark it examined so the scan runs once per body.
used = reference_canons(row.kind, row.path, row.symbol, body, canons)
if used:
await record_uses(session, row, used, basis="reference",
evidence="proposer: body names the canon's symbol")
row.proposed_at = now
row.proposed_sha = examined_as
continue
examined += 1
group = row.proposal_group # derive grouping is reassigned below
hit = match_canon(
+13
View File
@@ -230,6 +230,19 @@ async def test_uses_edges_are_the_consumer_map(seeded):
)
assert out["classified"] == 1
assert (await list_project_shapes(owner, pid, uses=hid))[1] == 2
# The proposer writes uses edges for JUDGED rows too: Config (exempt)
# names hash_token in its body → an edge, no proposal.
from scribe.services.shape_ledger import propose_for_repo
await classify_shapes(owner, pid, [
{"path": "src/app.py", "symbol": "Config", "status": "exempt", "reason": "settings"},
])
defs = _defs(("src/app.py", "sym", "Config", "class Config:", "class Config:\n token = hash_token(raw)\n"))
with _quiet_semantic():
await propose_for_repo(owner, pid, REPO, defs)
rows, total = await list_project_shapes(owner, pid, uses=hid)
assert total == 3 and {r.symbol for r in rows} >= {"Config"}
cfg = next(r for r in rows if r.symbol == "Config")
assert cfg.status == "exempt" and cfg.proposal is None
with pytest.raises(ValueError):
await classify_shapes(owner, pid, [
{"path": "src/app.py", "symbol": "Config", "status": "exempt", "reason": "x", "uses": [999999]},
+7
View File
@@ -481,6 +481,13 @@ def test_extract_definitions_fingerprints_each_block():
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
# One-line rules hash their own declarations — never the empty string
# (first deploy grouped 68 unrelated one-liners as one copy).
one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.d {\n color: red;\n}\n"
e = {x.name: x for x in extract_definitions(one)}
import hashlib
assert e["a"].body_sha == e["b"].body_sha != e["c"].body_sha
assert e["a"].body_sha != hashlib.sha1(b"").hexdigest()[:16]
def test_coverage_line_names_the_proposers_standing():