Skip to content

[finding] eslint . intermittently dies with "Maximum call stack size exceeded" parsing migrations/registry.ts — a monotonically-growing file now at the parser's stack cliff #10121

Description

@os-elon

Observation-class finding, filed unassigned. No shipped behaviour is affected — this is CI reliability. It blocks Lint & Repo Gates, which every PR must pass, so it costs an arbitrary PR an arbitrary round at an arbitrary time.

The failure

Lint & Repo Gatespnpm lint (eslint . --no-inline-config), on PR #10060, run 32333753207, job 96319189294, 2026-08-20T04:58:36Z:

/home/runner/work/objectstack/objectstack/packages/spec/src/migrations/registry.ts
0:0 error Parsing error: Maximum call stack size exceeded
✖ 1 problem (1 error, 0 warnings)

One problem, zero warnings, and the job dies 85 seconds in.

Why it is non-deterministic rather than a content error — measured, not assumed

The obvious reading is "something landed that broke the file". That is falsified:

last commit touching packages/spec/src/migrations/registry.ts73cfddfa9, 2026-08-19 14:05 (#9998)
main tip1800ffac2, 2026-08-20 02:56 — unchanged across both runs below
PR #10093, Lint & Repo Gates at 04:12→04:18success
PR #10060, Lint & Repo Gates at 04:57→04:58failure, the parse error above

Same main, same registry.ts, forty minutes apart, opposite outcomes. And PR #10060's diff does not touch that file — or anything in packages/spec — at all.

Maximum call stack size exceeded is a resource-limit failure, not a syntax verdict. It depends on the stack depth actually available to the parser, which varies with runner memory pressure and job concurrency. The file sits close enough to the limit that the same input parses on one runner and overflows on another.

The structural half — this gets worse on a schedule, by design

registry.ts is currently 6,795 lines / 528 KB, and it is append-only by process: every ADR-0087 retirement adds an entry. The last eight commits touching it are all retirements or prescription edits. Nothing in the workflow ever removes an entry — that is the point of a retirement registry.

So the margin to the cliff shrinks monotonically, and the symptom is intermittent, which is the worst combination: it will be blamed on whichever PR happens to be running when the runner is loaded.

⚠️ This is the same structural shape the repo already legislated against for release notes — "every code PR appending its own row turns that file into the single hottest merge-conflict magnet in the repo" — one monotonically-growing shared file, hit by every contributor. There the cost is conflicts; here it is a tool limit. The parallel is worth noticing when choosing a fix.

What this card should establish

Do not "fix" this by raising Node's stack size in CI. That buys back a few months of appends and hides the next occurrence, and the failure would then reappear on contributors' machines instead of in CI, where nobody is watching for it.

The question to answer first, before choosing a remedy, is which construct in that file the parser recurses on — a 528 KB file is not inherently fatal to a parser, so the depth is coming from something specific (most likely one deeply-nested literal or a very long chain rather than the file's size as such). That is measurable: bisect the file, or run the parser directly with a raised stack and instrument the depth. Only then is it possible to choose sensibly between splitting the registry into per-entry modules (the pattern migrations/entries/semantic/*.ts already uses), flattening the offending construct, or something else.

⚠️ Note the entries directory already exists and already carries one file per retirement (migrations/entries/semantic/17.filter-regex-options-retired.ts and siblings), while registry.tsalso carries the same prose inline. If those are duplicative rather than complementary, the split may already be half-done and the remedy smaller than it looks. Verify that before designing anything.

Reproduction status — honest limits

Not reproduced on demand. Two observations of the same input with opposite outcomes is enough to establish non-determinism, and the file's size and growth pattern are facts. The exact threshold, the offending construct, and the failure rate are all unmeasured. A re-run of the failed job was attempted and refused (403 This workflow is already running), so not even the one-re-run confirmation is on the record yet.

Do not let this card assert more than that. The next reader should treat "which construct" as an open question, not as something this card answered.

Priority

Low-to-medium, and the reason it is not simply low: it costs someone else's PR a wasted round, at random, with a failure message that points at a file they did not touch. The direct cost is small; the misattribution cost is not.

Refs: PR #10060 run 32333753207 job 96319189294 (the failure) · PR #10093 run 32331045331 job 96311573898 (the same gate green on the same main, 40 minutes earlier).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions