Skip to content

fix(scripts): make check-test-typecheck signatures path-independent - #18173

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-17739-typecheck-signature-abs-path
Sep 14, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-17739-typecheck-signature-abs-path

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #17739

The defect

normalizeMessage in scripts/check-test-typecheck.mts keeps a quoted span verbatim when it is short and free of the structural punctuation that makes tsc print a whole shape. A TS6059 message reads File 'ABS' is not under 'rootDir' 'ABS', and an absolute filesystem path carries none of that punctuation — so its LENGTH was the only thing deciding whether it survived, and the identical diagnostic produced one signature in a short-path checkout and a different one in a long-path checkout.

The ledgers were recorded from a long path, and CI's checkout path is long too, which is why this survived: it reds only locally and in short-path agent worktrees.

The two readings — pristine main, same commit, same command

Both worktrees at origin/main 7e58d177e, both after pnpm install and the same pnpm --workspace-concurrency=2 --filter '@objectstack/lint^...' build dependency-closure build. Nothing but the checkout path differs.

worktree packages/lint/src pnpm --filter @objectstack/lint typecheck
/home/user/objectstack-issue-17739 52 chars exit 0OK, 2 file(s) / 6 error(s) / 2 pinned signature(s)
/tmp/w9 25 chars exit 14 problem(s)

The short-path run reports the same six already-ledgered TS6059 errors as both ARRIVED (... 'rootDir' '/tmp/w9/packages/lint/src' — a signature the ledger does not record) and VANISHED (... 'rootDir' '...' — the recorded one absent), and its ARRIVED text instructs the reader to go repair errors that are not new. The card's headline is therefore confirmed, and confirmed on today's main, not only on the 431c757120 it was filed against.

It also contradicted the function's own docblock: an absolute checkout path is not "a name, a dotted path, or a small literal type", and it changes when the checkout moves rather than when the error does.

The repair

A quoted span that is a filesystem-absolute path (POSIX, Windows drive letter with either separator, UNC) now collapses at every length — which is exactly what a long-path checkout already did. The change is one added condition plus its detector, isAbsolutePathSpan, exported so the self-test can prove it still reaches its subject.

Rejected alternative: relativising the path against the repo root. It buys a sharper signature, but it re-spells the signature every existing ledger already holds in its collapsed form — churning a maintainer-gated shrink-only ratchet for discrimination no ledger has ever recorded. Measured across all 19 test-typecheck-debt.json ledgers before choosing: quoted spans beginning with / = 0, drive-letter spans = 0, against 89 capital-initial spans as the firing control.

Effect on the existing ledger: none, measured two ways.

  1. By construction, from the counts above: no recorded signature contains an absolute-path span, so none can be re-spelled.
  2. Empirically, at the final head with the fix in place: pnpm -r run check:test-typecheck over every package that wires this gate — 36 packages, 36 OK lines, 0 problems, and git status --porcelain empty afterwards (no ledger rewritten). Same lint verdict before and after the fix on the long path: 2 file(s) / 6 error(s) / 2 pinned signature(s).

This does not weaken the gate: it collapses only what a long path already collapsed, so no discrimination any ledger or CI has ever relied on is given up. An over-masking control in the self-test pins that short non-path spans (a relative specifier, a named type) still survive verbatim.

Tests

A sixth self-test battery, Path-independence: a signature must not move with the CHECKOUT (#17739), floor 7, registered in SELF_TEST_BATTERIES with SELF_TEST_BATTERY_FLOOR raised 5 to 6. It pins the property (one signature from two checkout paths), a control that the fixture really straddles the 32-character threshold the defect lived on, a zero-churn pin against the spelling the ledgers hold today, the Windows spellings, an over-masking control, and a detector-discrimination control.

Ablation (one-shot, on the throwaway short-path worktree whose copy was proved byte-identical to this branch's blob 76c65e7e41f1230139d7cbdbdf48ce09a3f0b462 before the mutation, and restored to that same hash after):

leg on-disk proof result
remove && !isAbsolutePathSpan(inner) marker count 1 to 0, blob hash 76c65e7e to d568fd90 self-test exit 1, 3 named #17739 failures — the property, the zero-churn pin, the Windows pin; pnpm --filter @objectstack/lint typecheck exit 1
restore from the branch git hash-object back to 76c65e7e41f1230139d7cbdbdf48ce09a3f0b462 self-test exit 0

The ablated property failure prints the defect verbatim: got "TS6059: File '...' is not under 'rootDir' '/tmp/w9/packages/lint/src'. ..." vs "TS6059: File '...' is not under 'rootDir' '...'. ...".

Gates

node scripts/pm/dispatch-gates.mjs --commands derived 27 families for this one-path diff; all 27 run at the final head f7c116ae2, all exit 0, reconciled with --ran carrying exit codes: 27 derived, 27 run, 0 NOT-MEASURED (a DERIVED zero). check:type-check-debt first returned exit 3 (PREREQUISITE NOT MET) twice — first for an unbuilt workspace closure, then for an OOM under the caller's own --max-old-space-size=4096, which is tighter than the CI-shaped 6144 ceiling that gate pins. Neither exit 3 is recorded as a measurement; the closure was built (turbo run build, 72/72) and the gate re-run at 8192 to a real exit 0.

Also run: the self-test through tsx directly, eslint on the changed file (0 errors / 0 warnings over 1 file, read from --format json; eslint.config.mjs enables no type-aware linting, so a one-file diff in scripts/ cannot move any untouched file's verdict), and a manual control-character sweep over the changed file (0 hits) beside check:nul-bytes.

Changeset

skip-changeset. Measured on the publish surface rather than asserted: all 70 non-private packages declare files[], the distinct entries are CHANGELOG.md README.md api-surface dist json-schema liveness llms.txt prompts spec-changes.json src/**/*.zod.ts, 0 of them escape their own package directory, and grepping the 69 built dist trees for the changed symbols gives isAbsolutePathSpan 0 and STABLE_QUOTED_SPAN 0 against a positive control of ObjectKernel 106. The repo-root scripts/ directory publishes nothing.

Acceptance notes

  • noted, not filed: the card's declared scope limit still stands — only TS6059 was measured to carry an absolute path in a quoted span, and no sweep of other diagnostic codes was done. The repair does not need one (it is keyed on the span's shape, not on the TS code), so this is an observation rather than a finding. Carrier: whoever next reads normalizeMessage.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk


Generated by Claude Code

A diagnostic signature must change only when the error changes. TS6059
reads `File 'ABS' is not under 'rootDir' 'ABS'`, and an absolute path
carries none of the structural punctuation the collapse rule keys on, so
its LENGTH was the only thing deciding whether it survived verbatim. A
25-character checkout produced one signature and a 52-character checkout
another, for the identical error.

Measured on pristine origin/main @ 7e58d17, same command, same commit:
`pnpm --filter @objectstack/lint typecheck` exits 0 from
/home/user/objectstack-issue-17739 (52 chars to packages/lint/src) and 1
from /tmp/w9 (25 chars), where it reports the same six ledgered TS6059
errors as both ARRIVED and VANISHED at once and tells the reader to go
repair errors that are not new.

A quoted span that is an absolute filesystem path (POSIX, Windows drive,
UNC) now collapses at every length, which is exactly what a long-path
checkout already did. Relativising against the repo root was the
alternative and is rejected: it re-spells the signature every ledger
already holds collapsed, churning a maintainer-gated shrink-only ratchet
for discrimination no ledger records. Measured across all 19
test-typecheck-debt.json ledgers before choosing: quoted spans beginning
`/` = 0, drive-letter spans = 0, against 89 capital-initial spans as the
firing control.

Self-test gains a seventh-case battery pinning the property (one
signature from two checkout paths), the control that the fixture
straddles the 32-character threshold, the zero-churn pin against the
ledgered spelling, the Windows spellings, and an over-masking control
proving short non-path spans still survive.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

1 participant