Uh oh!
There was an error while loading. Please reload this page.
fix(spec): re-spell step17.rationale as a joined fragment array, collapsing registry.ts AST depth 977 to 76 - #10446
Conversation
…apsing AST depth 977 to 76
`packages/spec/src/migrations/registry.ts` held `step17.rationale` as a single
left-nested 971-operand `+` chain, giving the file a max AST depth of 977 —
about 13x the repo's runner-up (71). @typescript-eslint/parser recurses over
the AST, so what it costs is depth, not size: the file needed a minimum
--stack-size above V8's 984 KB default to parse at all, and cliffed with
`0:0 error Parsing error: Maximum call stack size exceeded` depending on which
other files shared the eslint invocation.
Re-spelled as an array of the same string literals joined with .join(''). The
971 fragments are carried over byte-for-byte (same quoting, same escapes);
the resulting string value is asserted identical — 79328 chars, sha256
2099de977992696945f00d772042b27c3be79135d2a4d44109433973f979de1f — before
and after. step17.rationale's own subtree depth goes 971 -> 4.
Nothing outside the initializer's byte range moved: the generated
<os-generated> regions, other steps' rationales, conversionIds and the two
retirement tables are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 116 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 95d8adee5003c8b3faaf7588aeacc0a9f474214e && git checkout 95d8adee5003c8b3faaf7588aeacc0a9f474214e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 91f303cb4bfa47bc3526633f5275d0aafc5aad24 192d3c58e71f5fcfdb90a22cecb2dafba4893d3e && git checkout -B drift-repro 91f303cb4bfa47bc3526633f5275d0aafc5aad24 && git merge --no-ff 192d3c58e71f5fcfdb90a22cecb2dafba4893d3e
node scripts/docs-audit/affected-docs.mjs --json 91f303cb4bfa47bc3526633f5275d0aafc5aad24
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10122
step17.rationaleinpackages/spec/src/migrations/registry.tswas a singleleft-nested
+chain of 971 string literals.@typescript-eslint/parserbuildsits ESTree AST by recursion, so what it costs is depth, not size — that one
expression gave the whole file a max AST depth of 977, roughly 13x the repo's
runner-up, and put it over V8's default main-thread stack.
Re-spelled as an array of the same literals joined with
.join('').Before / after, measured on this branch
origin/main@6439f8b42)192d3c58e)registry.tsmax AST depthstep17.rationaleinitializer subtree depthstep17.rationaleinitializer kindBinaryExpressionCallExpression+chain in the filestep18.rationale, untouched)--stack-sizefor eslint to parse the fileDepth convention:
SourceFile= depth 1, walked withts.forEachChild. The cardmeasured 976 / 970 operands at
1800ffac2; the file has since grown by onefragment, and the numbers above are re-measured on current
main.--stack-sizemethod: binary search overnode --stack-size=N node_modules/eslint/bin/eslint.js PATH --no-inline-config,1 KB resolution, cliff detected by the literal
Maximum call stack size exceededstring. Node v22.22.2. The 1099 KB "before" figure sits close to the 1085 KB
recorded in
eslint.config.mjs— the difference is the one fragment appendedsince, at the ~1.10 KB/fragment cost that comment measured.
After the change the file needs 9.5x less stack than V8 gives it by default,
where before it was ~12% over budget and could only parse on borrowed headroom.
The acceptance criterion: the string value is unchanged
The 971 fragments are carried over byte-for-byte — same quoting (941 single,
30 double), same escapes, one fragment per line. Only the
+separators became,, plus the[/].join('')wrapper.Proven three ways, all mechanical:
===plus sha256 —79328chars,2099de977992696945f00d772042b27c3be79135d2a4d44109433973f979de1f, identical.re-extracted from
origin/main's file and from this branch's file via theTypeScript AST and re-evaluated: same length, same sha256,
strict === : true.import('packages/spec/dist/index.mjs')then
MIGRATIONS_BY_MAJOR[17].rationale→79328chars, sha2562099de97…de1f. The value a consumer actually receives is the same string.A gap check also ran before the rewrite: every one of the 970 gaps between
consecutive operands was asserted to be whitespace plus a single
+, so no commentor non-literal operand could be silently dropped. All 971 operands are plain
StringLiteral(0 other kinds).Nothing else moved
The rewrite replaced exactly the byte range
[19762, 108025)— the initializer —and the surrounding bytes were asserted unchanged on both sides.
git diffis asingle hunk,
@@ -397,971 +397,973 @@.os-generatedmarker regions are untouched.step17's markers are at L1432 andL4926; the chain ended at L1367, entirely outside them.
conversionIds,RETIRED_KEYS_BY_MAJORandRETIRED_DEFS_BY_MAJORare byte-identical.pnpm check:merge-driveris green.
A template literal was considered and rejected: the content contains backticks
(
`execute`,`target`…) and${-hostile text, so it would have requiredescaping inside every fragment — which is exactly the byte-level churn this change
is trying not to introduce. The
.join('')spelling keeps each literal verbatim.Gates
Union re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs(no paths — it reads the change set from the merge base itself) and run at
192d3c58e, the final commit.The two that this card is about, both green:
pnpm lintis the whole-repo run — the one that was returningregistry.ts 0:0 error Parsing error: Maximum call stack size exceeded.Also green at
192d3c58e:check:merge-driver,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:spec-parsed-alias,check:type-source-resolution,check-adr-0087-registration,check-cross-package-test-inputs,check-affected-docs, and the spec-livenessfamily (
check:empty-state,check:liveness,check:strictness-ledger,check:variant-docs), pluspnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/spec test.check:doc-formula-expressionsred on first pass withERR_MODULE_NOT_FOUND … @objectstack/formula/dist/index.mjs— a missing buildartifact in a fresh worktree, not this diff. After
pnpm --filter '@objectstack/formula...' buildit is green:One declared narrowing:
node scripts/check-dev-prereqs.mjswas not broughtgreen locally. Its own verdict names the cause and it is not this diff —
✗ The workspace is not built — 1 unmet precondition, not a list of problems. / 66 of 67 workspace packages declare an entry point under dist/ that is not on disk. Clearing it needs a fullpnpm buildof the workspace, which would holdthe shared verify lock for a long time on a container running several agents; CI
builds the workspace and runs this gate anyway.
No changeset — deliberate, and here is what
distactually containsregistry.tsdoes ship in the published@objectstack/spec, and its emittedbytes do change:
dist/index.js/dist/index.mjsare not minified, so theycarry the source spelling verbatim —
rationale: [ … ].join('')where theypreviously carried the
+chain.What does not change is anything a consumer can observe: the exported string is
the same 79328 characters with the same sha256 (verified out of the built artifact,
above), no export was added or removed, and no type changed. Per AGENTS.md — "Add a
changeset for feature work … Pure bug fixes do not require a changeset" — this is a
repo-tooling fix with no functional delta, so it carries the
skip-changesetlabelinstead of declaring a release of its own.
For the closers of the duplicate reports
#10071 and #10121 report the same signature and were already triaged as
duplicates of the card this PR addresses; they remain open or closed at their
own triagers' discretion — nothing here changes their state. The evidence
recorded on the card at 18:45Z today — PR #10396
dequeued from the merge queue with
reason: CI_FAILUREwhile its own head wasgreen (24 checks, 0 failing,
mergeable_state: clean), the queue build failing onpackages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceededfor a file not in that PR's diff — is the collateral-eviction modethis removes. #10030 raised the parser's headroom to
--stack-size=4000; thislowers the floor instead, from 1099 KB to 104 KB.
Two things deliberately left alone
eslint.config.mjsstill says--stack-size=4000, and its comment block(L31–91) still describes the pre-fix state. Whether that headroom should now be
lowered or kept as margin is a real decision with a measurement attached, not a
mechanical follow-on, and this PR's file surface is one file by claim. Filed
separately as [finding] Once the registry.ts depth fix lands,
eslint.config.mjs's--stack-size=4000and its 60-line rationale describe a state that no longer exists #10448 (unassigned), which lays out both dispositions.step18.rationaleis a 70-operand chain (file depth 76). That is the sameauthoring habit at harmless scale — the repo's runner-up across all 4659 linted
files was already 71 — and the card explicitly scopes the smaller same-shape
chains out.
Generated by Claude Code