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 Gates → pnpm 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.ts | 73cfddfa9, 2026-08-19 14:05 (#9998) |
main tip | 1800ffac2, 2026-08-20 02:56 — unchanged across both runs below |
PR #10093, Lint & Repo Gates at 04:12→04:18 | ✅ success |
PR #10060, Lint & Repo Gates at 04:57→04:58 | ❌ failure, 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).
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 Gates→pnpm lint(eslint . --no-inline-config), on PR #10060, run32333753207, job96319189294, 2026-08-20T04:58:36Z: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:
packages/spec/src/migrations/registry.ts73cfddfa9, 2026-08-19 14:05 (#9998)maintip1800ffac2, 2026-08-20 02:56 — unchanged across both runs belowLint & Repo Gatesat 04:12→04:18Lint & Repo Gatesat 04:57→04:58Same
main, sameregistry.ts, forty minutes apart, opposite outcomes. And PR #10060's diff does not touch that file — or anything inpackages/spec— at all.⇒
Maximum call stack size exceededis 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.tsis 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.
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/*.tsalready uses), flattening the offending construct, or something else.migrations/entries/semantic/17.filter-regex-options-retired.tsand siblings), whileregistry.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
32333753207job96319189294(the failure) · PR #10093 run32331045331job96311573898(the same gate green on the samemain, 40 minutes earlier).