Repository navigation
[finding] nothing reads a DEBT/TEST_DEBT note, and --lower rewrites only the digits — so the advertised way to lower an entry desynchronises its prose by construction #10722
Description
Filed from the #10174 dispatch (the note-vs-field consistency check triage explicitly held back as
"a new finding, not a rider"). Not fixed here — proposal plus the root cause and a live second instance.
The class
scripts/check-type-check-coverage.mjs freezes two ledgers, DEBT and TEST_DEBT. Each entry is a
measured errors: number plus a free-prose note:. The ratchet compares errors against tsc and
never reads the prose, so a note may narrate any count at all — including one its own field
contradicts — and every gate stays green.
Known instances: #7038 and #8982 (both closed), and #10174 (service-automation recorded errors: 3
while its note opened 'code-tier 5.', closed 'Re-measured 5 at 5ab08428.', and itemised 2 + 3).
Root cause — --lower manufactures this drift by construction
lowerLedgerEntries() at :2229 rewrites the digits and nothing else:
const pattern = new RegExp(`('${...}'\s*:\s*\{[\s\S]*?errors:\s*)(\d+)`);
out = out.slice(0, block.start) + text.replace(pattern, `$1${l.to}`) + out.slice(block.end);The capture group ends at errors:\s*, so the replacement can only touch the number. The note is
outside it and is carried through untouched.
That is not an incidental gap. The gate recommends --lower in its own surplus output —
"Close it with pnpm check:type-check-debt --lower, which writes the measured number for you" —
and the surrounding docblock at :160 sells it as the thing that makes closing an entry FREE. So the
sanctioned, advertised, one-command path to lowering an entry is also the path that silently
desynchronises that entry's prose. Every future --lower mints another instance.
A live second instance, measured tonight
DEBT['@objectstack/metadata'] at :411-417:
errors: 89,
note: 'code-tier 34 (TS2345 x30, TS2322 x4); config-tier 24 (TS2835); noise 34 (TS7006 x33, TS6133). '
+ 'Re-measured 92 at 5ab08428, up from 87. ...'The itemisation sums to 34 + 24 + 34 = 92, and the note says "Re-measured 92". The field says
89. The note is internally consistent at 92 and disagrees with its field by 3 — the same shape as
#10174, a different entry, still live.
It is genuinely stale rather than merely narrating history: tonight's full --re-measure (33 entries,
built closure) reported exactly one surplus entry, @objectstack/plugin-auth, so @objectstack/metadata
measures its recorded 89 exactly. Compare TEST_DEBT['@objectstack/objectql'], which also contains an
older number ("Re-measured 333") but frames it explicitly as history and re-tallies to its recorded 355 —
that one is correct and any check must not flag it.
What would actually close the class
A general "does this prose agree with this number" check is not tractable, and should not be attempted.
A bounded one is, because these notes already follow house conventions:
- Tier itemisations —
code-tier N,config-tier N,noise N. Where an entry states a complete
tier breakdown, require the tiers to sum toerrors. This alone catches both finding: DEBT['@objectstack/service-automation'] says errors: 3 while its own note narrates 5, naming two engine.test.ts errors tsc no longer reports #10174 and metadata. - Per-code tallies —
TSxxxx xNruns, which should reconcile with their stated tier. - Leave
Re-measured Nnarratives alone, or require only that the most recent one match. objectql
shows why a naive scan over every such number produces false positives.
The cheaper half is worth pricing separately: make --lower refuse to lower an entry whose note
carries a tier itemisation that would no longer sum, or have it append a marker to the note so the
staleness is visible in the diff instead of invisible. That turns the advertised path from a drift
generator into a prompt.
Not proposed
Extending the prose rule at :167-173. It already states the lesson and cites #10174's entry as its
worked example; it did not prevent this because nothing mechanical reads it. Adding more prose about
prose drift is the one fix this class has already shown does not work.
Generated by Claude Code
Generated by Claude Code
Activity
os-zhuang commentedon Aug 21, 2026
Triage (finding concentration round): promoted to
pm:queue· type Task. The class is manufactured by the advertised path (--lowerrewrites digits only), so it will keep minting instances until the mechanism changes; the bounded check (tier itemisations must sum toerrors, leaveRe-measured Nnarratives alone) is tractable precisely because the notes follow house conventions, and the objectql entry is the stated false-positive guard. Serial chain (fold-or-serial answered): #10174 (in-flight, same file) → #10723 (data fix, S) → this card (mechanism fix) — the new check would red on the metadata entry until #10723 lands, and all three touchscripts/check-type-check-coverage.mjs. Five-gate fold refused: #10723 is a data repair, this is a mechanism change — different defect shapes. M,mode:subagent, opus; Clause-②: no.Generated by Claude Code
claude commentedon Aug 21, 2026
Claiming: session
session_01DdCnBGcHeufjrq7drTD3wt, branchclaude/issue-10722-ledger-note-consistency(pushed).Files in scope:
scripts/check-type-check-coverage.mjs(+ its self-test surface).Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 21, 2026
{ "issue": 10722, "status": "done", "branch": "claude/issue-10722-ledger-note-consistency", "pr": "https://github.com/objectstack-ai/objectstack/pull/10820", "premise_still_valid": true, "summary": "Both halves landed together, because neither is safe alone and that is the conclusion the card asked for. (1) COMPOSITION reads the note's OPENING tier itemisation (code-tier/config-tier/noise, either word order) and refuses an entry whose tiers do not sum to `errors`; it abstains silently on a tier counted twice, on a further tier count anywhere later in the note (a note quoting its own history), and on per-code `TSxxxx xN` tallies, which are partial by construction -- plugin-rest's sums to 147 while saying 'composition as counted at 153', so summing them would red an accurate entry. (2) `--lower` no longer mints the class: `plannedLowerings()` computes a `declareCompositionAt` and `lowerLedgerEntries()` writes it as a sibling `compositionAt` field beside the digits the regex already locates -- no prose surgery, no invented composition. Shipping (1) alone would have left the ledger RED after the next `--lower` run, in whoever's PR ran the tool, breaking the 'closing an entry is FREE' invariant #5278/#6376 paid for; shipping (2) alone leaves today's instances undetected. `compositionAt` may only declare a pile LARGER than the field (so it cannot launder a raise) and equal-to-`errors` is refused (so the field only shrinks away); declared-stale entries are named in the gate's GREEN summary, on the #6376 precedent that a declaration nobody can see is the half that does the damage. The general rule at :167-173 now covers downward drift, names both same-day hand repairs, and points at the mechanical half in each place. 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 on any of the three trees measured. FALSE-NEGATIVE DIRECTION: everything else, and it is most of the ledger -- 21 of 33 entries carry no tier itemisation at all and 1 more is skipped as ambiguous, so 11 of 33 are guarded. That is the intended trade: a floor under one known class, not a reader of prose. No `errors:` value changed; the diff is one file, no changeset (gate tooling publishes nothing), `skip-changeset` applied additively and read back.", "tests": "Union re-run at the FINAL commit f4d00e4e7d, every exit code captured before any pipe (`cmd > file 2>&1; ec=$?`): pnpm check:type-check-coverage 0 | pnpm check:cross-package-test-inputs 0 | node scripts/check-cross-package-test-inputs.mjs 0 | pnpm check:entry-guard 0 | node scripts/check-parse-guard.mjs 0 | pnpm check:nul-bytes 0 | pnpm check:ratchet-remedy-authority 0 (this file is in its CONTROL corpus) | pnpm exec eslint scripts/check-type-check-coverage.mjs 0. entry-guard and parse-guard were run BY HAND, per the dispatch: dispatch-gates is blind to both (#10784/#10705) and printed check:parse-guard under 'unreachable by construction'. `node scripts/pm/dispatch-gates.mjs` with no paths (it derives its own change set from the merge base) exit 0, 4 families, all four run above. SELF-TEST, the gate's own verdict line: '✓ check:type-check-coverage --self-test — 34 semantic case(s) + 24 observation case(s) + 25 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) hold.' -- 11 new COMPOSITION fixtures (both real instances transplanted, the ambiguity guard, every compositionAt branch), 4 plan cases, 4 rewrite cases, 2 round-trip cases. MINTING REPRODUCED on the base rewriter, real metadata entry, synthetic 89->80: applied ['DEBT @objectstack/metadata: 89 -> 80'], entry left as `errors: 80,` over a note still itemising 'code-tier 30 ...; config-tier 25 ...; noise 34 ...' (= 89) -- one command, a fresh instance, every gate green. AFTER, same lowering through the shipped plan+rewrite: planned [{\"ledger\":\"DEBT\",\"name\":\"@objectstack/metadata\",\"from\":89,\"to\":80,\"declareCompositionAt\":89}], entry becomes `errors: 80,` / `compositionAt: 89,` and COMPOSITION read back over the rewritten ledger reports 'clean: 0 problem(s)'. CORPUS CHECK, shipped implementation over all DEBT+TEST_DEBT entries: this branch 33 entries / 11 checked / 1 ambiguous / 21 no-itemisation / FIRED=0. At 699132f259^ (before the #10721 repair): FIRED=2 -- service-automation ('code-tier 5' over `errors: 3`) and metadata (34+24+34 = 92 over `errors: 89`). At 699132f259 (before the #10775 repair): FIRED=1 -- metadata. Both known instances on the trees that carried them, nothing else on any of the three; no entry was adjusted to make the rule pass. ABLATION: mutated tierItemisation() to `if (typeof note !== 'string' || true) return null;` so the rule can never fire. Confirmed ON DISK by anchor count, never by an editor's exit code -- injected anchor grep -c = 1, original anchor grep -c = 0. PREDICTED 10 failures (7 semantic, 1 plannedLowerings, 2 round-trip, 0 rewrite -- the rewrite cases pass the declaration explicitly so the mutation cannot reach them). OBSERVED '✗ check:type-check-coverage --self-test — 10 failure(s)' and the failing labels were exactly the predicted set. Restored and verified byte-identical: `sha256sum -c` -> 'scripts/check-type-check-coverage.mjs: OK', self-test back to 0. NO BUILD is involved in either leg -- this is a plain .mjs run directly by node with no dist/ between the edit and the run, so the stale-artifact failure mode the ablation rule guards against cannot apply here. DECLARED NARROWING: `pnpm check:type-check-debt` exits 1 with '--re-measure cannot run: 55 workspace dependenc(ies) ... have no built type entry point on disk', as the dispatch predicted. Its STRUCTURAL half -- where COMPOSITION runs -- printed 'check-type-check-coverage: OK' green before the refusal. The ledger numbers cannot move from this diff: it adds no TypeScript, and every `errors:` value in both ledgers is byte-identical to origin/main.", "open_questions": [], "out_of_scope_findings": [ "filed as #10821: plugin-rest's TEST_DEBT per-code tally lists TS2835 x67 + TS7006 x57 + TS2554 x13 + TS2550 x10 = 147 while declaring 'composition as counted at 153', with no 'plus N singletons' remainder clause of the kind every other abridged tally in the ledger carries; unresolvable without a built-closure re-measure, and deliberately outside this card's rule (per-code tallies are not summed)" ] }Generated by Claude Code