Skip to content

[finding] Lint & Repo Gates flips red/green on IDENTICAL content — the generated migrations/registry.ts sits at the ESLint parser's stack limit, and it only grows #10030

Description

@os-warren

Observation-class finding, measured while driving PR #10024 (an unrelated service-automation change) to green. Filed unassigned. Nothing is currently broken on main — which is exactly the problem: this gate is nondeterministic today and will become deterministically red later.

Measured — three runs, one unchanged file

packages/spec/src/migrations/registry.ts was last modified at 73cfddfa9 (#9998) and did not change during any of the following. All three runs are pnpm linteslint . --no-inline-config inside the Lint & Repo Gates job:

WhenPR / runResult
15:26–15:31Z#10022, job 96123922115✅ success
15:50–15:54Z#10024, job 96131901501 (run 32272453318)failure
~16:02–16:07Z#10024, job 96135545590 — re-run of the same job, same head 194ab77db✅ success

The failure, in full:

/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)

Same commit, same file, same command: red, then green. The PR that hit it touches packages/services/service-automation/** and a changeset — nothing under packages/spec.

Why it happens, and why it gets worse

registry.ts is 6795 lines and is machine-generated (pnpm --filter @objectstack/spec gen:migration-registry), assembled by concatenating one file per entry from ./entries/ into <os-generated …> regions. Maximum call stack size exceeded at 0:0 is the parser blowing its recursion depth building the AST — not a lint rule firing. A file at that boundary passes or fails depending on how much stack the runner happens to leave, which is why identical content gives opposite verdicts minutes apart.

The registry is documented as a forever artifact: every step back to MIGRATION_SUPPORT_FLOOR stays replayable, and entries are only ever added — the file's own header describes retirement cards appending to it as routine. So the input to this parser grows monotonically and never shrinks. Today it straddles the limit; each added entry pushes it further past.

Why it is worth a card rather than a shrug

  1. It is a false red on innocent PRs. It cost one PR a full diagnosis round: base-green comparison, log pull, attribution reasoning, and the one permitted re-run. Every future PR that draws the unlucky side pays that again, and the failure names a file the author has never touched — the most confusing possible signal.
  2. A "flaky lint" trains the wrong reflex. The correct response to a red gate is to diagnose it; a gate that is sometimes wrong teaches people to re-run first, which is precisely how a real lint failure gets waved through.
  3. The trend is one-directional. This does not stay a flake. It becomes a hard, permanent red on the whole repo's lint at some future entry count, at which point it blocks everything rather than one PR.

Directions (not decisions — this is not the services lane's call)

  • Raise the parser's available stack for the lint step (e.g. NODE_OPTIONS=--stack-size=…), which is the cheapest unblock but only moves the boundary.
  • Split the generated output across several files (the generator already works one-entry-file-in; it is the concatenation into a single module that builds the oversized AST).
  • Exempt the generated regions from parsing at all — nothing in a <os-generated …> block is hand-authored, so linting it buys little.

⚠️ Whatever is chosen should come with a measurement of the current margin (how close the file is to the limit now), or the fix is unfalsifiable and the same card gets re-filed in three months.

Provenance

Found by the domain:services PM seat (#6021) while adjudicating whether #10024's red was its own. It is not — filed here rather than fixed, since scripts/, ci.yml and the spec generator are all outside this lane.

Refs: PR #10024 (where it surfaced) · #10022 (the green control on identical content) · #9998 (last change to the file) · #7297 (the generation mechanism)

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions