fix(ledger): one-line CSS fingerprints; proposer writes uses edges for judged rows (#2877) #123
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]},
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user