fix(ledger): a canon may hold a class and the to_dict beside it (#4220)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 28s

The review surface shipped yesterday reported two canons on its first live
day and both were sound. Coherence was "do all judged rows share a form",
which #2844 failed at 37/62 = 0.597 for containing a model class and the
to_dict the canon's own text says the class must carry, and #2849 failed at
4/7 for pairing sync loop-starters with the async ticks they schedule. A
review surface whose whole output is noise is one that stops being read.

The obvious repair is a trap, and there is now a test standing in front of
it. Grouping by family and keeping a majority test makes the check BLIND:
before #2844 was cleaned by hand it held 37 classes and 56 callables, which
as families is 56/93 = 0.602 — a clean pass, and the 31 rows that had no
business being there (Vue functions, route handlers, a dozen tests) would
never have been reported at all. A looser bar in the same shape is worse
than the bug.

So the verdict is inverted. Instead of asking whether most rows agree, it
asks how many rows the canon CANNOT ACCOUNT FOR: a row in the majority
family is accounted for; a callable defined in a file that also holds a
majority-family `type` row is a method of a member, not a foreign body; and
strangers above a fifth of the readable rows make the canon incoherent. The
majority vote abstains those methods, so a class's own serialisers cannot
outvote the classes and turn the members into the strangers.

Measured on the real ledger before it was written, which is why it is this
rule and not a nudge to the share: clean #2844 has 0 strangers in 62,
#2849 has 0 in 7, and polluted #2844 had 31 in 93 — the same 31 withdrawn
by hand this morning, named exactly.

The entry now carries `families`, the majority `family`, `attached`,
`stranger_count`, `unattended`, and `strangers` — THE ROWS THAT DO NOT FIT,
replacing a sample of the first twelve members. The reader's question is
which rows are wrong, and a sample of the agreeing majority cannot answer
it. `unattended` is the discriminator between a check that is too strict and
a ledger full of junk: both canons flagged on day one were entirely
audit-judged, and nothing showed that without opening each one.

Scoped to the review surface. `canon_form` still answers at the precise form
level for stamping and divergence, where a sync helper beside an async canon
is a fair question; nothing here changes what the ledger writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-20 23:39:01 -04:00
co-authored by Claude Opus 5
parent e87bcfa48c
commit 0fe19a8440
3 changed files with 341 additions and 33 deletions
+15 -5
View File
@@ -267,13 +267,23 @@ async def stamps_to_review(project_id: int, top: int = 10) -> dict:
score, signature and derived form so you can judge rather than trust the
threshold.
`incoherent` — canons whose judged rows do not agree on a form: the
`forms` histogram, how many members were hook-stamped weakly, and a
sample. A canon asserts that some shapes are the same sort of thing; when
its members are a class, three getters and a dozen tests, that assertion
has stopped being true and every base-rate reading built on it — the
`incoherent` — canons whose membership no longer agrees what the canon
is. A canon asserts that some shapes are the same sort of thing; when its
members are a class, three getters and a dozen tests, that assertion has
stopped being true and every base-rate reading built on it — the
divergence prompt included — is reading noise.
Disagreement is judged by FAMILY (a sync helper and an async one are one
family), and a method defined beside a member class counts as part of the
shape rather than against it — so a model convention that covers both the
class and its `to_dict` reads as coherent, which it is (#4220). Each
entry carries `families` and `forms`, the majority `family`, `attached`
(minority rows excused as methods of a member), `strangers` — THE ROWS
THAT DO NOT FIT, which is what you judge — and `unattended` / `weak_rows`,
the count written by the hook with nobody reading. An incoherence whose
rows are all judged is likelier this check being strict than a bad
ledger; one made of unattended rows is the real thing.
A row listed here is a question, not a verdict. Some will be correct.
Read-only; requires read access to the project.