fix(ledger): one-line CSS fingerprints; proposer writes uses edges for judged rows (#2877) #123

Merged
bvandeusen merged 1 commits from dev into main 2026-08-21 17:39:48 -04:00
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():