Filed unassigned from the #9863 dev seat (session session_01DdCnBGcHeufjrq7drTD3wt, PR #10450). Found while running the derived gate union for a card that touches no packages/spec file at all.
The symptom
Both ESLint-backed ratchet gates fail, on a branch that does not touch the named file, with:
✗ check-slot-lookup-ratchet: 1 parse failure(s) inside the population this gate measures:
• packages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded
ESLint returns a parse failure as a message with no rule id, so it matches no
rule this gate counts. A file that does not parse was never read for sites at
all: counting it as zero would report it clean without measuring it, which is
the one thing this gate exists to prevent. Nothing was counted this run.
check:query-options-erasure produces the identical message from its own ratchet. Exit code 2 in both cases.
The gates are behaving correctly. Refusing to report a number they did not measure is exactly right — this is not a request to soften them. The defect is that the parse fails at all, and non-deterministically.
Measured
Same worktree, same commit, same file, repeated runs:
| gate | runs | failures |
|---|
check:slot-lookup | 11 | 4 |
check:query-options-erasure | 5 | 1 |
Roughly 25–35%. One batch of six consecutive check:slot-lookup runs went 0, 2, 0, 2, 0, 0.
It is not memory pressure, which was the first hypothesis and is wrong. At the moment of a failure:
total used free shared buff/cache available
Mem: 16075 1011 11529 4 3868 15063
19:52:00 up 6:33, 0 user, load average: 1.53, 1.32, 1.71
11.5 GB free, load 1.5. It also reproduced both with and without NODE_OPTIONS=--max-old-space-size=4096, so the heap flag is not the trigger either.
packages/spec/src/migrations/registry.ts is 6861 lines. A non-deterministic "Maximum call stack size exceeded" on one large file is the signature of a recursive descent sitting just under the stack limit and tipping over depending on JIT/inlining state — i.e. the file is at the edge, not over it, which is why the same input parses on most runs.
⚠️--stack-size cannot be used to test that directly: node rejects it inside NODE_OPTIONS (exit 9), so confirming the hypothesis needs the flag on the parser's own invocation.
Why it is worth fixing rather than retrying
Both gates sit in lint.yml, both are named by scripts/pm/dispatch-gates.mjs for a wide range of changed paths (packages/** reaches check:slot-lookup; any new test file reaches check:query-options-erasure), and the steps run sequentially inside the ESLint job — so the first failure aborts the rest. A ~25% per-run failure that names a file the author never touched costs a round every time, and it reads as a real defect in the author's branch until someone re-runs it.
Directions a fixer might take
Not ranked, and each needs measuring first:
- Raise the parser stack for the ratchet invocations (and
pnpm lint), which makes the current file parse deterministically but only moves the edge. - Split or shrink
registry.ts. At 6861 lines it is an outlier; if it is generated, the generator can emit several modules. - Have the ratchets retry a parse failure once before failing. Cheapest, and the weakest — it hides an edge that will return as the file grows.
Whoever picks it up should first confirm the stack-depth hypothesis by invoking the parser directly with a raised stack, since the three directions above diverge on the answer.
Filed unassigned from the #9863 dev seat (session
session_01DdCnBGcHeufjrq7drTD3wt, PR #10450). Found while running the derived gate union for a card that touches nopackages/specfile at all.The symptom
Both ESLint-backed ratchet gates fail, on a branch that does not touch the named file, with:
check:query-options-erasureproduces the identical message from its own ratchet. Exit code 2 in both cases.The gates are behaving correctly. Refusing to report a number they did not measure is exactly right — this is not a request to soften them. The defect is that the parse fails at all, and non-deterministically.
Measured
Same worktree, same commit, same file, repeated runs:
check:slot-lookupcheck:query-options-erasureRoughly 25–35%. One batch of six consecutive
check:slot-lookupruns went0, 2, 0, 2, 0, 0.It is not memory pressure, which was the first hypothesis and is wrong. At the moment of a failure:
11.5 GB free, load 1.5. It also reproduced both with and without
NODE_OPTIONS=--max-old-space-size=4096, so the heap flag is not the trigger either.packages/spec/src/migrations/registry.tsis 6861 lines. A non-deterministic "Maximum call stack size exceeded" on one large file is the signature of a recursive descent sitting just under the stack limit and tipping over depending on JIT/inlining state — i.e. the file is at the edge, not over it, which is why the same input parses on most runs.--stack-sizecannot be used to test that directly: node rejects it insideNODE_OPTIONS(exit 9), so confirming the hypothesis needs the flag on the parser's own invocation.Why it is worth fixing rather than retrying
Both gates sit in
lint.yml, both are named byscripts/pm/dispatch-gates.mjsfor a wide range of changed paths (packages/**reachescheck:slot-lookup; any new test file reachescheck:query-options-erasure), and the steps run sequentially inside the ESLint job — so the first failure aborts the rest. A ~25% per-run failure that names a file the author never touched costs a round every time, and it reads as a real defect in the author's branch until someone re-runs it.Directions a fixer might take
Not ranked, and each needs measuring first:
pnpm lint), which makes the current file parse deterministically but only moves the edge.registry.ts. At 6861 lines it is an outlier; if it is generated, the generator can emit several modules.Whoever picks it up should first confirm the stack-depth hypothesis by invoking the parser directly with a raised stack, since the three directions above diverge on the answer.