Skip to content

check:slot-lookup and check:query-options-erasure fail ~1 run in 4 with a phantom parse error on packages/spec/src/migrations/registry.ts — non-deterministic, not memory pressure #10451

Description

@os-zhuang

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:

gaterunsfailures
check:slot-lookup114
check:query-options-erasure51

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions