Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): the two ESLint ratchets refuse a population that will not parse (#10123) - #10130
Conversation
…ot parse (#10123) Both ratchets drive ESLint through its Node API and count the messages that match their own rule. ESLint does not throw on a parse failure — it returns it as a message with no rule id and `fatal: true` — so an unparseable file matched neither filter, contributed zero sites, and the gate printed `✓ … holds` and exited 0: a clean verdict on a file it had never read, while `pnpm lint` failed loudly on the same input. The check lives once, in scripts/eslint-fatal-guard.mjs, and both gates route their run through `lintFilesStrict()` instead of `eslint.lintFiles()`. A parse failure now names the file, the position and the parser's message, and exits 2 — the code both gates already reserve for "refusing to report clean", as distinct from 1 = "the ratchet moved". Site counts are unchanged. The query-options `--self-test` (run by CI ahead of the gate) proves the guard in both directions over real ESLint output, and asserts from source that both gates still route through it — `pnpm check:slot-lookup` has no self-test hook of its own, so that assertion is the wired coverage of its call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — Clause-② gate. Declared
Governed surfaces re-derived from Ruling on the declared file-surface deviation. The dispatch named two scripts plus a fixture; the dev shipped a third file, the shared Evidence I am accepting on:
Carried forward, not blocking:
Correction to my own dispatch, on the record. I gave Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10123
The hole
Both ratchets drive ESLint through its Node API over
packages/**and count themessages matching their own rule. ESLint does not throw on a file that fails to
parse — it returns the failure as an ordinary message with no rule id and
fatal: true, which matches neither filter (m.message === SLOT_LOOKUP_ANY_MESSAGE,m.ruleId === QUERY_OPTIONS_RULE_ID). The file therefore contributed zero sitesand each gate printed
✓ … holdsand exited 0 — output byte-identical to a filethat was measured and is clean.
Re-derived on this branch point (
5262124388) against apackages/**file holding asingle syntax error:
The same file through the root
lintscript's spelling (eslinton that path, with--no-inline-config) exits 1. That asymmetry is what the issue flags: each ratchet'sown docblock argues it exists because a green
pnpm lintproves nothing for the filesit covers, and these two were the ones going quiet.
What changed
scripts/eslint-fatal-guard.mjs(new) — the check, once.lintFilesStrict()wrapseslint.lintFiles(), collects everyfatalmessage (cross-checked against ESLint's ownper-result
fatalErrorCount, so a future ESLint that moves the flag cannot make thissilent again), and on any hit prints the file, the position and the parser's message and
exits 2. Exit 2 is what both gates already reserve for "refusing to report clean"
(renamed rule, rescoped population), as distinct from 1 = "the ratchet moved"; a parse
failure belongs to the first family, because nothing moved — the measurement did not
happen.
lintFilesStrict()instead ofeslint.lintFiles(), so theguard also covers their
--updatepaths (measurement precedes the write in both).the reason this mattered.
--self-test(query-options, which CI runs ahead of the gate) proves the guard inboth directions over real ESLint output, and
checkGuardAdoption()asserts from sourcethat both gates still route through it.
pnpm check:slot-lookuphas no self-test hook,so that source assertion is the wired coverage of its call site.
Harm reproduction — both halves, both gates
An unparseable file placed in the population (
packages/spec/src/__ratchet_fatal_probe__.ts,never committed). Before — the two gates at the branch point, same tree, same file:
while
eslint packages/spec/src/__ratchet_fatal_probe__.ts --no-inline-configsaid:After — same file, this branch at
72038565f9, both gates:Counts unchanged — the issue's own acceptance criterion
526212438872038565f987e46090c5Ablations — the new self-test can fail
Both legs are plain
node scripts/…sources (no packageexports, nodist), so norebuild is involved; each was restored by checking the file out of this branch again
(
git checkoutwith the branch ref and the path), and the restored tree verifiedbyte-identical (
git diff --stat HEADempty).eslint.lintFiles()→ the self-test goes red naming it:"calls
.lintFiles(directly, so a parse failure in its population is discarded as amessage matching no rule" (exit 1).
m.fatalswapped for a never-matching predicate) →red on the detail assertions (exit 1). Worth recording: the file was still caught,
by the
fatalErrorCountcross-check, but with the position and parser text lost — thesecond limb is real and is what the assertions measure.
Gates run locally at
87e46090c5pnpm check:slot-lookup·pnpm check:query-options-erasure(self-test + gate) ·pnpm check:cross-package-test-inputs·pnpm check:ratchet-remedy-authority(98 scriptsswept, the new one classified with the 89 handing out no ratchet-expanding remedy, so the
control corpus needs no entry) ·
pnpm check:nul-bytes·pnpm check:pm-dispatch-gates(351 cases — it reads the slot-lookup gate's source for its population declaration) ·
eslintover the three changed files. All green. The union was re-derived after the finalcommit with
node scripts/pm/dispatch-gates.mjs(no paths), against merge base2cd5f8113: same three paths, same four gates.Deliberately not here
The stack headroom question the issue defers. #10124 landed while this branch was open
and is merged in here; it is comment-only in
eslint.config.mjsplus the rootlintscript, so it moved none of the counts above (re-measured at
87e46090c5, table). Thesetwo gates still run as plain
node scripts/…at V8's default stack and do not inheritthat headroom — the deferred question, untouched.
One datum for whoever picks it up, and it exists only because of this PR: both gates now
go green only if every file in the population parsed, so the green runs above are
positive evidence that
packages/spec/src/migrations/registry.ts— the file #10124 isabout — parses inside these gates' own wide
packages/**run at the default stack today.That matches #10124's measurement that the wide scopes succeed and only a narrow one
crashes. The flip side is the honest risk to weigh: where the old behaviour was a silent
skip, this guard makes that file's stack margin a red gate if it is ever crossed.
Root
scripts/only, soskip-changesetrather than a changeset.Generated by Claude Code