Skip to content

fix(devx): read the ledger note against its own field, and stop --lower minting the drift - #10820

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10722-ledger-note-consistency
Aug 21, 2026
Merged

fix(devx): read the ledger note against its own field, and stop --lower minting the drift#10820
os-zhuang merged 2 commits into
mainfrom
claude/issue-10722-ledger-note-consistency

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10722

What was wrong

scripts/check-type-check-coverage.mjs freezes two ledgers whose entries pair a measured errors: with a free-prose note:. The ratchet compares errors against tsc and never reads the prose, so a note could narrate any count at all — including one its own field contradicts — with every gate green. Four hand repairs of that one shape are on record (#7038, #8982, #10721, #10775), the last two on the same day.

None of them closed the class, because --lower mints it. Its capture group ends at the digits, so the sanctioned one-command way to close a surplus is also the way to desynchronise a note.

Reproduced, not quoted. Driving the baselowerLedgerEntries() over the real @objectstack/metadata entry with a synthetic 89 → 80 measurement:

applied: [ 'DEBT @objectstack/metadata: 89 -> 80' ] skipped: []
'@objectstack/metadata': {
errors: 80,
note: 'code-tier 30 (TS2345 x30); config-tier 25 (TS2835 x25); noise 34 (TS7006 x33, TS6133). '

One command, a fresh instance of the class, no gate anywhere the wiser.

What lands, and why both halves together

Neither half is safe alone, and the order is the whole argument:

1. COMPOSITION — the check

Reads exactly one thing: the house-convention tier itemisation the notes already open with (code-tier N; config-tier N; noise N, either word order). It abstains, silently, everywhere else:

  • the itemisation must be the note's opening clause, reachable across a lead-in carrying no digit and no sentence break (spec-monorepo's "the workspace root itself: code-tier 4 …" qualifies);
  • a tier counted twice, or a further code-tier 9-shaped count anywhere later in the note, means the note quotes its own history → skipped, no verdict. metadata-protocol quotes the misleading note [finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278 found and is skipped for exactly that reason;
  • per-code tallies are NOT summed, contrary to the issue's item 2. They are partial by construction: plugin-rest's tally sums to 147 and says so in the same breath ("composition as counted at 153"). Summing them would red an entry whose author was being precise.

False-positive direction: an entry whose opening itemisation is deliberately partial, with the remainder described in words rather than digits, would fire wrongly. None exists.
False-negative direction: everything else, and it is most of the ledger — 21 of 33 entries carry no tier itemisation at all. That is the intended trade: a floor under one known class, not a reader of prose.

2. --lower stops minting it

plannedLowerings() now computes declareCompositionAt — the size the itemisation was tallied at — where it is pure and fixture-testable; lowerLedgerEntries() writes it as a sibling field beside the number the regex already locates. No prose surgery (the docblock's own warning against parsing this file's prose still stands), no invented composition (the tool knows the new total and nothing about which tier gave it up).

The same lowering, after:

planned: [{"ledger":"DEBT","name":"@objectstack/metadata","from":89,"to":80,"declareCompositionAt":89}]
'@objectstack/metadata': {
errors: 80,
compositionAt: 89,
note: 'code-tier 30 (TS2345 x30); config-tier 25 (TS2835 x25); noise 34 (TS7006 x33, TS6133). '
--- COMPOSITION read back over the rewritten ledger ---
clean: 0 problem(s)

compositionAt may only ever declare a pile LARGER than the field, so it cannot launder a raise; equal-to-errors is refused too, so the field only ever shrinks away. Declared-stale entries are named in the gate's green summary, for the reason the surplus is (#6376): a declaration nobody can see is the half that does the damage.

3. The :167-173 rule now covers downward drift

It only ever said "raise", on the reasoning that a note describing a larger pile "misleads in the safe direction". @objectstack/metadata drifted down (92 → 89), and a note itemising 34 + 24 + 34 over a field reading 89 does not mislead safely — it contradicts its own field. The paragraph now says so, names both same-day repairs, and points at the mechanical half in each place.

Evidence

Union re-run at f4d00e4e7d (the final commit), exit codes captured before any pipe:

gateexit
pnpm check:type-check-coverage0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
pnpm check:entry-guard (by hand — dispatch-gates is blind to it, #10784/#10705)0
node scripts/check-parse-guard.mjs (by hand, same reason)0
pnpm check:nul-bytes0
pnpm check:ratchet-remedy-authority (this file is in its CONTROL corpus)0
pnpm exec eslint scripts/check-type-check-coverage.mjs0

node scripts/pm/dispatch-gates.mjs (no paths, derives its own change set) → exit 0, 4 families, all run above.

Self-test:✓ 34 semantic case(s) + 24 observation + 25 re-measure + 28 built-closure + 19 auto-lowering case(s) hold — 11 new COMPOSITION fixtures (the two real instances transplanted, the ambiguity guard, and every compositionAt reconciliation branch), 4 new plan cases, 4 new rewrite cases, and 2 round-trip cases that lower the fixture and read the result back through the check.

Corpus check, shipped implementation over every DEBT + TEST_DEBT entry:

treeentriescheckedambiguousno itemisationfire
this branch / main33111210
699132f259^ (before the #10721 repair)33111212service-automation (code-tier 5 over errors: 3) and metadata (34+24+34 = 92 over errors: 89)
699132f259 (before the #10775 repair)33111211metadata

Both known instances, on the trees that carried them. Nothing else, on any of the three.

Ablation. Mutated tierItemisation() to if (typeof note !== 'string' || true) return null; so the rule can never fire. Confirmed on disk by anchor count, not by an editor's exit code — injected anchor 1, original anchor 0. Predicted 10 self-test failures (7 semantic, 1 plan, 2 round-trip, 0 rewrite — the rewrite cases pass the declaration explicitly). Observed 10, and the failing labels were exactly the predicted set. Restored and verified byte-identical (sha256sum -cOK). No build is involved: this is a plain .mjs run directly by node, with no dist/ between the edit and the run.

Declared narrowing

pnpm check:type-check-debt refuses without a built workspace closure (--re-measure cannot run: 55 workspace dependenc(ies) … have no built type entry point on disk), as expected. Its structural half — which is where COMPOSITION runs — executed green before the refusal. No ledger number can move from this diff: it adds no TypeScript, and every errors: value in both ledgers is byte-identical to origin/main.

Out of scope

No changeset: gate tooling publishes nothing. skip-changeset applied additively.

Generated by Claude Code


Generated by Claude Code

…er minting the drift
`DEBT`/`TEST_DEBT` pair a measured `errors` with free prose, and nothing read
the prose: a note could narrate any count at all, including one its own field
contradicted, with every gate green. Four hand repairs of that shape are on
record (#7038, #8982, #10721, #10775), the last two on the same day. None of
them closed the class, because `--lower` MINTS it -- its regex capture ends at
the digits, so the sanctioned one-command way to close a surplus is also the way
to desynchronise a note.
Both halves land together, because neither is safe alone. A check without the
tool fix leaves the ledger RED after the next `--lower` run, in whoever's PR ran
it, breaking the "closing an entry is FREE" invariant #5278/#6376 paid for. The
tool fix without the check leaves today's instances undetected.
COMPOSITION reads exactly one thing -- the house-convention tier itemisation the
notes already open with -- and abstains, silently, everywhere else. A tier
counted twice or a further tier count later in the note means the note quotes
its own history and the entry is skipped rather than guessed at. Per-code
tallies (`TS2835 x67, ...`) are deliberately NOT summed: they are partial by
construction, and plugin-rest's sums to 147 while saying "composition as counted
at 153".
`--lower` now plans a `compositionAt` declaration alongside each lowering that
would strand an itemisation, and writes it as a sibling field beside the number
it already locates -- no prose surgery, no invented composition. The staleness
becomes a declared fact the check reads and the green summary reports, instead
of a silent one nothing reads at all. `compositionAt` may only ever declare a
pile LARGER than the field, so it cannot launder a raise.
Measured: over all 33 entries on main, 11 are checked and 0 fire. Over the tree
before #10721 landed, exactly the two known instances fire and nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude