Pin test-typecheck debt by error IDENTITY, not per-file count - #13512
Merged
os-project-manager merged 2 commits intoAug 30, 2026
Merged
Conversation
The EXACT ratchet in scripts/check-test-typecheck.mts recorded ONE INTEGER per file. An integer measures a quantity, never an identity, so a file's entire error population could rotate underneath a constant number and the gate printed OK. Measured by ablation in packages/rest, not argued (#13470): src/rest.test.ts was recorded at 2, PR #13466 replaced both hand-built IHttpRequest literals with a typed builder, and it measured 2 again -- while NEITHER error was the same error. tsc reports at most ONE argument-assignability error per call, so the request literals had been MASKING response-literal errors at the very same two call sites; repairing the request unmasked the response one line down. A ledger entry is now a map of normalized error SIGNATURE to count. A signature is the TS code plus the diagnostic message with churn-prone spans collapsed, and it carries NO line or column -- so unrelated edits above an error do not touch the ledger, and only a change to WHICH error is present does. Both halves of the set difference are named: which signature ARRIVED and which VANISHED. Counts are provably unmoved by the migration: rest 1 file / 2 errors, spec 54 files / 262 errors, identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…st-typecheck-ratchet-identity
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 30, 2026
os-project-manager
marked this pull request as ready for review
August 30, 2026 16:10
os-project-manager
enabled auto-merge
August 30, 2026 16:10
os-project-manager
deleted the
claude/issue-13470-test-typecheck-ratchet-identity
branch
August 30, 2026 16:30
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13470
The EXACT test-typecheck ratchet recorded one integer per file. An integer measures a quantity, never an identity, so a file's entire error population could rotate underneath a constant number while the gate printed
OK.A ledger entry is now a map of normalized error signature to count.
The ablation is the deliverable
The card's finding was measured, so the repair is measured the same way: revert exactly one of the two migrated call sites in
packages/rest/src/rest.test.ts, so that site re-masks its own response error. The file's error population is substituted wholesale at strictly constant cardinality, and the old and new gates then judge the same mutated tree.Mutation proven on disk against the HEAD blob, never by an editor's exit code:
tsc under the mutation — 2 diagnostics, exactly what the ledger records:
Leg A — the OLD gate over that tree (exit 0):
Leg B — the NEW gate over the SAME tree (exit 1), naming both halves:
Restore proven byte-identical for all three touched files, finishing on empty
git diff HEADand emptygit status --porcelain.No rebuild leg is owed: the gate compiles
src/**/*.test.tsfrom source viatsconfig.test.json, so the mutated file is the subject tsc reads. There is nodist/indirection to go stale.Why a signature, and why it excludes position
The card warned that the obvious identity — code plus line/column — churns on every edit above it and would make a generated ledger a merge-conflict magnet. So a signature carries no position: it is the TS code plus the message with churn-prone spans collapsed.
That collapse is measured, not guessed, over the 264 diagnostics the ledgered packages actually carry:
packages/restfile reduces toRaw messages embed whole structural types including tsc's own version-dependent elisions (
... 37 more ...) — confirming the dispatch's guess that a full message string is not stable enough to pin. Collapsing every quoted span that is long or structural leaves the discriminating half: the code, the prose, and the named types. That is exactly what separatesparameter of type 'IHttpRequest'fromparameter of type 'IHttpResponse'.Both properties are pinned in the self-test rather than asserted here:
Scope, stated plainly
The declared file surface on the card was
packages/rest/test-typecheck-debt.jsonplus the fixture. The repair necessarily reaches two files beyond it, and both are named by the card's own body ("the blindness is inscripts/check-test-typecheck.mts, not inpackages/rest"; "the ledger is read by every package that carries one"):scripts/check-test-typecheck.mts— where the blindness lives.packages/spec/test-typecheck-debt.json— 54 files / 262 errors. Leaving it on bare counts would mean the fix protects 2 errors and leaves 262 unprotected, which is a declared-but-unenforced surface.The migration is provably count-neutral: spec's ledger was cross-checked against a live tsc run before regenerating — 54 files / 262 errors on both sides, 0 mismatches — and it re-records at 54 files / 262 errors / 146 signatures.
packages/restlikewise holds at 1 file / 2 errors.packages/client/test-typecheck-debt.jsonis deliberately untouched: itsentriesis{}, which is already a valid signature map, so the gate is green either way. Regenerating it would require building a 10-package closure that could write bogus entries; it will migrate on its next legitimate regeneration.A legacy bare-count entry is red, not silently trusted — a count cannot be judged for identity, so a ledger predating the pin must be re-recorded rather than read.
Governance
Recording a new signature expands an EXACT shrink-only ratchet exactly as adding a whole file does, and it is the more tempting of the two because the file total need not have moved. The ARRIVED verdict therefore carries the same
MAINTAINER-ONLYmarker the unledgered-file verdict carries (the #8435 convention), and the detector was widened to reach it. VANISHED is the ratchet tightening and stays unmarked, with a self-test control pinning that asymmetry in both directions.Verification
Gate and self-test verdict lines, both quoted from the gates' own output (never a bare
$?):Green at
db354780, after mergingorigin/main:pnpm lint(full repo,eslint . --no-inline-config) — exit 0, no narrowing claimedcheck:nul-bytes,published-files,test-source-alias,parse-guard,entry-guard,watch-hint-literal,pnpm-filter-targets,agent-test-spelling,cli-command-ids,merge-driver,doc-authoring,dispatcher-error-vocabulary— all exit 0node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackfrom the real change set, not a hand-written listcheck:type-check-debt— convention-triggered, and honestly reported. This script sits in the ROOT tsc program, so it can move that shrink-only raw count. The full gate refuses on an unbuilt worktree (26 workspace deps have no built type entry point) — that is NOT MEASURED, never green. The root-program half is closure-independent, because the root config excludespackages/,apps/andexamples/and this script imports only node builtins. Measured directly, swapping only this file between its base and its new version:Unmoved.
Object.hasOwnoccurrences also held at 3, the ES2022 API this file's own comment warns costs a TS2550 each under the rootlib: ES2020.No changeset
This PR publishes nothing:
test-typecheck-debt.jsonis absent from every package'sfilesarray (dist, README, CHANGELOG only), and rootscripts/ships in no package. Bylint.yml's own prescription, "this PR edits a CI-internal script" is the textbookskip-changesetcase. Clause (2) was verified the same way and does not apply — the ledger is a dev-time debt record, not a published or contract-gating surface.Generated by Claude Code
Generated by Claude Code