Skip to content

Commit a68aed7

Browse files
committed
fix(tooling): pin the re-measure heap ceiling at 6144 and give the job the memory
Ruling A1 (#14569, 2026-09-03). The 02:25Z ruling A — raise on a recorded measurement — is completed the only way the measurement allows: the pin and the process's actual old space move together. - .github/workflows/lint.yml: the `typecheck-debt` job's re-measure step now runs under `NODE_OPTIONS: --max-old-space-size=6144`, so the process running tsc really has the old space the pin describes. V8's default there is 4096 MB, measured on the runner. - CI_TSC_HEAP_CEILING_MB: 4096 -> 6144, with the runner measurement already written beside it kept as the evidence. - Two new `remeasureHeapCeiling` self-test rows pin both directions of the pairing: the runner as the workflow now starts it (6192 reported, caller cap tying the pin, chosen ceiling 6144 named as the CI pin), and the same runner WITHOUT the workflow line (its measured 4144 MB default, refused). The `stale` arm is untouched and still refuses any pin above the process's own limit — that refusal is what caught the bare constant raise, and it is what keeps the two halves inseparable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent cc3a72a commit a68aed7

2 files changed

Lines changed: 113 additions & 28 deletions

File tree

.github/workflows/lint.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4808,7 +4808,33 @@ jobs:
48084808
- name: Build the ledgered packages' dependencies
48094809
run: pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'
48104810

4811+
# ⚠️ `NODE_OPTIONS` here is HALF of the heap ceiling this gate runs tsc
4812+
# under, and the two halves ship together or not at all (#14569, ruled
4813+
# 2026-09-03). The other half is `CI_TSC_HEAP_CEILING_MB` in
4814+
# scripts/check-type-check-coverage.mjs, which is a DESCRIPTION of the old
4815+
# space the process running tsc really has -- and the gate chooses the
4816+
# MINIMUM of that pin and this process's actual limit. So without this
4817+
# line the pin buys nothing (V8's default old space on this runner is
4818+
# 4096 MB, measured) and the gate's `stale` arm refuses the run outright,
4819+
# before the first tsc, on every PR and on `main`. That refusal is the
4820+
# pairing's enforcement, and it is deliberate.
4821+
#
4822+
# Why 6144 and not more, measured on this runner rather than reasoned
4823+
# (run 33708954003, job 100504131338, 2026-09-03): the heaviest ledger
4824+
# program keeps 4,077,718K live under a 4096 cap and 4,420,706K under
4825+
# 6144 while finishing ~5s sooner -- i.e. under 4096 it was paying GC
4826+
# pressure to fit -- and 10,562,192 kB of the box's 16,373,452 kB stayed
4827+
# available at the tightest moment of either run. The raise is headroom
4828+
# the box really has, not a promise it cannot keep; a ceiling above the
4829+
# box's real memory buys nothing and converts a recoverable heap error
4830+
# into an exit-137 SIGKILL with no diagnostic.
4831+
#
4832+
# ⛔ Do not change this number without changing the constant, or the
4833+
# reverse. The constant's comment carries the full reading and both
4834+
# directions are pinned in that file's `--self-test`.
48114835
- name: Re-measure the type-check DEBT / TEST_DEBT ledger
4836+
env:
4837+
NODE_OPTIONS: --max-old-space-size=6144
48124838
run: pnpm check:type-check-debt
48134839

48144840
# Lane 4 of 4 behind the required `TypeScript Type Check` context. The

scripts/check-type-check-coverage.mjs

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,7 +2792,7 @@ function countTscErrors(output, { dropRootDirDiagnostics = false } = {}) {
27922792
// same tree. ⚠️ The asymmetry IS the defect: a local pass was never a claim
27932793
// about CI, and nothing said so out loud.
27942794
//
2795-
// ## Where the number comes from -- CI, never this box
2795+
// ## Where the runner's DEFAULT old space comes from -- CI, never this box
27962796
//
27972797
// Read off the CI runner itself: run 33136681083, job `Type Check · debt
27982798
// ledger`, at 6d097a604, Node v22.23.2. The `packages/qa/http-conformance`
@@ -2851,36 +2851,66 @@ function countTscErrors(output, { dropRootDirDiagnostics = false } = {}) {
28512851
//
28522852
// The scarce resource is therefore NOT the runner's memory -- 15.6 GiB with
28532853
// ~918 MB of it spoken for -- but V8's DEFAULT old space on that runner, which
2854-
// is 4096 MB. This constant describes that default, and as of 2026-09-03 it
2855-
// still describes it exactly. That is why the re-measure leaves it here.
2854+
// the reading above pins at 4096 MB from two directions.
28562855
//
2857-
// ⚠️ If 4096 is wrong, it is wrong DOWNWARD -- the only safe direction. This
2858-
// number's entire job is to be no HIGHER than CI's ceiling. A pin ABOVE CI's is
2859-
// worse than no pin at all: it makes local runs pass where CI still OOMs, which
2860-
// is exactly this defect with extra confidence attached. The 2026-09-03
2861-
// reading above is the first taken with the runner in hand rather than
2862-
// inferred from a crash, and it lands on the same 4096 from the other side.
2856+
// ## The raise, on that measurement (#14569, ruled A then A1, 2026-09-03)
2857+
//
2858+
// A default is not a budget. The ledger's heaviest program was paying GC
2859+
// pressure to fit inside 4096 rather than fitting, and the tripwire (spec
2860+
// declaration growth) is a weekly event, so the ruling raises the ceiling --
2861+
// on the measurement above, never on a typed number. What that raise is NOT
2862+
// is a bigger promise about the box. It is the pair below, and ⛔ neither
2863+
// half is shippable alone:
2864+
//
2865+
// the workflow `.github/workflows/lint.yml`, job `typecheck-debt`, step
2866+
// "Re-measure the type-check DEBT / TEST_DEBT ledger", now
2867+
// runs under `NODE_OPTIONS: --max-old-space-size=6144`. That
2868+
// is the half that actually hands the process the old space:
2869+
// V8's default there is 4096 and no constant in this file can
2870+
// move it.
2871+
// this constant 6144 -- a description of the old space that step now
2872+
// really has, exactly as 4096 described the default before it.
2873+
//
2874+
// ⛔ Raising this constant ALONE cannot buy the ledger a roomier run --
2875+
// measured on 2026-09-03, not reasoned. `remeasureHeapCeiling` below takes the
2876+
// MINIMUM of this pin and the limit the running process actually has, so with
2877+
// the pin at 6144 and the gate started under the runner's DEFAULT the chosen
2878+
// ceiling is still 4144 -- and the `stale` arm below then refuses the run
2879+
// outright: `--re-measure` exits 1 before the first tsc ("the pin is now ABOVE
2880+
// the ceiling it claims to describe"), on every PR and on `main`. That
2881+
// refusal is the pairing's enforcement -- it is what caught the bare raise
2882+
// when it was attempted -- and both directions are pinned as self-test rows
2883+
// below ("the runner as the workflow now starts it" and "the same runner
2884+
// WITHOUT it"). Delete the `NODE_OPTIONS` line and the lane says so, loudly,
2885+
// on the runner.
2886+
//
2887+
// ⚠️ If 6144 is wrong, it is wrong DOWNWARD -- the only safe direction. This
2888+
// number's entire job is to be no HIGHER than the ceiling the process running
2889+
// tsc on CI really has. A pin ABOVE it is worse than no pin at all: it makes
2890+
// local runs pass where CI still OOMs, which is exactly this defect with extra
2891+
// confidence attached.
28632892
//
28642893
// ⛔ Do not raise this to make a local measurement complete. `--re-measure`
28652894
// OOMing under this ceiling is the gate WORKING -- it is CI's failure,
28662895
// reproduced on your box before you push. What grew is the type graph, not the
28672896
// memory CI has. (`packages/spec/tsup.config.ts` carries the other half of this
28682897
// lesson from the build side: a ceiling above the box's real memory does not
28692898
// buy a bigger run, it converts a recoverable heap error into an exit-137
2870-
// SIGKILL that carries no diagnostic at all.)
2871-
//
2872-
// ⛔ And raising it ALONE cannot buy the ledger a roomier run -- measured on
2873-
// 2026-09-03, not reasoned. `remeasureHeapCeiling` below takes the MINIMUM of
2874-
// this pin and the limit the running process actually has, so with the pin at
2875-
// 6144 and the gate started under the runner's own default the chosen ceiling
2876-
// is still 4144 -- and the `stale` arm below then refuses the run outright:
2877-
// `--re-measure` exits 1 before the first tsc ("the pin is now ABOVE the
2878-
// ceiling it claims to describe"), reproduced against a 4144 MB process. A
2879-
// raise has to hand the gate PROCESS the memory first -- a `NODE_OPTIONS` on
2880-
// the job's re-measure step -- so the pin keeps describing what the process
2881-
// really has. That is a workflow decision, not one this constant can take on
2882-
// its own; #14569 carries it.
2883-
const CI_TSC_HEAP_CEILING_MB = 4096;
2899+
// SIGKILL that carries no diagnostic at all.) The 6144 is not an exception to
2900+
// that rule, it is an application of it: the runner was MEASURED to carry the
2901+
// heaviest program under a 6144 cap (4,420,706K used, 4,545,500 kB peak RSS,
2902+
// 10,562,192 kB still available at the tightest moment) before it was pinned.
2903+
//
2904+
// ⚠️ One protection the pair costs, recorded here so nobody rediscovers it as
2905+
// a surprise. With `NODE_OPTIONS` set explicitly on that step,
2906+
// `heap_size_limit` there reads 6192 whatever the runner's physical memory
2907+
// does -- so on THAT job the `stale` arm can no longer notice the runner
2908+
// shrinking; it now only notices a pin above a DEFAULTED process. The margin
2909+
// is what makes that acceptable: the pin asks for 6144 MB where the
2910+
// measurement found 10,562,192 kB available at the heaviest moment, ~1.7x. If
2911+
// that margin is ever in doubt the answer is a fresh runner measurement and a
2912+
// smaller number in BOTH places, ⛔ never a bigger one here.
2913+
const CI_TSC_HEAP_CEILING_MB = 6144;
28842914

28852915
/**
28862916
* The last `--max-old-space-size` in a `NODE_OPTIONS` string, in MB, or null.
@@ -5225,15 +5255,44 @@ function selfTest() {
52255255
expect: { mb: CI_TSC_HEAP_CEILING_MB, stale: false },
52265256
},
52275257
{
5228-
// `+ 48` is the RUNNER, not a construction: the `Type Check · debt
5229-
// ledger` job reports a `heap_size_limit` of 4144 MB for its 4096 MB old
5230-
// space (measured there 2026-09-03, #14569), so this row is the shape of
5231-
// the machine whose verdict the pin exists to describe -- and the row
5232-
// above it is every box that is roomier than that one.
5258+
// `+ 48` is the RUNNER's offset, not a construction: V8 reports the old
5259+
// space plus a fixed ~48 MB of other spaces, measured on the `Type Check
5260+
// · debt ledger` job itself (4144 for a 4096 old space, 2026-09-03,
5261+
// #14569). So this row is the machine whose limit EQUALS the pin with no
5262+
// caller flag in play -- and the row above it is every box roomier than
5263+
// that one.
52335264
label: 'on a box shaped like CI the ceiling is a no-op that still names itself',
52345265
where: { heapLimitMb: CI_TSC_HEAP_CEILING_MB + 48, onCi: true },
52355266
expect: { mb: CI_TSC_HEAP_CEILING_MB, stale: false },
52365267
},
5268+
{
5269+
// THE RUNNER AS THE WORKFLOW NOW STARTS IT (#14569). `lint.yml`'s
5270+
// re-measure step sets `NODE_OPTIONS: --max-old-space-size=6144`, so the
5271+
// gate process reports 6192 AND carries a caller cap EQUAL to the pin.
5272+
// Both candidates tie, the tie-break keeps the CI ceiling's name, and
5273+
// that name is what the job's log then prints. Pinned because an
5274+
// off-by-one in either direction here reads as a caller cap overriding
5275+
// the pin on the one machine whose verdict counts.
5276+
label: "the workflow's own NODE_OPTIONS ties the pin and is not read as a tighter caller cap",
5277+
where: {
5278+
heapLimitMb: CI_TSC_HEAP_CEILING_MB + 48,
5279+
nodeOptions: `--max-old-space-size=${CI_TSC_HEAP_CEILING_MB}`,
5280+
onCi: true,
5281+
},
5282+
expect: { mb: CI_TSC_HEAP_CEILING_MB, stale: false },
5283+
},
5284+
{
5285+
// THE OTHER HALF OF THE PAIR, and the row that keeps the two halves
5286+
// inseparable. 4144 is the runner's DEFAULT `heap_size_limit`, measured
5287+
// on that job 2026-09-03. Take the `NODE_OPTIONS` line back out of
5288+
// `lint.yml` and this is the reading the gate gets: refused outright,
5289+
// before the first tsc, on every PR and on `main`. A bare raise of the
5290+
// constant was attempted and this is what caught it, so the pin above
5291+
// cannot quietly outlive the workflow line that pays for it.
5292+
label: 'the same runner WITHOUT the workflow NODE_OPTIONS -- its 4144 MB default -- is refused',
5293+
where: { heapLimitMb: 4144, onCi: true },
5294+
expect: { mb: 4144, stale: true },
5295+
},
52375296
{
52385297
// Never RAISE. Promising V8 memory the box does not have trades a
52395298
// recoverable heap error for a kernel SIGKILL that says nothing.

0 commit comments

Comments
 (0)