Commit Graph
31 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 19438cc894 fix(shapes): the capped meaning pass reads new shapes first, and its miss withdraws an early flag (#4208)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Failing after 51s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 21s
Measured on the first live refresh after 91cde6c deployed: the version bump
queued 1,533 rows for the semantic arm, the cap read 150 of them in row
order, and all six shapes new since the previous refresh - the only rows
flag_divergence acts on, and the highest ids - were flagged before the arm
reached them. The gate silenced nothing because it never got to look, and
since a flag persists until judged, a later conclusive miss could not take
it back.

- propose_for_repo sorts the semantic todo by _semantic_priority:
  unclassified before scoped, newest first.
- flag_divergence withdraws a standing flag when the row now carries a
  conclusive miss; that evidence alone withdraws one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-22 08:05:49 -04:00
bvandeusenandClaude Opus 5 91cde6c3e4 fix(shapes): let a measured meaning-miss silence a divergence prompt (#4208)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 52s
CI & Build / Build & push image (push) Successful in 28s
#4204 gave the divergence check a structural gate, which silenced one of the
five false prompts it was filed for. The other four are `def` helpers in a
directory whose canon is an `async def` service unit. No refinement of
`shape_form` reaches them: they differ from #2793's acceptance case — a
hand-rolled sync `confirmDanger` where an async confirm helper is canon —
only by the JOB they do, and a signature does not carry a job.

The proposer's semantic arm already reads bodies per symbol, which is the
comparison option 2 asked for and was thought to be missing. What it did not
do was record its MISSES: a hit became `proposal_basis = "semantic"`, a miss
left the row indistinguishable from one nobody had looked at. So
`flag_divergence` could ask the proposer "do you agree this is the canon?"
but never "did you check, and is it not?".

`_semantic_canon` now reports whether an empty answer is evidence, and
`flag_divergence` withholds the prompt when it is.

The whole risk is in the negative, so only a conclusive miss is stored. A
body too thin to embed, a row the per-refresh cap never reached, an arm that
threw, and a result set that came back full — and may therefore have hidden
the canon behind the limit — all stay "cannot tell" and still ask the
question. That is the discipline `FORM_UNKNOWN` already enforces here: not
knowing must make a check quieter, never more confident. `_SEMANTIC_LIMIT` is
named for that reason; the number is load-bearing, not a tuning knob.

No migration: `proposal_basis` is nullable Text with no CHECK constraint
(verified in the model and across alembic/versions), so rule 36 does not
bite. Nothing can mistake the miss for a proposal either — every reader keys
on `proposed_snippet_id` or `proposal_group`, and `confirm_shape_proposals`
requires the id non-NULL before it will confirm anything.

`_PROPOSER_VERSION` 3 -> 4, per its own contract: rows remember the ruleset
they were examined under, and without the bump no already-examined row would
ever acquire a miss.

Option 1 (widening `kind`) stays closed, on the merits rather than on cost:
bucketing density by exact form takes the async canon out of a sync
candidate's denominator and silences #2793's acceptance case by the identical
mechanism, one layer down. The reasoning is on #4208.

Tests: tests/test_divergence_meaning_gate.py pins the report contract, with
the truncation case tested hardest — reading a cut-off as a negative would
weaken the guard in proportion to how many snippets the operator has. The
end-to-end discrimination is in test_integration_shape_classify.py on
deliberately the SAME fixture as #2793's acceptance case, so the two runs
differ in exactly one thing: whether the arm claims to have looked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-21 20:35:55 -04:00
bvandeusenandClaude Opus 5 04775c3496 fix(drafter): the base rate counted a dataclass and an async service unit as comparable things (#4208)
CI & Build / Python lint (push) Successful in 8s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 26s
A CORRECTION TO THIS ISSUE'S OWN PLAN, first, because the plan was wrong in
a way that would have cost the acceptance case.

#4208 recommended widening `kind` past `css | sym` to carry the form, calling
the denominator the deeper win and a migration the price. Two things:

1. THE MIGRATION IS NOT NEEDED. The issue says density "is computed from
   stored rows and `shape_form` only runs on read". `canon_density` does
   `select(CodeShape)...scalars().all()` and filters to the directory in
   PYTHON; `signature` is a stored column; `canon_form(siblings, ...)` on the
   next line already derives form from those rows. Bucketing the denominator
   costs a list comprehension.

2. WIDENING `kind` WOULD BREAK MILESTONE #2793's ACCEPTANCE CASE. If `kind`
   separated `fn` from `async-fn`, density would bucket them apart — which
   does silence #4204's four `def` helpers, as claimed. Apply it to a
   hand-rolled SYNC `confirmDanger` in a directory where an async confirm
   helper is canon: the canon leaves the candidate's denominator, nothing
   dominates, no flag. That is the flag the milestone exists to produce, and
   it is the same inversion the first form gate made, one layer down.

So option 1 is not expensive-but-right, it is wrong; and option 2 (compare
meaning) is not the interim, it is the only lever. `confirmDanger` beside an
async confirm helper and `is_registered` beside a service unit are identical
at every structural level — same family, same form contradiction, same
density. They differ only in whether the candidate does the canon's JOB, and
no signature carries that.

WHAT THIS SHIPS is the part that is right and provable: an honest
denominator. `comparable_siblings` narrows the count to rows whose family
does not contradict the candidate's, using `families_conflict` — the same
predicate the gate uses, so the count and the verdict cannot drift into
disagreeing about what comparable means. "372 judged siblings" stops counting
a dataclass, a constant and an async service unit as three comparable things.

NARROWED BY FAMILY, NOT FORM, for the reason above: `fn` beside `async-fn`
stays a fair question. An unreadable sibling STAYS COUNTED — dropping it
would shrink `judged`, raise the share, and fire the check more on the
directories it can read least. Every unknown-form decision in this module
goes that way.

Density is now per candidate rather than per kind, cached on (kind, form).

TWO BUGS THIS CHANGE HAD, both caught before CI and both pinned:

- I passed a FAMILY where `families_conflict` reads a FORM, so
  `shape_family("callable")` returned "" and the narrowing was a silent
  no-op that still read as applied. The regression guard deliberately uses a
  callable: `type` is both a form and a family name, so testing with it
  proves nothing and the bug hides.
- My new `_Row` in the test file shadowed the one already there — same
  fields, different `snippet_id` default — silently breaking three passing
  `canon_form` tests. Reused the existing class. A duplicate definition
  quietly changing a neighbour's meaning is this file's own subject.

The residue test's docstring said separating the four needs "widen `kind` or
a comparison of meaning". Corrected: they are not alternatives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-21 01:33:45 -04:00
bvandeusenandClaude Opus 5 0fe19a8440 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
2026-09-20 23:39:01 -04:00
bvandeusenandClaude Opus 5 2be17828a9 fix(ledger): live_rows_for called access with nothing in scope (#4208)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / Build & push image (push) Successful in 29s
Lint caught an F821 that would have been a NameError the first time
`stamps_to_review` was called: `access` is imported locally inside each of the
seven functions in this module that need it — services/access reaches back
here, so a module-level import closes a cycle — and the new function used it
without one.

I wrote the function by pattern-matching its neighbours and did not check what
those neighbours do to make themselves work. Same shape as the tuple unpack
two commits ago (#4207): the mistake is not in the logic I was thinking about,
it is in the surrounding contract I did not read.

Unit and integration were both green on the failing run (7095); only lint was
red. Worth recording because the lane that caught it is the cheapest one and I
had read its command as covering tests — `ruff check src/ scripts/` does not
look at tests/ at all, so a clean test suite says nothing about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-20 22:33:22 -04:00
bvandeusenandClaude Opus 5 400253d039 feat(ledger): the ledger can say "these look wrong" without acting on it (#4208)
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Python tests (push) Successful in 1m49s
CI & Build / Build & push image (push) Skipped
THE HALF THAT WAS MISSING. #4204 put a floor under what the write-path hook
may assert. A floor only guards new writes; every row already stored stands
(lesson #4202). Measured after that fix shipped: Portal carried 32 rows under
one canon and 3 under another, all stamped on scores of 0.69-0.77 — below the
0.80 floor, so none of them could be written today, and all of them were still
there. Scribe's own ledger carries 334 under #2860.

`stamps_to_review` reports two things and changes nothing:

  weak       — rows the hook stamped on a resemblance below the current floor,
               each with its score, signature and derived form.
  incoherent — canons whose own judged rows do not agree on a form. A canon
               claims some shapes are the same sort of thing; when its members
               are a class, three getters and a dozen tests, that claim has
               stopped being true and every base-rate reading built on it is
               reading noise. `canon_form` already made such a canon fall
               silent — nothing made it VISIBLE.

IT DELIBERATELY CANNOT FIX ANYTHING, and that is the design, not an omission.
The first version of this commit was an automatic sweep that reset rows by
score. That is the original defect pointed the other way: what harmed the
ledger was not one wrong score, it was a machine recording permanent
classifications unattended. Un-recording them unattended is the same act with
a wider blast radius. An agent reads the evidence, judges, and records the
judgment under its own name through `classify_shapes`.

`test_the_service_carries_no_machinery_for_bulk_withdrawal` asserts that
structurally, so the next person to reach for an auto-retire has the argument
again on purpose rather than in a diff nobody reads.

A JUDGMENT IS NEVER LISTED AS WEAK, whatever its age. This is the measured
correction to an assumption I nearly shipped: of Scribe's 334 rows under
#2860, 302 are in `services/` — the canon's own home — and the ones sampled
there are `classified_by="audit"` with no score at all. The legitimate bulk
of that canon was never scored; it was judged by an agent in batch. Listing
those as weak would invite an agent to withdraw the only real judgments in the
ledger. An agent's decision is a different KIND of evidence, not a worse one.

THE SCORE NOW HAS A PARSER. It lived only inside a prose sentence, so nothing
could ask how strong the evidence for a row was without re-deriving it — which
is how 32 rows sat unexamined for nineteen days. Format and reader are one
constant apart (`_RESEMBLE_REASON` / `stamp_score`), with a round-trip test and
a test pinned to reason strings taken verbatim from the two poisoned ledgers.

`live_rows_for` is `live_rows` behind the project read gate, for callers that
arrive from outside rather than from a job that already knows who is asking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-20 22:28:12 -04:00
bvandeusenandClaude Opus 5 d5b46ffc45 fix(ledger): the in-play tuple widened and one consumer kept reading four (#4204)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 1m42s
CI & Build / Build & push image (push) Successful in 26s
[s_id for rank, _at, s_id, _why in bucket if rank == 2]
    ValueError: too many values to unpack (expected 4, got 5)

`in_play` gained the canon's form as a fifth element so the stamp could be
decided per shape rather than per file. The `record_uses` call eighty lines
below still destructured four, and every stamp that reached it raised.

Both integration failures on runs 7090 and 7091 are this one line —
`test_write_path_stamp_is_evidence_that_yields_to_judgment` and
`test_a_brand_new_shape_gets_a_provisional_row_the_sync_settles`.

Now indexed rather than destructured, matching the candidate scan above it, so
the next widening cannot break it positionally.

WHY THE UNIT LANE STAYED GREEN THROUGH TWO PUSHES. `record_uses` is only
reached once a stamp is actually written, which needs a real snippet, a real
ledger row and the write ACL — so no unit test crosses that line. 139 local
assertions and the whole unit suite passed on code that raised on every
successful stamp. The integration lane was the only thing that could say so,
which is the case for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-20 21:53:53 -04:00
bvandeusenandClaude Opus 5 a4883c8ac1 fix(ledger): divergence asks at family level — the first gate was inverted (#4204)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Failing after 56s
CI & Build / Python tests (push) Successful in 1m29s
CI & Build / Build & push image (push) Successful in 28s
CI run 7090 caught this; the unit suite could not. Integration job 25763 —
`test_a_second_confirm_dialog_is_detected_and_named`, the acceptance case of
milestone #2793.

WHAT I GOT WRONG. The previous commit gated BOTH halves of the ledger on
`forms_agree`. That is right for stamping and backwards for divergence,
because the two assert opposite things:

  STAMPING says "this IS that canon". Agreement in form is evidence FOR the
  claim, so demanding it is correct.

  DIVERGENCE says "this is NOT the canon that dominates here — did you mean
  to?" A form MISMATCH is the PREMISE of that prompt. Requiring the candidate
  to match the canon silences the check precisely where it belongs.

So #2793's case stopped firing: a hand-rolled sync `confirmDanger` in a
directory where an async confirm helper is canon read as `fn` against
`async-fn`, disagreed, and was dropped. `flag_divergence` returned 0 where the
test demands 1, and the write-time check returned nothing where it must name
the canon. That is a real flag the milestone exists to produce, and my change
removed it.

THE FIX. Divergence now gates at FAMILY level — callable {fn, async-fn},
type, value, css — and only on contradiction. A sync function beside an async
one is still a fair question. A frozen dataclass told to build from an async
service function is not a question at all.

WHAT THIS DOES NOT FIX, asserted rather than commented so it fails the day it
changes (`test_how_many_of_the_five_the_divergence_gate_actually_silences`):
of #4204's five false prompts this silences ONE. `Point` is a type against a
callable canon. `_p`, `get_point`, `is_registered` and
`sources_expected_to_emit` are callables like the canon and still ask — and
at the signature level they are indistinguishable from the #2793 case above,
so nothing readable here can separate them. That needs #4204 option 2 (widen
`kind` past `css | sym`) or a comparison of meaning rather than form.

The stamping half — `_RESEMBLE_MIN` 0.80 and the graded burden — is unchanged
and unaffected by this failure. It is also the half that matters more: loose
stamping is what manufactures the density the divergence check reads as
authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-20 21:26:12 -04:00
bvandeusenandClaude Opus 5 947203fa44 fix(ledger): a canon is only urged on a shape that could be it (#4204)
CI & Build / integration (push) Failing after 51s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 25s
Two halves of one defect, found while writing #3431 and measured on a second
project.

THE DIVERGENCE CHECK WAS A BASE RATE. `dominant_canon` answers "what is most
common in this directory" and never "is this that" — the candidate's
signature was not examined at all. With `kind` carrying only `css | sym`, a
frozen dataclass, a module constant, a sync predicate, a class and an async
service function are all siblings, so the prior was not merely the best
signal, it was the only one. Writing a registry module of pure helpers
produced five prompts to build them from the `async_session` service canon.

THE AUTO-STAMP HAD NO FLOOR. `elif sid in resembles` took any score at all:
0.69 asserted as confidently as 0.95, and the number went into the reason
line without ever being compared to anything. Worse, the score is computed
against the WHOLE PAYLOAD, so one number spoke for every symbol in the file.
On Portal that recorded `class SessionAbsent`, `def build_channel`,
`async def attach` and a dozen test functions as instances of one snippet —
17 rows under #3283, which then made that directory "canon-dense" and started
instructing every later writer in it.

The two compound: loose stamping manufactures the density the divergence
check reads as authority. Both are fixed by one primitive.

`shape_form` derives a coarse form — css / type / async-fn / fn / binding —
from the signature, on READ. `kind` is part of the row identity, so widening
that column needs a migration and a re-extract (#4204 option 2, still the
principled fix); deriving costs nothing and is reversible. Every caller asks
`shape_form`, so the day the column carries the answer it returns that.

THE BURDEN SCALES WITH THE EVIDENCE, and getting this wrong was the first
version. A by-name reference — the payload names the canon's symbol — is
strong and needs only the absence of contradiction; demanding positive
agreement there silenced it whenever a shape's definition was not in the
payload (an Edit rather than a Write), turning strong evidence into none for
a reason unrelated to the code. A resemblance score is weak and must
positively agree.

`canon_form` reads the form a canon's own judged rows agree on, and returns
unknown when they disagree. That makes the halves cooperate: a canon already
poisoned by loose stamping — Portal's #3283 — falls silent instead of
flagging anyone else.

`forms_agree` requires BOTH sides known, so an unreadable signature makes the
checks quieter rather than more confident. `_RESEMBLE_MIN` is 0.80 rather
than the retrieval floors near 0.70: those decide whether to SHOW a record,
where being wrong costs a glance; this decides whether to RECORD a claim
unattended, where being wrong misinstructs everyone who writes there after.

Caught by the tests, not by review: a first pass required `const`/`let`/`var`
before a binding, so every Python module constant read as unreadable and a
whole form was silently excluded from both checks.

Not addressed: rows already carrying a wrong snippet_id are not undone by a
guard at the point of classification (lesson #4202). Portal's 17 keep
producing dominance until something re-judges them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-20 21:16:51 -04:00
bvandeusenandClaude Fable 5 df18e897af feat(ledger): the consumer map reads transition names and concatenated prefixes (#2970)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 10s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 23s
Paying down #2962 measured `flag="unused-css"` against a hand audit and
found it had a permanent false-positive floor: roughly sixty classes it
called unused are alive and always would be, because two ordinary authoring
forms produce names no reader of `class=` attributes can see. A flag whose
list you cannot act on line by line is worse than no flag — act on it and
you delete live UI.

- A transition `name=` IS a class reference. `<Transition name="toast">`
  makes Vue apply `.toast-enter-active` and its siblings at runtime, and
  React's `<CSSTransition classNames="fade">` does the same with a different
  suffix set. Every spelling of the tag is read (`Transition`,
  `TransitionGroup`, `transition-group`), and the emitted suffix set is the
  union of Vue 3, Vue 2 and React: naming a class no rule defines costs
  nothing, since it resolves to no row. A bound `:name` stays unknowable.
- A concatenated name contributes its static head as a PREFIX reference.
  `` `status-${s}` ``, `'pri-' + p` and `class="card-{{ v }}"` all leave a
  head behind once the hole is blanked — and `_CLASS_TOKEN_RE` accepts a
  trailing hyphen, so until now the extractor emitted a junk token
  `"status-"` that matched nothing. It is now `status-*`, and
  resolve_consumers credits every row whose symbol starts with that head,
  each under the same own-file-else-fan-out rule as an exact token. `*`
  cannot occur in a class token, so the marker rides the existing
  dict[str, int] with no schema change. A head shorter than two characters
  says nothing and is dropped.

Crediting every candidate row is the honest reading: the template genuinely
does not say which one it built, and the alternative is reporting live rules
as dead. What the map still cannot see is a name assembled in a script —
`classList.add` — which stays deliberately out of scope; the skill, the
`flag=` docstring and the refresh payload docs all say so now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 21:27:27 -04:00
bvandeusenandClaude Fable 5 8664d8ad14 feat(ledger): the consumer map surfaces — every css row carries used_by on list_shapes, flag="unused-css" (the map's negative space, surfaced never deleted), derive groups and the write-path family carry consumers, the derive line says "used by N template(s)", coverage payload unused_css + the standing block; docs, SKILL, plugin 0.1.44; the two step-2 tests expected 5 edges where fan-out makes 6 (milestone 302 step 3, #2936)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Failing after 44s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 31s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 14:03:35 -04:00
bvandeusenandClaude Fable 5 ffbdf19116 feat(ledger): the CSS consumer map — code_shape_consumers edges (shape → file whose markup names the class, count), migration 0086, resolve_consumers (own-file row when the template defines the class, else every other definition), sync_repo_consumers rebuilt from the archive on every refresh, consumers_of; derived, so not backed up (milestone 302 step 2, #2935)
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 52s
CI & Build / Build & push image (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 13:59:55 -04:00
bvandeusenandClaude Fable 5 85111442a6 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>
2026-08-23 00:21:17 -04:00
bvandeusenandClaude Fable 5 2324c15418 feat(write-path): the derive arm — the hint names a duplicate family (no canon) or a canon elsewhere for the shapes being written; exclude_derive channel (#2900, milestone 299 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 23s
shape_ledger.write_time_derive asks the ledger what it knows about each
named (kind, symbol): a derive-grouped family (identical body / same name
in N other files) -> "derive it now, do not add a copy"; a canonical row
at another path -> "canon #N at <path>, reuse". Judged rows at the path and
the canon own file stay silent. Rendered by _derive_line beside the
divergence line; keyed (group id / canon:<id>) on a third per-session dedup
channel in the hook (.derive.ids -> exclude_derive=).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 13:30:33 -04:00
bvandeusenandClaude Fable 5 bb242ca566 feat(coverage): the line names standing work with 0 unclassified + derive_new, copies that joined a family since the previous refresh (#2899, milestone 299 step 1)
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
Since the scoped bucket (#2869) the ledger reads 100% accounted while 439
derive rows stand; the standing block was gated on unclassified > 0 and so
went silent. Build it whatever the todo count ("; standing: ..."), and add
derive_new — derive-grouped rows first seen after the previous refresh
stamp — so entering a project names the drift ("+2 new copies since last
refresh: .error-msg in InceptionCard.vue") instead of waiting for an audit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 13:27:29 -04:00
bvandeusenandClaude Fable 5 aba16583ab 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>
2026-08-21 17:34:12 -04:00
bvandeusenandClaude Fable 5 d4c7b0e48d feat(ledger): uses edges — consumption is its own relation, conformance keeps one snippet_id (#2870, milestone 294)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Failing after 39s
CI & Build / Build & push image (push) Skipped
A shape can follow one convention canon AND call several helper canons; the
row's single snippet_id made the 2026-08 audit pick (hash_token won, the
service-function convention lost), and hook evidence — pulled a snippet, then
wrote code naming it — was stamped as instance when it is a uses fact.

- code_shape_uses (migration 0084): shape → snippet, basis, evidence; unique
  per pair; cascades with both ends. USE_BASES: reference | hook | agent |
  audit | import. A judgment-grade basis overwrites a mechanical one, never
  the reverse.
- classify_shapes items and classify_shapes_by_rule take uses=[snippet ids]
  (targets validated like snippet_id; all-or-nothing).
- The write-path hook writes a uses edge for every pulled canon the payload
  names (the instance stamp is unchanged); the proposer writes a uses edge
  for every canon a body names (reference_canons: kind + language family +
  stoplist, same rules as the reference basis) — the mechanical form of
  "auto-confirm own-import references" deferred from #2871.
- list_shapes(uses=N) lists the consumers of a canon; get_snippet's consumer
  map gains `uses` beside instances/variants.

Operator decision on #2870 (2026-08-21): keep one snippet_id, add uses edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:18:25 -04:00
bvandeusenandClaude Fable 5 1a8e5787e8 feat(ledger): reason codes, case-insensitive repo filter, next action on the coverage line (#2874, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 32s
CI & Build / Python tests (push) Canceled after 31s
CI & Build / Build & push image (push) Canceled after 0s
- code_shapes.reason_code (migration 0083): an optional code from a fixed
  catalogue (scoped-css, one-off-handler, test-helper, convention-plumbing,
  pure-helper, generated, script, typed-record) beside the prose reason, so
  the ledger can be filtered/aggregated by kind of one-off; validated in
  classify_shapes and classify_shapes_by_rule; on to_dict/to_compact.
- Snippet location lookups match repo case-insensitively in both dialects
  (location_matches / location_jsonpath via like_regex flag "i") — "Scribe"
  vs "FabledScribe" vs "fabledscribe" recorded free-form hid half the canon
  from list_snippets(repo=, path=).
- coverage line names the next action: "top canon #N ×k" (biggest proposal
  queue) and "top copy <label> ×files" (widest body-identical group).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:14:34 -04:00
bvandeusenandClaude Fable 5 57d68c9355 feat(ledger): derive readout ranks body-identical groups first; CSS fingerprints are declarations, not selectors (#2872, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 32s
The 2026-08 audit consolidated identical bodies under different names and
files (five auth views' CSS, two Workspace formatDate()s, four modal blocks)
while the derive readout led with name groups (to_dict ×25, main ×5, load ×6)
that were convention or coincidence.

- proposal_summary ranks dup:<sha> groups above name groups, wider file spread
  first, and carries `files`; scoped rows (#2869) are in the readout, since
  that is where view-level copies live.
- extract_definitions fingerprints a CSS rule by its declarations — the row
  identity already carries the selector — so .closed-msg / .error-block /
  .success-msg with one body are one dup group. One-time effect: judged css
  rows whose stored fingerprint predates this may show a recheck on the next
  refresh (the judgment stands; re-confirm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:08:57 -04:00
bvandeusenandClaude Fable 5 1ab614bfbe feat(ledger): the scoped bucket — by-construction one-offs are stamped by the sync, not judged by a person (#2869, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Failing after 25s
CI & Build / Python tests (push) Canceled after 51s
CI & Build / Build & push image (push) Canceled after 0s
The 2026-08 audit left 77% of Scribe's ledger `exempt`, most of it a Vue
component's scoped <style> rules and <script setup> functions — one-offs by
construction (unreachable from any other file) that add nothing when judged
one by one and bury the rows a person should look at.

- coverage: Definition carries its line; scoped_definitions() names, per
  .vue file, every sym and every css rule inside <style scoped>; ArchiveShape
  carries the flag.
- sync: such rows are stamped status=scoped / classified_by=mechanical with
  the by-construction reason (history event recorded); un-stamped back to
  unclassified if a later tree makes them ordinary; a judgment overrides.
- The machine still sees them: proposer, derive grouping, divergence, hook
  evidence, canonical stamping and classify_shapes_by_rule's default all
  treat unclassified + scoped as the unjudged set (_MECHANICAL_TODO). Only
  the human todo (status=unclassified) and largest_gaps exclude them.
- accounting counts `scoped`; coverage line and the project card legend show
  it; SHAPE_STATUSES gains it (no DB CHECK on status — no migration).
- shape-accounting skill documents the bucket; plugin 0.1.37.

Operator decision on #2869 (2026-08-21): keep extracting everything, stamp
mechanically, keep `exempt` a human judgment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:06:52 -04:00
bvandeusenandClaude Fable 5 9abc4443fb feat(ledger): audit surfaces — list_shapes(compact=True) and classify_shapes_by_rule, the sweep form of a judgment (#2868, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 26s
The 2026-08 audit judged 3,427 rows in 14 hand-driven batches through a raw
MCP client because a list_shapes page overflowed the tool budget and every
row had to be sent back one by one. Now:

- list_shapes(compact=True): path · symbol · kind · status · signature, plus
  snippet_id / by / proposal / diverges_from / recheck only when set. A full
  500-row page fits. CodeShape.to_compact() is the row shape.
- classify_shapes_by_rule(project_id, path, status, pattern=, kind=,
  snippet_id=, reason=, via=, include_judged=): ONE judgment over every
  unclassified live row under a directory whose symbol matches a glob;
  judged rows are untouched unless include_judged; canonical is refused;
  same gates as the row form; one transaction; returns count + sample.
  shape_ledger.classify_shapes_where / rule_matches (pure) carry it.

Tests: compact row pinned, rule_matches directory/glob/kind semantics, the
tool mount, and an integration sweep (unclassified-only, include_judged,
gates).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:02:55 -04:00
bvandeusenandClaude Fable 5 3a4031d7f8 fix(ledger): proposer v3 — sym bases gated by language family, reference skips generic verbs, semantic held to the shape's own project (#2871, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s
The 2026-08 audit examined 407 proposals. Every cross-language hit was wrong:
the Python MCP tool-module canon (symbol `register`) was offered for each auth
view's handleSubmit (it calls authStore.register()) and for the TS auth store's
own `register`; Minstrel/Forge TS canon matched Python bodies by resemblance.
Every cross-project semantic proposal was noise.

- Canon carries the snippet's language; match_canon skips a sym canon whose
  family (py / js / css / sh / sql, by language ↔ by path extension) differs
  from the shape's. Unknown on either side = no gate.
- The reference basis ignores a stoplist of generic verbs (register, load,
  save, get, …): a bare mention is not a call site of THIS canon; the symbol
  basis still catches a second definition, and the call-site relation moves
  to `uses` edges with #2870.
- The semantic arm only reaches canon in the shape's own project and family;
  symbol/text still reach family canon elsewhere (note 2786).
- _PROPOSER_VERSION 2 → 3 so standing proposals re-examine on the next refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:00:48 -04:00
bvandeusenandClaude Fable 5 7d48eb0b1b refactor(services): one periodic-task shape, one APScheduler job shape, one token hash, one summary rule — the services pass of the shape audit (#2830, milestone 296)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / integration (push) Successful in 24s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 24s
- background.start_periodic(interval, work, label=) replaces the three hand-rolled
  while-True/sleep/try loops in logging, auth and notifications.
- services/scheduler.ScheduledJob replaces the four private BackgroundScheduler
  copies in recurrence/version_pinning/trash/db_maintenance schedulers; public
  start_/stop_/reschedule_ surfaces unchanged.
- api_keys.hash_token is the one sha256 helper; auth.py used to inline it 5x.
- auth.is_registration_open reads via settings.get_admin_setting; notification
  prefs read via settings.get_setting; _fire_share_email uses _get_user_email.
- projects.get_project_summary / milestones.get_project_milestone_summary are
  now the one-id view of their batch siblings instead of a second copy of the
  queries; sharing.best_permission_by (was _deduplicate_by_permission) is the
  one rank-dedup, now also used by list_projects_for_user.
- backup: the row builders for every section both exporters carry are named
  functions, so a column added to one export cannot silently miss the other.
- iso() from models.base replaces the attr.isoformat()-if-attr-else-None idiom
  and db_maintenance._iso across services; backup keeps its explicit shape.
- trash.py hoists the sql_delete/timedelta imports it re-imported per function.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 12:34:28 -04:00
bvandeusenandClaude Fable 5 dad56a51bd fix(ledger): the proposer matches signatures against a canon's own definition only and floors semantic at 0.8; ruleset version re-examines standing proposals
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 31s
First live run: a canon whose recorded code opens with a call-site example
(confirmed()'s onTrash) made every `async function x(): Promise<void>`
resemble it at 0.86, and the write-path semantic floor paired alembic
upgrade()/downgrade() bodies with unrelated canons at 0.68-0.75. Rows now
remember (body, ruleset) so a tightened rule looks again once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 07:42:12 -04:00
bvandeusenandClaude Fable 5 d74a244b3a feat(ledger): divergence readout — button B where button A is canon, shape history, and judged-shape recheck (#2793, milestone 294 step 7)
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Every judgment now goes through one helper that remembers the fingerprint
judged (classified_sha) and writes a code_shape_events row; the sync writes
vanished / reappeared / drifted events and flags recheck_at when a body
moves under an instance/variant. The refresh flags diverges_from on shapes
new since the previous computation that sit where one canon dominates the
judged siblings of their directory+kind and were not proposed as that canon
(a first seed flags nothing); the write-path hint asks the same question
in-band for the shapes the hook names. list_shapes(flag=divergence|recheck),
shape_history(project_id, path, symbol) (read-only), coverage line/payload/
card carry divergent + recheck. Backup v8 carries the history. Plugin 0.1.36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 23:13:39 -04:00
bvandeusenandClaude Fable 5 386b27e422 fix(ledger): the proposer prefers a same-project canon on a tie — family canon elsewhere is the fallback, not the first hit
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Python lint (push) Successful in 5s
CI & Build / integration (push) Successful in 35s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Build & push image (push) Successful in 40s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 21:36:10 -04:00
bvandeusenandClaude Fable 5 ba0030e51d feat(ledger): mechanical proposer — every refresh proposes instances against canon and groups derive-first candidates; agents confirm in batches (#2792, milestone 294 step 6)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 48s
Shapes now carry a content fingerprint (signature + whitespace/comment-
insensitive body_sha; migration 0080) and the proposer runs inside the
coverage refresh, the one moment bodies exist: symbol elsewhere → textual
containment → body references the canon → signature resemblance → semantic
(capped per refresh, unreached rows stay unexamined for the next). A hit is
a proposal on the row (proposed_snippet_id/basis/score), never a
classification; rows with no canon hit group by the derive-first rule
(identical body in ≥2 places, same name in ≥3 files) as proposal_basis=
derive + a group key. list_shapes(proposal=any|canon|derive|<basis>) is the
queue; confirm_shape_proposals(project_id, snippet_id|path|basis) confirms
in batches as agent instances; any classify_shapes/hook stamp retires the
proposal. Readout carries proposed + derive_groups (line, payload, card).
Plugin 0.1.35 (skill: the machine proposes, judgment classifies).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 21:30:35 -04:00
bvandeusenandClaude Fable 5 a9e1cddba7 fix(tests): provisional ledger rows carry the empty seen-marker default, not NULL
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 31s
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Python tests (push) Successful in 1m1s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 20:23:12 -04:00
bvandeusenandClaude Fable 5 475f0857c9 feat(ledger): write-path stamping — a pulled canon the session then instantiates lands as a hook instance row (#2791, milestone 294 step 5)
CI & Build / Plugin hooks (push) Failing after 2s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Failing after 28s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 39s
The prior-art hook now names the shapes being written (shapes=kind:name —
every definition in the payload, or the one enclosing an Edit found by
walking the file upward) and the server stamps them as instance rows when
the session PULLED a snippet inside PULL_WINDOW that the payload references
by symbol or that the semantic arm scored for this very payload.
classified_by=hook, evidence in reason; never overrides a judgment or a
canonical row, overridable by classify_shapes. Offered-but-unopened stamps
nothing. Pulled-and-already-seen snippets stay in the semantic query as
evidence without re-entering the deduped menu. A brand-new shape gets a
provisional row the next sync confirms or vanishes. Read-scoped keys get
the hint, never the stamp. Plugin 0.1.34.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 20:19:45 -04:00
bvandeusenandClaude Fable 5 942edd1eb5 feat(ledger): classify_shapes + list_shapes MCP tools; get_snippet carries the consumer map (#2789, milestone 294 step 3)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Skipped
The judgment write path. classify_shapes applies a batch of classifications
to a project's live ledger rows — all-or-nothing (#2709's lesson: the whole
batch is validated, write-ACL'd, and every snippet target proven readable
before any row is touched); rows match by exact (path, symbol), kind narrows,
and shapes no live row matches come back as 'unmatched' rather than errors.
variant/exempt REQUIRE the reason — the why is the record (note 2786) — and
'unclassified' deliberately withdraws a judgment back to the todo. The 'via'
channel is caller-restricted to agent|audit|import; hook and mechanical stay
server-internal so a caller can't launder judgment as machinery.

list_shapes is the todo query (status=unclassified) with composable filters:
path is exact-or-under like recorded locations, snippet_id reads a consumer
map, include_vanished reads history; paged with the true total.

get_snippet now attaches  and  — the structured consumer
map, filtered to projects the CALLER can read so a shared snippet never side-
channels another project's file layout; attached only when non-empty (#2483).

Integration tests pin the batch atomicity, ACL gates, filter composition, the
consumer map on the MCP pull, and the SET NULL companion: a judgment whose
snippet was purged rejoins the todo on the next sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:55:47 -04:00
bvandeusenandClaude Fable 5 9b1597a3c9 feat(ledger): coverage refresh feeds the shape ledger; the readout inverts to accounting (#2788, milestone 294 step 2)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 44s
compute_coverage is now the ledger's sync point: every walk upserts the
extracted shapes (new → unclassified, the todo state; surviving → last-seen
bump; vanished → stamped, kept as history), re-files judgments whose snippet
target went away, and mechanically stamps snippet reference locations as
canonical — the one always-safe rule, self-healing only for its own stamps
(an agent's judgment is never unwound by machinery).

The covering predicate moves to shape_ledger.location_covers as the single
home (match_shapes retired with its consumer); coverage's payload and line
invert from 'N/M shapes recorded' to shape ACCOUNTING per note 2786:
accounted/total with a canonical·instance·variant·exempt breakdown, and
unclassified — THE todo — with its largest directories. Cache key bumps to
v2 so pre-ledger blobs honestly read 'not measured yet' instead of rendering
in a shape no longer spoken.

Readout is deliberately project-wide (all repos' live rows), while the walk
serves whichever repos the owner's keyring reaches this refresh.

Integration tests pin the new contract: rows for every extracted shape,
mechanical canonical stamps carrying snippet ids, idempotent recompute,
agent judgments surviving recompute AND vanish/return, vanished rows leaving
the readout but keeping their history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:37:18 -04:00