Skip to content

Commit f0b2db3

Browse files
claude[bot]claude
andauthored
fix(scripts): let the type-check debt gate declare the heap its tsc needs (#18192)
Fixes #17708 Clause-②: no No `packages/spec` schema, no declared contract and no authorable key moves here. The diff changes which heap ceiling ONE root gate hands the tsc children it spawns, and the reading it produces is identical to the one CI already takes. ## The defect, re-derived rather than recalled `pnpm check:type-check-debt` runs `scripts/check-type-check-coverage.mjs --re-measure` (the filename does not match the task name). Its ceiling was the MINIMUM of three numbers: the CI-shaped pin `CI_TSC_HEAP_CEILING_MB = 6144`, this process's own `heap_size_limit`, and an explicit `NODE_OPTIONS` cap the caller had set. The last two describe the process running the gate — which walks a manifest and allocates almost nothing — and not the tsc children, whose heaviest program held 4,420,706K live on the CI runner. So a lane whose resource discipline prefixes heavy commands with `--max-old-space-size=4096` handed 4096 straight through, and the gate refused, correctly, on every run it has ever made in that lane. ⭐ The two numbers are the SAME number, which is why removing only the caller arm would have fixed nothing: V8 reports the old space plus ~48 MB of other spaces, so a caller capping this process at 4096 also makes its `heap_size_limit` read 4144 — and 4144 OOMs exactly as 4096 does. ## Before / after, at the ceiling the callers impose Both runs on this container (MemTotal 16,461,028 kB, no swap, no cgroup memory limit), same tree, same warm turbo closure, via `scripts/pm/os-verify-lock.sh`: | run | before (ba9b498) | after (e504ea9) | | --- | --- | --- | | `NODE_OPTIONS=--max-old-space-size=4096 pnpm check:type-check-debt` | **exit 3** — `FATAL ERROR: Ineffective mark-compacts near heap limit`, 103s burned | **exit 0** — 5 ledger entr(ies) re-measured in 72.6s, 55 raw tsc error(s) total, none above its recorded number | | `pnpm check:type-check-debt` (no caller cap) | exit 0 — 5 entr(ies), 55 raw tsc error(s), 73.8s | unchanged | The **control that makes the pair discriminate** is the second row: the card's own "exit 0 at 8192" reading was never a run at 8192. The roomier cap was already refused by the pin, so that run measured at 6144 — the same ceiling the repaired run now uses under a 4096 caller cap. Before and after therefore produce the SAME ledger reading (5 entries, 55 errors); what changed is only whether the lane can obtain it. The gate's own heap line says so now: ``` heap: tsc runs under --max-old-space-size=6144 MB -- the CI-shaped ceiling pinned by scripts/check-type-check-coverage.mjs; this process's own limit is 4144 MB and this box can bear 15051 MB. ... Your NODE_OPTIONS caps THIS process at 4096 MB; tsc is handed 6144 MB anyway, because that cap describes this process and not the ~4.4 GB program the ledger measures (#17708). ``` ## What the repair is — and why it measures nothing less The minimum is now over TWO numbers: 1. `CI_TSC_HEAP_CEILING_MB` — unchanged at 6144, still the top of the minimum, still refusing every roomier caller cap. ⛔ A local run is still never roomier than CI. 2. What this BOX can bear — `os.totalmem()` and the cgroup limit (v1 and v2, both "no limit" sentinels handled), less a 1 GB reserve. That is where the "⛔ never promise V8 memory the machine does not have / exit 137 carries no diagnostic" lesson actually lives, and unlike `heap_size_limit` no caller overwrites it by exporting an environment variable. Inside a container `totalmem()` reports the HOST, so the cgroup read is the half that sees the threshold the kernel kills at. The caller's `NODE_OPTIONS` cap is now **reported and never obeyed, in either direction**. The argument that this is not a weakening, stated as the file now states it: a tighter-than-CI ceiling **cannot change a number** — the ledger counts tsc diagnostics, and heap does not change what tsc reports, only whether tsc lives to report it — so it **can only ever refuse**. It had no upside to weigh against the exit 3 it causes. Every ledger entry is still counted, `--re-measure`'s OOM at the real 6144 (the type graph outgrowing the memory CI has) still lands, and nothing was narrowed, dropped or converted into an exit 3 to make it fit. ⚠️ One protection genuinely changed shape, recorded rather than hidden: the chosen ceiling no longer collapses to a shrunken CI runner's own 4144 as a second line of defence. The `stale` arm — CI-only, measured on the runner, `exit 1` before the first tsc — is untouched and remains the enforcement that keeps `CI_TSC_HEAP_CEILING_MB` paired with the `lint.yml` step that pays for it. That arm was always the load-bearing half; the collapsed ceiling was a reading of this process that any environment variable could set. `.github/workflows/lint.yml` is **not touched** — it is read-only for this seat, and the repair did not need it. ## Tests — self-test batteries, with ablations `re-measure cases` battery: 45 → 56 registered, floor raised to match, printed tally corrected to include the two new batteries (it would otherwise have under-reported like the documented `observation cases` line does). New/changed rows, each handed a ROOMY box beside a TIGHT `heapLimitMb` so the two readings can finally be told apart: - `a caller's TIGHTER NODE_OPTIONS cap does not reach tsc, and neither does its heap_size_limit` — the #17708 row. - `the same runner WITHOUT the workflow NODE_OPTIONS -- its 4144 MB default -- is refused` — still refused, now by `stale` alone. - 5 rows for `boxHeapCeilingMb` (host total, cgroup below host, host below cgroup, box smaller than the reserve, absent limit). - 5 rows for `cgroupMemoryLimitMb` (v2 `max`, v2's 8 ZiB sentinel as this very container writes it, a real v2 limit, v1 fallback, no cgroup files at all). **Ablation, both legs restored and verified** (mutation proved on disk by marker count, restore proved by `git hash-object` against the `HEAD` blob plus an empty `git diff HEAD`, under an `EXIT INT TERM` trap): | mutation | self-test | | --- | --- | | re-add the caller's tighter cap as a candidate | exit 1 — `remeasureHeapCeiling — a caller's TIGHTER NODE_OPTIONS cap does not reach tsc...: expected 6144 MB ... got 4096 MB` | | drop the cgroup "no limit" sentinel guard | exit 1 — `cgroupMemoryLimitMb — cgroup v2's 8 ZiB sentinel is no limit either...: expected null, got 8796093022207` | Clean self-test: `55 semantic + 97 observation + 56 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code case(s) hold.` ## changeset `skip-changeset`: the only changed file is `scripts/check-type-check-coverage.mjs` at the repo root. The root package is private, and no published package's `files[]` can name a repo-root path (those arrays are package-relative). Nothing on the publish surface moves. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 14f274c commit f0b2db3

1 file changed

Lines changed: 349 additions & 52 deletions

File tree

0 commit comments

Comments
 (0)