Skip to content

Pin test-typecheck debt by error IDENTITY, not per-file count - #13512

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13470-test-typecheck-ratchet-identity
Aug 30, 2026
Merged

Pin test-typecheck debt by error IDENTITY, not per-file count#13512
os-project-manager merged 2 commits into
mainfrom
claude/issue-13470-test-typecheck-ratchet-identity

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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:

HEAD blob            5d5571a97515230b3da5584cbb9823f466628672
mutated fixture blob 4f3e15d08e49e4ec8c4102b251cb970efbbbca39
removed-anchor       0   (was 1)
surviving other site 1

tsc under the mutation — 2 diagnostics, exactly what the ledger records:

src/rest.test.ts(2064,7): error TS2345: ... is not assignable to parameter of type 'IHttpRequest'.
src/rest.test.ts(2090,7): error TS2345: ... is not assignable to parameter of type 'IHttpResponse'.

Leg A — the OLD gate over that tree (exit 0):

check:test-typecheck: OK - @objectstack/rest's test layer compiles under
packages/rest/tsconfig.test.json; 1 file(s) / 2 error(s) held in
test-typecheck-debt.json (shrink-only, .../issues/5286).

Leg B — the NEW gate over the SAME tree (exit 1), naming both halves:

check:test-typecheck: 2 problem(s)

  - src/rest.test.ts: 1 type error(s) carrying a signature the ledger does not
    record - ARRIVED: TS2345: Argument of type '...' is not assignable to
    parameter of type 'IHttpRequest'. ... MAINTAINER-ONLY, NOT a co-equal
    option: add the signature to test-typecheck-debt.json.
  - src/rest.test.ts: 1 type error(s), ledger records 2 - the debt SHRANK for
    TS2345: Argument of type '...' is not assignable to parameter of type
    'IHttpResponse'., which is the goal. Re-record it ...

Restore proven byte-identical for all three touched files, finishing on empty git diff HEAD and empty git status --porcelain.

No rebuild leg is owed: the gate compiles src/**/*.test.ts from source via tsconfig.test.json, so the mutated file is the subject tsc reads. There is no dist/ 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:

reading value
raw message length (min / median / max) 25 / 70 / 546
distinct raw code+message 187
distinct normalized signature 138
longest signature 186
packages/rest file reduces to 1 key

Raw 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 separates parameter of type 'IHttpRequest' from parameter of type 'IHttpResponse'.

Both properties are pinned in the self-test rather than asserted here:

  • position-blind — the same errors at different line/column produce an identical ledger;
  • identity-sharp — swapping only the named parameter type produces a different key, cardinality held equal.

Scope, stated plainly

The declared file surface on the card was packages/rest/test-typecheck-debt.json plus the fixture. The repair necessarily reaches two files beyond it, and both are named by the card's own body ("the blindness is in scripts/check-test-typecheck.mts, not in packages/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/rest likewise holds at 1 file / 2 errors.

packages/client/test-typecheck-debt.json is deliberately untouched: its entries is {}, 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-ONLY marker 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 $?):

check:test-typecheck --self-test - 11 semantic case(s), the parser, the #13470
identity pins ..., the #8435 convention ... and the #12624 ledger-prose pins ...
all hold.

check:test-typecheck: OK - @objectstack/rest's test layer ...;
1 file(s) / 2 error(s) / 1 pinned signature(s) ... (shrink-only and identity-pinned)

check:test-typecheck: OK - @objectstack/spec's test layer ...;
54 file(s) / 262 error(s) / 146 pinned signature(s) ... (shrink-only and identity-pinned)

Green at db354780, after merging origin/main:

  • pnpm lint (full repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • check: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 0
  • gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the real change set, not a hand-written list

check: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 excludes packages/, apps/ and examples/ and this script imports only node builtins. Measured directly, swapping only this file between its base and its new version:

mine:  root program 31 errors total, 1 in scripts/check-test-typecheck.mts
base:  root program 31 errors total, 1 in scripts/check-test-typecheck.mts

Unmoved. Object.hasOwn occurrences also held at 3, the ES2022 API this file's own comment warns costs a TS2550 each under the root lib: ES2020.

No changeset

This PR publishes nothing: test-typecheck-debt.json is absent from every package's files array (dist, README, CHANGELOG only), and root scripts/ ships in no package. By lint.yml's own prescription, "this PR edits a CI-internal script" is the textbook skip-changeset case. 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

claude added 2 commits August 30, 2026 15:10
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 2 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json, packages/spec/test-typecheck-debt.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)).

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json, packages/spec/test-typecheck-debt.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e7191ce71da1a8cc25c05e3d372eb4f63be5517apackageMentionDocs.

@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 30, 2026 — with Claude
@os-project-manager
os-project-manager marked this pull request as ready for review August 30, 2026 16:10
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit 57827b6 Aug 30, 2026
38 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13470-test-typecheck-ratchet-identity branch August 30, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants