Uh oh!
There was an error while loading. Please reload this page.
fix(tooling): make an unparseable source REFUSE instead of scoring clean - #10573
Conversation
`ts.createSourceFile` never throws. A syntax error — or the wrong
`ScriptKind` — returns a `SourceFile` built by error recovery with the
errors parked on `parseDiagnostics`, a property nothing under `scripts/`
read. Fifteen gates walked TypeScript that way, so any of them could
report a confident zero about a file it never managed to read.
This was not latent. `check-engine-double-contract.mjs` forced
`ts.ScriptKind.TSX` on all 2504 `*.{test,spec}.{ts,tsx,mts}` files under
`packages/` and `examples/`; in TSX a `<` opens a JSX element, so an
ordinary `new Map<string, X>()` made the rest of the file wreckage. 32 of
those 2504 parsed with errors (up to 633 diagnostics in one file) and the
gate printed `OK — 342 pinned, 133 in the DEBT ledger, 2 exempt`. Read
under the ScriptKind their own names imply, three of those files turn out
to pin six engine doubles the ledger had never recorded.
- `scripts/ts-parse.mjs` — the one sanctioned parse. Reads the
diagnostics and exits 3 (distinct from 1: "could not read the tree" is
not "found violations"), naming the file, the line:column and
TypeScript's own message. It exits rather than throws because
`try { createSourceFile } catch { continue }` is already written in
`packages/lint/src`, against a throw that cannot happen.
- `scripts/check-parse-guard.mjs` — the half that makes it hold. A raw
`ts.createSourceFile` anywhere in `scripts/**` outside the parser home
is a failure, so there is no second spelling left to drift from. Same
shape as `invoked-as.mjs` + `check-entry-guard.mjs`, and for the same
measured reason.
- All 32 call sites across the 15 gates converted; the three
engine-double-contract sites that forced TSX over real files now let
the file name decide.
- `engine-double-contract.pinned.json` grows by the 6 rows that became
visible: 342 -> 348 pinned, 0 lost.
Fixes: #10133
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wtos-zhuang
commented
Aug 21, 2026
PM review — ⭐⭐ accepted. You falsified the card's central claim, and the thing it was hiding is the biggest find of the night.⛔ "Nothing is red today" was wrong, and the gate was hiding real pins
⇒ Letting the file name pick the ScriptKind turns that green into 6 problems, and three files pin six engine doubles the ledger had never recorded. ⭐ That is not a latent gap. A gate that exists to pin engine doubles was mangling its own inputs into wreckage and reporting OK over them, and six real pins were invisible for as long as that has been true. The card said the class had no live instance; measurement found one inside the very gate the card listed first. ⭐ The sub-finding that reframes the whole class
⭐ That is the sharpest statement anyone made tonight about this defect family, and it retroactively justifies every "refuse rather than under-report" ruling this repo has landed this week. A shrinking count is a symptom that may not appear; the silent green is the invariant. ⇒ Anyone who tries to detect this class by watching totals will find nothing and conclude there is nothing. What I checked hardest
Follow-ups — all three correctly placed#10574 — #10575 — the same class outside
CI is finishing. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…TypeScript parser entry points Fixes#10574Fixes#10575 `scripts/check-parse-guard.mjs` landed in #10573 and nothing ran it: a gate that existed and enforced nothing. Wire it (`pnpm check:parse-guard` + a step in `lint.yml`'s `Lint & Repo Gates`), and close the two scope caveats its own header carried, so its green line stops being narrower than it reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…ER.md, with a parser that refuses rather than fails open `check-platform-checklist` enforced a closed vocabulary for `status`, `priority`, `surface`, `oracle` and `blocked.by` — and never read `traps` at all. So an item could carry any trap name and the validator stayed green, which is how eight undocumented values drifted in (#10416), and how a TYPO in a documented one (`hydration-races` for `hydration-race`, which is on 79 of the 205 items) lands as simply a twentieth trap that no runner rules out. The vocabulary is READ from RUNNER.md's `### Trap vocabulary` table, not copied into the script: a sixth hardcoded `Set` would only move the drift one level up, between the script and RUNNER.md, with nothing watching that seam. The load-bearing part is therefore not the parser but its positive control — `extractTrapVocabulary` refuses on a table it cannot recognise (heading renamed, table moved, zero rows, a row that lost its backticks) instead of returning an empty allow-list, and a 22-assertion fixture battery proves the refusal still fires. The battery runs inline on every invocation, not only behind `--self-test`, because this gate is not CI-wired by maintainer decision and its `pnpm` alias lives in the fenced root package.json (#9465) — a self-test nothing runs is the #10574/#10573 defect. The OK line now states what the parse read: `traps: 19 documented, 19 in use (extractor control: 22 assertions)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Fixes#10133
ts.createSourceFilenever throws. A syntax error — or the wrongScriptKind— returns aSourceFilebuilt by error recovery, with the errors parked onparseDiagnostics, a property that nothing underscripts/read. So a gate walks the wreckage, finds none of the shapes it is looking for, and scores the file clean: a file the gate could not read, reported as a file with nothing to report.The card said "nothing is red today". That was wrong, and the measurement is the point
check-engine-double-contract.mjswalks 2504*.{test,spec}.{ts,tsx,mts}files underpackages/andexamples/while forcingts.ScriptKind.TSXon every one of them. Under TSX an angle bracket opens a JSX element, so an ordinary generic type argument —new Mapwith its two type parameters, a generic arrow — made the rest of the file wreckage. 32 of those 2504 files parsed with errors, up to 633 diagnostics in one file, and the gate printed:Read under the ScriptKind their own file names imply, the same 2504 files give
6 problem(s). Nothing about the tree changed; only whether the gate could read it:maintoday)OK(exit 0)6 problem(s)(exit 1)The six are
RETAINEDfindings on three files —meta-object-primary-designation-roundtrip,meta-object-search-companion-roundtrip,meta-object-tenant-index-roundtrip— each pinning engine doubles the ledger had never recorded, because the scan had never been able to see them.--writerecords them: the pinned ledger goes 329 rows to 335, 6 added or grown, 0 lost, and the seam ledger is unchanged (0 added, 0 lost).What landed
scripts/ts-parse.mjs— the one sanctioned parse. ReadsparseDiagnosticsand refuses, naming the file, theline:columnand TypeScript's own message. It exits (code 3) rather than throwing:try { createSourceFile } catch { continue }is already written twice inpackages/lint/src, dead code against a throw that cannot happen and a silent skip the moment one arrived. Code 3 is deliberately not 1 — "could not read the tree" is a different verdict from "found violations".scripts/check-parse-guard.mjs— the half that makes it hold. A rawts.createSourceFileanywhere inscripts/**outside the parser home is a failure. Comments and string / template / regex literals are masked first (js-comment-mask.mjs), so the helper's own documentation is not read as a call site.ScriptKindit named.engine-double-contract.pinned.jsongrows by the 6 rows that became visible.Shared helper vs per-gate — the ruling, and why
Shared helper, plus a gate that removes the second spelling. The real objection to a helper is that it becomes a second source of truth while the first one is still reachable — so the answer is to remove the first source, not to argue about drift. That is not a theory about this tree; it is the tree's own result twice over, both in
scripts/:invoked-as.mjsreplaced eleven hand-typed spellings of "was I run?" across 33 files, nine of them wrong — andcheck-entry-guard.mjsexists because the one-time sweep did not stop a twelfth.js-comment-mask.mjsreplaced two privatestripCommentsfamilies, each silently wrong in a different direction.Fifteen independent copies of "and check the diagnostics" drift the same way: one reads
.lengthon a field it forgot can be undefined, one warns instead of failing, one is simply never typed into the sixteenth gate — and a missing copy is invisible, because its symptom is a green line.How the entry-guard constraint shaped it.
check-entry-guard.mjsbansprocess.argv[1]inscripts/**outsideinvoked-as.mjs, verified here (node scripts/check-entry-guard.mjs→121 scripts/ file(s), exit 0). Three consequences, designed in from the start rather than discovered:ts-parse.mjshas a--self-testCLI mode, so it needs an entry guard, and it may not type its own. It importsisEntrypointfrominvoked-as.mjs.check-entry-guard.mjsis the template for the enforcing gate — including the literal-masking, which is load-bearing here for a reason it is not there: this helper's header discusses the banned call at length, so an unmasked scan would fabricate findings out of its own documentation.invoked-as.mjsexplains whypackages/cli/src/utils/invocation.tsis a deliberate duplicate rather than an import:scripts/runs as plain.mjsagainst a possibly unbuilt tree. The same argument applies unchanged, so this coversscripts/**and the package-side parses are filed rather than swept in.Is any gate hiding a real unparseable file today?
Yes — one was, and it is fixed here. After the fix, no. The sweep was run, not assumed:
.ts/.tsx/.mts/.ctsfiles: 0 have parse errors under the ScriptKind their extension implies. 40 are wreckage under forced TSX and 15 under forced TS — which is why the ScriptKind a call site pins is part of this defect and not a separate one.OS_TOOLING_PARSE_CENSUSset, the 15 converted gates perform 5157 parses and 0 refusals — per-gate distinct file names summing to 3251 (a name shared between two gates counts once in each, so that is a per-gate denominator, not a repo-wide set). Every gate now prints its own numerator and denominator.Positive control for that zero, because a zero-hit with no control is not a measurement: restoring the
ScriptKind.TSXthatmainships today makes the refusal fire immediately on the unmodified tree —— a file
main's gate says nothing about while printingOK.Reproduced harm, and the ablations
Harm, on a real gate over a real population. A genuine syntax error injected into
packages/qa/dogfood/test/read-coercion-conformance.test.ts(incheck-verify-stand-in-erasure's scan set):origin/mainOK … 10 call site(s) reached— zero mentions of the fileline:column, TypeScript's messageThe refusal is the load-bearing part, isolated. Same converted gate, same poisoned tree, with only
if (diagnostics.length > 0)neutered ints-parse.mjs: exit 0 and the identical greenOK … 10 call site(s) reached.A sub-finding worth recording: the count does not reliably move. TypeScript's recovery kept all 10 call sites through 27 parse diagnostics. Controlled fixtures show it can go either way — an unterminated template literal took a three-site file to one site, while a broken statement left all three. That is exactly why the silent green is the dangerous half: you cannot tell from the number whether anything was lost.
Every assertion added was ablated:
check-kernel-hook-pairs.mjsscripts/check-kernel-hook-pairs.mjs:108ts-parse.mjsOK … 10 call site(s)ScriptKind.TSXin engine-double-contractplugin.test.tsEach mutation was confirmed on disk by anchor count before the measurement was read, and each restore by
git hash-objectplus an emptygit status --porcelain.Gates
19 families from
node scripts/pm/dispatch-gates.mjs(no path arguments), run atd7b687604b, pluscheck:nul-bytes,check:entry-guardand both new self-tests: all green, one environment refusal —node scripts/pm/check-half-states.mjsexits 3 withPREREQUISITE NOT MET — the token in the environment is not a valid GitHub token, unrelated to this diff; itspnpm check:pm-half-statesform passes 715 cases.Left undone, deliberately — needs a decision
check:parse-guardis not wired into CI. That needs acheck:*entry in rootpackage.jsonand a step in.github/workflows/lint.yml. Rootpackage.jsonis fenced (Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465) andlint.ymlis claimed by in-flight work (lint.yml's "Build the ledgered packages' dependencies" step duplicates the closure refresh once #8330 lands #8331), so this PR does not touch either. Until it is wired, the gate is dormant and a sixteenth raw call would land unnoticed — the conversions and the refusal are fully live regardless.scripts/**are not covered and are filed:packages/lint/src(3 sites, two of them behind the deadtry/catchcited above),packages/lint/scripts/check-doc-formula-expressions.mjs,packages/spec/scripts/check-skill-examples.ts,packages/spec/scripts/lib/strictness-ledger.ts,packages/cli/src/utils/detect-free-identifiers.ts.ts.createProgramandts.transpileModulecarry the same class through different APIs and are named in the gate header as deliberately not covered, so its green line does not over-claim.Changeset
None. The whole diff is root
scripts/**; the root package isprivate: trueand every published package ships onlydist,README.mdandCHANGELOG.md. Nothing here reaches a publish surface — measured at diff time, not defaulted.skip-changeset.Merge-order note — one live overlap, reported rather than absorbed
#10562 (
claude/route-envelope-decision-y3itw6) is open againstscripts/check-route-envelope.mjs, +312/-24 — the same file this PR converts (3 call sites plus an import). Whichever lands second should re-runnode scripts/check-parse-guard.mjs: if #10562's 312 added lines introduce a rawts.createSourceFile, the guard will name it, and the fix is the one-line conversion this PR already applies three times in that file. Nothing here was changed to accommodate it.Follow-ups filed: #10574 (wire
check:parse-guardinto CI — blocked on the two claimed surfaces above) and #10575 (the same class outsidescripts/ts-parse.mjs's reach:ts.createProgram,ts.transpileModule, and 10 sites underpackages/).Generated by Claude Code
Generated by Claude Code