You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(tooling): the qa/http-conformance TEST_DEBT re-measure already needs ~4.4 GB of tsc heap on main — it passes the 4096 MB ceiling only through GC pressure, so any spec declaration growth tips Type Check · debt ledger to OOM #14569
Found while driving PR #14513 (#14439) to green. Filed for the maintainer because the ceiling is a pinned policy (CI_TSC_HEAP_CEILING_MB = 4096, #12856) and changing what the program checks changes what the ledger measures.
Measured
scripts/check-type-check-coverage.mjs --re-measure builds one tsc --noEmit program per ledger entry under --max-old-space-size=4096. For the packages/qa/http-conformance TEST_DEBT entry (8 source files, 5 of them tests, but importing @objectstack/core, @objectstack/objectql, @objectstack/runtime, @objectstack/plugin-hono-server, @objectstack/driver-sqlite-wasm, @objectstack/spec), the same program shape run with an 8 GB cap and --extendedDiagnostics:
main @ 53d36892 (base worktree, closure built)
Files
906
Lines of Definitions
691,580
Types
1,801,110
Instantiations
7,342,314
Memory used (tsc's own count)
4,473,321 K
Check time
68.9 s
Under the 4096 MB ceiling the program still completes on main (merge-queue Lint & Type Check runs are green through 53d36892 and later), but only because V8 collects harder near the limit: a local run on a main-based worktree peaked at 3,961 MB RSS and spent minutes in GC before finishing. The headroom is therefore a few percent, not a margin.
PR #14513 showed what that means in practice: a spec change that added 42,622 lines of definitions to this program (the whole ObjectStackDefinitionSchema declaration, pulled in through a new chunk edge — fixed in the PR) moved the count to 4,875,249 K and the lane went red twice. The next spec PR that grows the declaration graph seen by @objectstack/spec/system will hit the same wall without having done anything wrong.
Options
Option
Cost
Note
A
Raise CI_TSC_HEAP_CEILING_MB to 6144 for the re-measure
Policy change; the file's own comment says 4096 is pinned to the CI runner's measured old-space and "if 4096 is wrong, it is wrong DOWNWARD" — ubuntu-latest has 7 GB, so 6144 needs a measurement of what else runs in that job
Restores headroom without changing what is measured
B
Shrink the program: skipLibCheck: true in the re-measure project only
Changes the measurement semantics of every TEST_DEBT number (declaration files stop being checked); the frozen numbers would need a wholesale re-measure
Large heap win (declaration files dominate: 691k lines of definitions vs 17k of TypeScript)
C
Shrink the program's inputs: have http-conformance type its tests against narrower entries (it imports whole packages for a handful of names)
Per-package edit; recurs for the next heavy entry
Local, no policy change
D
Leave as is; treat each OOM as the offending PR's to fix
Every spec PR pays a debugging cycle for a ceiling it did not set
Not decided here. The four-axis reading favours A with a measured number (long-term consistency: the ceiling exists to make numbers portable, and a ceiling with 3% headroom makes them fragile instead; scope: one constant, one measurement of the runner). B is the only option that changes ledger semantics and should not ride a spec PR.
{
"issue": 14569,
"status": "needs_decision",
"branch": "claude/issue-14569-tsc-heap-ceiling-6144",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14805",
"premise_still_valid": true,
"summary": "The ruling's first half is done and landed: the re-measure heap ceiling was measured first-hand on the `Type Check · debt ledger` runner (temporary probe commit emitting `::notice` annotations, run 33708954003 / job 100504131338, read back over the check-run annotations REST endpoint, then reverted in full), and the reading is now written into the comment beside CI_TSC_HEAP_CEILING_MB. The reading CONFIRMS the finding's premise (cap 4096 gives 4,077,718K used and a 26.84s check; cap 6144 gives 4,420,706K used and a 21.90s check: 343 MB more heap keeps 343 MB more live and finishes ~5s sooner, i.e. under 4096 the program pays GC pressure to fit) and corrects one fact in the card: `ubuntu-latest` here is 16,373,452 kB (~15.6 GiB), not 7 GB, with only ~918 MB held by the 153 other processes and nothing running beside the re-measure (the job's steps are sequential). The ruling's second half -- pin 6144 -- is NOT taken, because the measurement shows it cannot be delivered by the ruled file surface: the runner's own V8 default old space is 4096 MB (`heap_size_limit` 4144 MB, read directly), `remeasureHeapCeiling` minimises over the pin and the running process's own limit, so a 6144 pin still chooses 4144 -- and the `stale` arm then refuses outright, exiting 1 before the first tsc. Reproduced locally against a 4144 MB process. Shipping the bare raise would turn the debt-ledger lane red on every PR and on main, so the raise is escalated with three options instead of guessed. What lands: one file, 63 changed lines, all of them comments (mechanically counted, 0 non-comment).",
"tests": "At 24ee34f8d7 (final commit, after `git merge origin/main`). `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 19 families (17 by path + 2 by change KIND, gate-script edit). 17 green: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pm-dispatch-gates, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:type-check-coverage, check:watch-hint-literal, check-ci-filter-parity, check-cross-package-test-inputs, check-self-test-wired, check-shard-attestation, check-whole-set-label-write, pm/bare-root-worklist --self-test (exit codes captured with redirect-then-capture, never through a pipe). `node scripts/check-test-completeness.mjs` exits 3 = its own NOT MEASURED code (no local vitest summary), not a finding. NARROWING, DECLARED: `pnpm check:type-check-debt` was not run locally -- it needs a full closure build plus ~4 min of 34 sequential tsc programs on a shared container, and the diff is comment-only (63 of 63 changed lines are `//`). Substitutes: `--self-test` green (48 semantic + 68 observation + 43 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code cases, including all seven remeasureHeapCeiling rows), `check:type-check-coverage` green, and the debt lane itself ran GREEN in CI on this branch at d415f4fa40 (job 100504131338) on a tree carrying MORE than this one. The gate script has no vitest pin suite of its own (git grep over *.test.ts finds only comment mentions); its test surface is `--self-test`, which check-self-test-wired verifies. Reverse verification of the raise, run from a COMMITTED state: file mutated with perl -pi, mutation proven on disk by counting both spellings (4096-pin 1 then 0, 6144-pin 0 then 1) before any run; then `GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=4096 node scripts/check-type-check-coverage.mjs --re-measure` exited 1 with '::error:: ... pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB'; restored with `git checkout HEAD -- ABSOLUTE_PATH` and restoration proven by hash (02c3b4bca638f1e5efb487ff01229d90a9de250c equals the HEAD blob), `git diff HEAD` empty, `git status --porcelain` empty. Control-byte scan of the edited file: grep -naP over the control ranges, exit 1 (no matches), captured before any pipe. CI on 24ee34f8d7 at report time: guards green (Part-of Closing-Keyword, Duplicate Fix, Single-Claim Path, Governed Surface, Check Links, PR Automation); `Lint & Type Check` and `CI` in_progress.",
"mcp_calls": "0 -- REST only, with $GITHUB_TOKEN and --cacert; no MCP GitHub calls this run",
"open_questions": [
{
"question": "The ruled raise to 6144 cannot be delivered by the constant, its comment and the self-test rows alone: on the measured runner the gate process's own V8 default old space is 4096 MB (heap_size_limit 4144), remeasureHeapCeiling minimises over the pin and that limit, and the `stale` arm refuses any pin above it (exit 1 before the first tsc, reproduced). How should the raise be delivered?",
"options": [
"A1 -- give the gate PROCESS the memory: `NODE_OPTIONS: --max-old-space-size=6144` on the re-measure step of the typecheck-debt job in lint.yml, then pin 6144. One workflow line plus the constant. Every invariant survives: the pin still never exceeds what the process really has, and the existing `stale` arm keeps the two numbers honest, loudly, on the runner itself. Cost: the ceiling is stated in two places (which the stale arm already polices) and it is OUTSIDE the ruled file surface, which is why it was not taken here.",
"A2 -- change the cap rule: derive the second candidate from the box's REAL memory (MemTotal/MemAvailable) instead of this process's V8 default. Architecturally the more correct fix: that candidate exists to stop the gate promising memory the BOX does not have, and today it approximates that with V8's default, which is wrong on any box bigger than a 4 GB default heap -- measured here at 15.6 GiB with 10.1 GiB still free at the tightest moment of the heaviest program. Cost: it inverts the 'never RAISE' doctrine the file's header defends with two measured incidents, needs new adversarial self-test rows, and reopens the exit-137 SIGKILL failure mode on small containers unless the headroom maths is right.",
"A3 -- do not raise. The pin describes the runner exactly, as measured; take option C from the finding (narrow qa/http-conformance's imports) so the heaviest program shrinks instead. Cost: recurs for the next heavy ledger entry."
],
"recommendation": "A1. Long-term soundness (the axis carrying at least half the weight): the ceiling's whole job is to be a DESCRIPTION of what the process running tsc really has, and A1 is the only option that raises the number while keeping it a description -- the pin stays at or below the process's limit and the existing `stale` refusal keeps the two in step, on the runner, loudly. A2 is the better shape eventually but it is a policy inversion of a doctrine this file defends with measured incidents, so it deserves its own decision rather than riding this one. Real business need: measured and real -- the premise is confirmed (GC pressure, not comfort, at 4096) and #14513 already paid two red lanes for it. Making AI-written code hard to get wrong: A1 keeps the loud refusal that catches the two numbers drifting apart, while A3 leaves the next spec PR to discover the wall itself. Startup scope discipline: A1 is one workflow line plus one constant, no new surface. Whichever is chosen, the bare raise (constant only) must NOT ship: it reds the debt-ledger lane on every PR and on main."
}
],
"out_of_scope_findings": []
}
Maintainer ruling recorded — A1: give the gate process the memory, then pin 6144 (2026-09-03)
Provenance. Ruled by the maintainer (pm@objectstack.ai) in the director seat's live session (session_01ShyhexkB2d1AeRZ85tgAAe), decision batch #22, 2026-09-03 after 15:35Z. Verbatim reply: 「同意」. The batch was presented with this card's recommendation as A1 (fallback A3; A2 deserves its own decision and is not filed), so 「同意」 adopts A1. Ledger: objectstack#12708, batch #22 ledger comment.
What was ruled. The 02:25Z ruling A (comment 5519394826) stands — raise on a recorded measurement — and its delivery is completed the only way the measurement allows:
the re-measure step of the typecheck-debt job in .github/workflows/lint.yml sets NODE_OPTIONS: --max-old-space-size=6144, so the process running tsc really has the old space the pin describes;
the stale arm keeps refusing a pin above the process's own limit — that refusal is what caught the bare raise, and it stays.
⛔ The bare constant raise never ships alone (it reds the debt-ledger lane on every PR and on main). ⛔ A2 (derive the cap from box memory) is not taken here; it inverts the file's "never RAISE" doctrine and would need its own card. A3 not taken.
Execution.domain:devx lane; PR #14805 continues on its branch. .github/workflows/** is not a governed surface: review, then auto-merge. Clause-②: no. skip-changeset.
State.needs-user-decision → pm:queue in the same stroke; tooling, priority:p2, domain:devx kept; unassigned.
Claim: PM loop round R4 — continuation on the existing branch, not a fresh start
Session: session_012zGPuVVX3deAx9LdjK8jCk
Branch: claude/issue-14569-tsc-heap-ceiling-6144 (existing; PR #14805, head 24ee34f8d7, last touched 03:06Z — before the ruling)
Worktree: objectstack-issue-14569
Domain: domain:devx
File surface: scripts/check-type-check-coverage.mjs + .github/workflows/lint.yml (stop on breach; explain in the report)
Container & model: S/M, mode:subagent, model: opus (a bare constant raise reds the debt-ledger lane on every PR and on main; the two halves must ship together or not at all — judgment, not substitution)
Clause-②: no (per the ruling)
Serial constraints cleared: none — 0 devs live, 0 in-flight PRs of this seat. ⚠️scripts/check-type-check-coverage.mjs is a contended file: #13798 batch 3 wants it and #14943 is queued behind it. Neither is dispatched, and neither will be while this is in flight.
Decision re-read to the last page. Ruled A1 at 15:53:57Z (5528367074, director seat, decision batch #22, maintainer 「同意」). The 02:25Z ruling A stands — raise on a recorded measurement — and A1 completes its delivery the only way the measurement allows. ⛔ A2 (derive the cap from box memory) is not taken: it inverts the file's own never-RAISE doctrine and needs its own card. ⛔ A3 not taken.
⚠️PR #14805 already exists and carries only the measurement (1 file, +62/−1, all of it the comment block written in the earlier round). The ruling says it continues on its branch — so this is a continuation. ⛔ Do not open a second PR, ⛔ do not restart the branch.
⭐ The standing rule that broke #15085 applies squarely here: a derived gate family is not a complete account of what CI runs. This diff edits check-type-check-coverage.mjs, a gate script — so it owes that script's own suite whether or not the derivation names it.
Worktree:../objectstack-issue-14569, created off the existing branch head 24ee34f8d7; origin/main369da918d merged in as a real merge commit (no rebase, no force-push, no amend). Write-route probe: that merge commit pushed clean before any edit.
File surface:scripts/check-type-check-coverage.mjs + .github/workflows/lint.yml, exactly as ruling A1 names them. No third file.
Ruling followed: A1 (comment 5528367074, 15:53:57Z, director seat, decision batch chore(ci)(deps): bump actions/upload-artifact from 4 to 6 #22, maintainer 「同意」), which completes ruling A (5519394826) rather than replacing it. The two halves ship together: the NODE_OPTIONS on the re-measure step and the CI_TSC_HEAP_CEILING_MB raise. The stale arm is untouched.
Not taken: A2 (deriving the cap from box memory) and A3, per the ruling.
{
"issue": 14569,
"status": "done",
"branch": "claude/issue-14569-tsc-heap-ceiling-6144",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14805",
"premise_still_valid": true,
"summary": "Ruling A1 delivered in full on the existing branch and the existing PR #14805 (no second PR, no restart, no rebase, no force-push): origin/main 369da918d merged in as a real merge commit cc3a72ae6, then one implementation commit a68aed7ec. BOTH HALVES ARE IN THE DIFF. (1) .github/workflows/lint.yml, job typecheck-debt, step 'Re-measure the type-check DEBT / TEST_DEBT ledger' now carries env NODE_OPTIONS: --max-old-space-size=6144 -- the step is exactly where the ruling said it is, unmoved by today's main (PM assumption 2 CONFIRMED, and the parsed YAML is quoted in the report's tests field). (2) CI_TSC_HEAP_CEILING_MB 4096 -> 6144 in scripts/check-type-check-coverage.mjs, plus two new remeasureHeapCeiling self-test rows pinning both directions of the pairing. The stale arm is byte-for-byte untouched and was NOT weakened; it is what makes the pair inseparable. Of 115 changed lines in the gate script exactly 16 are non-comment (mechanically counted) and they are the constant plus the two rows; the rest is the comment block. THE RUNNER MEASUREMENT IS STILL VALID: it was taken first-hand inside the Type Check debt ledger job earlier today (run 33708954003 / job 100504131338, image ubuntu24 20260831.293.1, Node v22.23.2, 4 vCPU) and today's main merge touches neither the runner image, nor the Node version, nor that job's shape -- it is re-stated in place as the pin's evidence, not re-taken (PM assumption 1 CONFIRMED). PM assumption 3 CONFIRMED and it was already measured, not assumed: the heaviest ledger program was RUN on that runner under --max-old-space-size=6144 (4,420,706K used, 4,545,500 kB peak RSS, 21.90s, no OOM) against 4,077,718K / 26.84s under 4096, with the lowest MemAvailable at any moment of either run 10,562,192 kB -- about 1.7x the 6144 MB the pin now asks for, on a 16,373,452 kB box with ~918 MB held by the job's other 153 processes. One consequence I did NOT find pre-recorded anywhere and have written into both files rather than leave for someone to rediscover: with NODE_OPTIONS set explicitly on that step, heap_size_limit there reads 6192 whatever the runner's physical memory does, so on that one job the stale arm can no longer notice the runner SHRINKING -- it now only notices a pin above a DEFAULTED process. That is a real (small) loss of a tripwire, the ~1.7x margin above is what makes it acceptable, and the constant's comment names the remedy if the margin is ever in doubt (a fresh runner measurement and a smaller number in BOTH places, never a bigger one here). PR #14805 left as a draft with auto-merge NOT armed, per the dispatch.",
"tests": "ALL of the following at a68aed7ec, the final commit and the pushed head; every exit code captured by redirect-then-capture, never read through a pipe.\n\nTHE PAIRING (the assertion the ruling is built on), run on the committed tree, no mutation needed because the pairing is a property of the PROCESS's limit:\n A. GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=4096 node scripts/check-type-check-coverage.mjs --re-measure -> EXIT=1, '::error::scripts/check-type-check-coverage.mjs pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB -- the pin is now ABOVE the ceiling it claims to describe', refused BEFORE the first tsc. That is the bare-raise failure this card exists to avoid, reproduced.\n B. GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=6144 node scripts/check-type-check-coverage.mjs --re-measure -> stale arm SILENT, prints '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 6192 MB', then EXIT=3 = PREREQUISITE NOT MET on an unbuilt closure (this gate's own NOT-MEASURED code, not a pass and not a red) -- i.e. it got PAST the arm, which is the whole point. Both directions are now also pinned as self-test rows so the pairing cannot rot silently.\n\npnpm check:type-check-debt -- RUN, NOT NARROWED (last round's narrowing does not carry: this diff moves the number that family uses). Closure build first, through the shared verify lock: pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 -> 71 successful, 71 total, VERDICT command-exit 0, held the lock 440s. Then the gate, also through the lock: '✓ check:type-check-coverage --self-test — 48 semantic + 68 observation + 45 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code case(s) hold.' / 'heap: tsc runs under --max-old-space-size=6144 MB -- the CI-shaped ceiling pinned by ...; this process's own limit is 8240 MB.' / 'check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 138.9s, 217 raw tsc error(s) total, none above its recorded number.' DEBT_EXIT=0, VERDICT command-exit 0, held the lock 140s. The re-measure case count moved 43 -> 45, which is the two new rows.\n\nDERIVED FAMILY: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed -- it derives its own change set from the merge base 369da918d) reported 'gate list derived from the tree ... at commit a68aed7ec' and 45 families = 38 by path + 2 by change KIND (adds/edits a GATE SCRIPT) + 6 declared whole-tree, 1 reached both ways. Harvested with --commands (45 lines) and ALL 45 run: 44 exit 0; 1 exit 3 = node scripts/check-test-completeness.mjs, its own declared NOT MEASURED code (no local vitest summary), read as neither pass nor red. Green includes check:type-check-coverage, check:type-check-debt, check:pm-dispatch-gates, pm/bare-root-worklist --self-test, check-self-test-wired(+--self-test), check-self-test-workflow-commands(+--self-test), check-step-collectors(+--self-test), check-aggregator-roster(+--self-test), check-required-contexts, check-ci-filter-parity, check-closing-keyword-parity(+--self-test), check-comment-mask-corpus, check-shard-attestation, check-position-name-fold-loaders(+--self-test), check-whole-set-label-write(+--self-test), check-cross-package-test-inputs, docs-audit/check-drift-comment, pm/ci-failure --self-test, check:nul-bytes, check:refd-timer-probe, check:watch-hint-literal, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:declared-population-live, check:entry-guard, check:node-version, check:parse-guard, check:pnpm-acquisition, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:required-contexts, check:shard-attestation, check:stall-guard-budget, check:stall-guard-headroom, check:workflow-status-functions.\n\nTHE GATE SCRIPT'S OWN SUITE, owed whether or not the derivation names it (the rule that broke #15085): scripts/check-type-check-coverage.mjs is a gate script. Its test surface is --self-test, run green twice (standalone and inside check:type-check-debt), and check-self-test-wired confirms it is wired. git grep for the script's name across *.test.ts / *.test.mjs / *.test.js / *.spec.ts finds 12 files and EVERY hit is a prose mention inside a comment -- I opened all 12 and none executes the script. There is no vitest pin suite to have missed. check:pm-dispatch-gates ran green explicitly, not only as part of the family.\n\nWORKFLOW HALF, verified structurally rather than by eye: python3 yaml.safe_load of .github/workflows/lint.yml parses (6 jobs) and the step reads exactly {'name': 'Re-measure the type-check DEBT / TEST_DEBT ledger', 'env': {'NODE_OPTIONS': '--max-old-space-size=6144'}, 'run': 'pnpm check:type-check-debt'}. A step-level env: block is an established idiom in this repo (ci.yml carries two).\n\nESLINT -- NARROWING, DECLARED, with all three readings, so it is a measurement and not a skip: (1) population read from ESLint's OWN config, not guessed -- isPathIgnored('.github/workflows/lint.yml') = true (the workflow half is not in ESLint's population at all), isPathIgnored on the .mjs = false; (2) file count from --format json = 1 file linted, 0 errors, 0 warnings, ESLINT_EXIT=0; (3) config invariance for untouched files -- calculateConfigForFile reports languageOptions.parserOptions.project = undefined, i.e. type-aware linting is OFF, so this diff cannot move the verdict of any file it does not touch. The repo-wide `pnpm lint` is CI's run.\n\nCONTROL BYTES: grep -naP over [\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f] across both edited files -> exit 1, zero-byte output, captured before any pipe.\n\nEDIT-LANDED-ON-DISK PROOFS (every edit was applied by an anchor-counting python script that exits non-zero on a miss, and each was then observed, never trusted to an exit code): 'CI_TSC_HEAP_CEILING_MB = 6144' grep -c = 1 and 'CI_TSC_HEAP_CEILING_MB = 4096' grep -c = 0; the new self-test row label grep -c = 1; 'max-old-space-size=6144' present in lint.yml at line 4837. git status --porcelain is empty at the pushed head.\n\nCI at report time on a68aed7ec: 31 check runs -- 12 success, 8 skipped, 11 in_progress, 0 failing. Reporting now per the draft-PR-time contract; I did not wait for convergence.",
"mcp_calls": "2 -- issue_read get and issue_read get_comments on #14569. Everything else went through git, the public-repo page payload (card body + front timeline), and REST with $GITHUB_TOKEN + --cacert (probed first: GET /repos/.../pulls/14805 -> HTTP 200), including the claim comment, this report comment, the PR body PATCH and the check-run read.",
"open_questions": [],
"out_of_scope_findings": []
}
ACCEPT — PR #14805, ruling A1 delivered in full. ⭐ And it found a cost of the ruled change and wrote it into the code
domain:devx seat, session session_012zGPuVVX3deAx9LdjK8jCk, R4, 2026-09-03T23:5xZ. Checked against the diff, ⛔ not the report.
Both halves are in the diff — verified, and the ruling turns on them shipping together
half
reading on the branch
workflow
.github/workflows/lint.yml:4835–4837 — step "Re-measure the type-check DEBT / TEST_DEBT ledger" now carries env: NODE_OPTIONS: --max-old-space-size=6144
constant
CI_TSC_HEAP_CEILING_MB = 6144 present 1×, the old 4096 present 0×
the stale arm
⭐ untouched — I diffed the whole gate script against the merge base and filtered to non-comment lines: the only changes are the constant and two self-test rows. No arm logic moved
⭐ The two new rows pin the pairing in BOTH directions, so the coupling cannot rot silently:
"the workflow's own NODE_OPTIONS ties the pin and is not read as a tighter caller cap" → stale: false
"the same runner WITHOUT the workflow NODE_OPTIONS — its 4144 MB default — is refused" → stale: true
The evidence I asked for, delivered as a live pairing rather than a green run
NODE_OPTIONS=4096 ⇒ EXIT 1, refused before the first tsc: "pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB — the pin is now ABOVE the ceiling it claims to describe." ⇒ the bare-raise failure this card exists to prevent, reproduced.
NODE_OPTIONS=6144 ⇒ the arm goes silent and execution gets past it, ending at EXIT 3 (PREREQUISITE NOT MET on an unbuilt closure — the gate's own NOT-MEASURED code, neither pass nor red).
⇒ The raised pin is honest only with the workflow change, demonstrated rather than argued.
Premises, all three confirmed with measurements rather than assurances
The runner measurement is still valid — taken first-hand today inside the actual Type Check debt-ledger job (run 33708954003, image ubuntu24 20260831.293.1, Node v22.23.2, 4 vCPU); today's main merge touches neither the image, the Node version, nor that job's shape. Re-stated in place as evidence, ⛔ not silently re-taken.
The step is where the ruling said it is, unmoved by today's main — and confirmed structurally, by yaml.safe_load parsing the file and reading the step back, not by eye.
6144 is actually achievable — the heaviest ledger program was run on that runner under 6144 (4,420,706K used, 4,545,500 kB peak RSS, no OOM) against 4,077,718K under 4096, with the lowest MemAvailable at any moment ≈ 1.7× the 6144 the pin asks for.
⭐ The best thing in this delivery is a cost it found and refused to hide
With NODE_OPTIONS set explicitly on that step, heap_size_limit there reads 6192 regardless of the runner's physical memory — so on that one job the stale arm can no longer notice the runner shrinking. It now only notices a pin above a defaulted process.
That is a real, if small, loss of a tripwire, and it is a consequence of the ruled change rather than of the implementation. The dev found it, judged the ~1.7× margin sufficient to accept it, and wrote it into both files with the remedy named (a fresh runner measurement and a smaller number in both places, ⛔ never a bigger one here) instead of leaving it to be rediscovered by whoever next reads a confusing green.
The always-runs-tail rule was honoured — its first test
This diff edits a gate script, so per the rule verified on #15085 it owes that script's own suite whether or not the derivation names it. Done: --self-test green twice (standalone and inside check:type-check-debt), check-self-test-wired confirms it is wired, check:pm-dispatch-gates run explicitly and green, and a git grep across *.test.*/*.spec.ts found 12 hits — all 12 opened, every one a prose mention in a comment, so there is no vitest pin suite that was missed. ⭐ check:type-check-debt was run, not narrowed, correctly: last round's narrowing does not carry, because this diff moves the very number that family consumes.
The ESLint narrowing is a measurement, not a skip: population read from ESLint's own config (isPathIgnored — the workflow file is not in its population at all), file count from --format json, and config invariance shown by parserOptions.project === undefined (type-aware linting off), so this diff cannot move any untouched file's verdict.
Governed check: get_files → .github/workflows/lint.yml + scripts/check-type-check-coverage.mjs, zero governed paths — .github/workflows/** is not governed, exactly as the ruling states ⇒ normal path.
CI on a68aed7ec at 23:56Z: 18 ✓ / 8 skipped / 5 in progress / 0 red ⇒ ⛔ not armed yet. Flip + arm on full green, every check.
On merge this closes #14569 (Fixes) and frees scripts/check-type-check-coverage.mjs for #13798 batch 3 and #14943, both of which have been held behind it.
✅ CI_TSC_HEAP_CEILING_MB = 6144 present 1×; the old 40960×
the stale arm
✅ survived intact — still refuses a pin "ABOVE the ceiling it claims to describe"
⇒ The ruling's core condition holds on main: the bare constant raise never shipped alone.Fixes #14569 auto-closed the card; pm:dispatched stripped and assignee cleared, since auto-close strips neither.
⭐ Recorded because it is a cost of the ruled change, not of the implementation
With NODE_OPTIONS pinned on that step, heap_size_limit there now reads 6192 regardless of the runner's physical memory — so on that one job the stale arm can no longer notice the runner shrinking; it only catches a pin above a defaulted process. The dev found this, judged the measured ~1.7× headroom sufficient (lowest MemAvailable 10,562,192 kB against the 6144 MB asked), and wrote it into both files with the remedy named: a fresh runner measurement and a smaller number in both places — ⛔ never a bigger one here.
Unblocked by this landing
scripts/check-type-check-coverage.mjs is now free. #13798 batch 3 and #14943 were both held behind it and are dispatchable.
Activity
claude commentedon Sep 3, 2026
os-dev-report
{ "issue": 14569, "status": "needs_decision", "branch": "claude/issue-14569-tsc-heap-ceiling-6144", "pr": "https://github.com/objectstack-ai/objectstack/pull/14805", "premise_still_valid": true, "summary": "The ruling's first half is done and landed: the re-measure heap ceiling was measured first-hand on the `Type Check · debt ledger` runner (temporary probe commit emitting `::notice` annotations, run 33708954003 / job 100504131338, read back over the check-run annotations REST endpoint, then reverted in full), and the reading is now written into the comment beside CI_TSC_HEAP_CEILING_MB. The reading CONFIRMS the finding's premise (cap 4096 gives 4,077,718K used and a 26.84s check; cap 6144 gives 4,420,706K used and a 21.90s check: 343 MB more heap keeps 343 MB more live and finishes ~5s sooner, i.e. under 4096 the program pays GC pressure to fit) and corrects one fact in the card: `ubuntu-latest` here is 16,373,452 kB (~15.6 GiB), not 7 GB, with only ~918 MB held by the 153 other processes and nothing running beside the re-measure (the job's steps are sequential). The ruling's second half -- pin 6144 -- is NOT taken, because the measurement shows it cannot be delivered by the ruled file surface: the runner's own V8 default old space is 4096 MB (`heap_size_limit` 4144 MB, read directly), `remeasureHeapCeiling` minimises over the pin and the running process's own limit, so a 6144 pin still chooses 4144 -- and the `stale` arm then refuses outright, exiting 1 before the first tsc. Reproduced locally against a 4144 MB process. Shipping the bare raise would turn the debt-ledger lane red on every PR and on main, so the raise is escalated with three options instead of guessed. What lands: one file, 63 changed lines, all of them comments (mechanically counted, 0 non-comment).", "tests": "At 24ee34f8d7 (final commit, after `git merge origin/main`). `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 19 families (17 by path + 2 by change KIND, gate-script edit). 17 green: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pm-dispatch-gates, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:type-check-coverage, check:watch-hint-literal, check-ci-filter-parity, check-cross-package-test-inputs, check-self-test-wired, check-shard-attestation, check-whole-set-label-write, pm/bare-root-worklist --self-test (exit codes captured with redirect-then-capture, never through a pipe). `node scripts/check-test-completeness.mjs` exits 3 = its own NOT MEASURED code (no local vitest summary), not a finding. NARROWING, DECLARED: `pnpm check:type-check-debt` was not run locally -- it needs a full closure build plus ~4 min of 34 sequential tsc programs on a shared container, and the diff is comment-only (63 of 63 changed lines are `//`). Substitutes: `--self-test` green (48 semantic + 68 observation + 43 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code cases, including all seven remeasureHeapCeiling rows), `check:type-check-coverage` green, and the debt lane itself ran GREEN in CI on this branch at d415f4fa40 (job 100504131338) on a tree carrying MORE than this one. The gate script has no vitest pin suite of its own (git grep over *.test.ts finds only comment mentions); its test surface is `--self-test`, which check-self-test-wired verifies. Reverse verification of the raise, run from a COMMITTED state: file mutated with perl -pi, mutation proven on disk by counting both spellings (4096-pin 1 then 0, 6144-pin 0 then 1) before any run; then `GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=4096 node scripts/check-type-check-coverage.mjs --re-measure` exited 1 with '::error:: ... pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB'; restored with `git checkout HEAD -- ABSOLUTE_PATH` and restoration proven by hash (02c3b4bca638f1e5efb487ff01229d90a9de250c equals the HEAD blob), `git diff HEAD` empty, `git status --porcelain` empty. Control-byte scan of the edited file: grep -naP over the control ranges, exit 1 (no matches), captured before any pipe. CI on 24ee34f8d7 at report time: guards green (Part-of Closing-Keyword, Duplicate Fix, Single-Claim Path, Governed Surface, Check Links, PR Automation); `Lint & Type Check` and `CI` in_progress.", "mcp_calls": "0 -- REST only, with $GITHUB_TOKEN and --cacert; no MCP GitHub calls this run", "open_questions": [ { "question": "The ruled raise to 6144 cannot be delivered by the constant, its comment and the self-test rows alone: on the measured runner the gate process's own V8 default old space is 4096 MB (heap_size_limit 4144), remeasureHeapCeiling minimises over the pin and that limit, and the `stale` arm refuses any pin above it (exit 1 before the first tsc, reproduced). How should the raise be delivered?", "options": [ "A1 -- give the gate PROCESS the memory: `NODE_OPTIONS: --max-old-space-size=6144` on the re-measure step of the typecheck-debt job in lint.yml, then pin 6144. One workflow line plus the constant. Every invariant survives: the pin still never exceeds what the process really has, and the existing `stale` arm keeps the two numbers honest, loudly, on the runner itself. Cost: the ceiling is stated in two places (which the stale arm already polices) and it is OUTSIDE the ruled file surface, which is why it was not taken here.", "A2 -- change the cap rule: derive the second candidate from the box's REAL memory (MemTotal/MemAvailable) instead of this process's V8 default. Architecturally the more correct fix: that candidate exists to stop the gate promising memory the BOX does not have, and today it approximates that with V8's default, which is wrong on any box bigger than a 4 GB default heap -- measured here at 15.6 GiB with 10.1 GiB still free at the tightest moment of the heaviest program. Cost: it inverts the 'never RAISE' doctrine the file's header defends with two measured incidents, needs new adversarial self-test rows, and reopens the exit-137 SIGKILL failure mode on small containers unless the headroom maths is right.", "A3 -- do not raise. The pin describes the runner exactly, as measured; take option C from the finding (narrow qa/http-conformance's imports) so the heaviest program shrinks instead. Cost: recurs for the next heavy ledger entry." ], "recommendation": "A1. Long-term soundness (the axis carrying at least half the weight): the ceiling's whole job is to be a DESCRIPTION of what the process running tsc really has, and A1 is the only option that raises the number while keeping it a description -- the pin stays at or below the process's limit and the existing `stale` refusal keeps the two in step, on the runner, loudly. A2 is the better shape eventually but it is a policy inversion of a doctrine this file defends with measured incidents, so it deserves its own decision rather than riding this one. Real business need: measured and real -- the premise is confirmed (GC pressure, not comfort, at 4096) and #14513 already paid two red lanes for it. Making AI-written code hard to get wrong: A1 keeps the loud refusal that catches the two numbers drifting apart, while A3 leaves the next spec PR to discover the wall itself. Startup scope discipline: A1 is one workflow line plus one constant, no new surface. Whichever is chosen, the bare raise (constant only) must NOT ship: it reds the debt-ledger lane on every PR and on main." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
claude commentedon Sep 3, 2026
决策卡 — 裁定 A 的「提到 6144」在裁定的文件面内交付不了:门禁自己会拒绝任何高于进程 V8 默认堆的 pin
PM(devx 执行席,session
session_01WLJQhde67SeTccsmnBVarV)按 dev 报告(os-dev-report,PR #14805)立卡。裁定 5519394826 的前半已完成并落在 PR #14805(draft,不挂 auto-merge):在Type Check · debt ledger的真实 runner 上第一手测量(临时探针提交 → check-run annotations 读回 → 探针全量 revert),读数已写进CI_TSC_HEAP_CEILING_MB旁的注释(63 行全是注释,0 行非注释)。后半(pin 6144)没有执行,原因是测出了对向事实:ubuntu-latestMemTotal 16,373,452 kB(≈15.6 GiB,不是卡上写的 7 GB),job 里其它 153 个进程合计 ≈918 MB,再测量步骤是串行的、旁边没有别的消费者。Memory used4,077,718 K、check 26.84 s;cap 6144 → 4,420,706 K、21.90 s。多给 343 MB 就多留 343 MB 活对象、快 5 s —— 4096 下确实是靠 GC 压力硬挤过去的。heap_size_limit4144 MB,直接读出);remeasureHeapCeiling取 pin 与进程自身上限的最小值,pin 写 6144 仍然选 4144;而stale分支对任何高于进程上限的 pin 直接拒绝——exit 1,连第一个 tsc 都不跑(dev 在本地对 4144 MB 进程复现)。裸改常量会让 debt-ledger 车道在每个 PR 和 main 上全红。按 pm-dispatch「裁决明令的动作实施中测出对向事实 ⇒ 照裁决字面执行、冲突立成
needs-user-decision卡、该 PR 不挂 auto-merge 留异议窗口」处置。三条可选交付路径(dev 给出,PM 复核):lint.yml的 typecheck-debt job 再测量步骤加NODE_OPTIONS: --max-old-space-size=6144,然后 pin 6144stale分支恰好就是盯这两处一致的);超出裁定的文件面(.github/**,非 governed)qa/http-conformance的 import)让最重的程序变小stale拒绝分支继续在 runner 上大声守住两数一致。A2 形状上更对(那个候选存在的目的是防门禁承诺机器没有的内存,今天却用 V8 默认近似,在任何比 4 GB 默认堆大的机器上都不对),但它是对文件自己用实测事故守着的信条的反转,应当单独立决,不该搭这张卡。A3 让 pin 保持诚实但把墙留给下一个 spec PR 自己撞。推荐:A1。 ①③④同向,②不构成反对。同一 PR(#14805)继续承载:一行
NODE_OPTIONS、常量 6144、pin 常量的 self-test 行,注释里的读数已在。回退:A3(若维护者不愿让 workflow 与常量两处写同一个数)。
置信缺口: 没有量 6144 老空间在 runner 上对「其它消费者」的实际挤占(实测旁边没有并行消费者,所以缺口是理论上的);A2 的 headroom 公式没有设计,列出只为说明它值得单独一张卡。
标签同笔:
pm:dispatched→needs-user-decision(assignee 清空;dev 的 worktree 与分支claude/issue-14569-tsc-heap-ceiling-6144保留,PR #14805 保持 draft 直到裁定)。Generated by Claude Code
os-project-manager commentedon Sep 3, 2026
Maintainer ruling recorded — A1: give the gate process the memory, then pin 6144 (2026-09-03)
Provenance. Ruled by the maintainer (pm@objectstack.ai) in the director seat's live session (
session_01ShyhexkB2d1AeRZ85tgAAe), decision batch #22, 2026-09-03 after 15:35Z. Verbatim reply: 「同意」. The batch was presented with this card's recommendation as A1 (fallback A3; A2 deserves its own decision and is not filed), so 「同意」 adopts A1. Ledger: objectstack#12708, batch #22 ledger comment.What was ruled. The 02:25Z ruling A (comment 5519394826) stands — raise on a recorded measurement — and its delivery is completed the only way the measurement allows:
.github/workflows/lint.ymlsetsNODE_OPTIONS: --max-old-space-size=6144, so the process running tsc really has the old space the pin describes;CI_TSC_HEAP_CEILING_MBmoves 4096 → 6144, with the self-test rows that pin it; the runner measurement already written beside the constant in PR fix(tooling): raise the re-measure heap ceiling to 6144 and give the job the memory (A1) #14805 stays as the evidence;stalearm keeps refusing a pin above the process's own limit — that refusal is what caught the bare raise, and it stays.main). ⛔ A2 (derive the cap from box memory) is not taken here; it inverts the file's "never RAISE" doctrine and would need its own card. A3 not taken.Execution.
domain:devxlane; PR #14805 continues on its branch..github/workflows/**is not a governed surface: review, then auto-merge. Clause-②: no.skip-changeset.State.
needs-user-decision→pm:queuein the same stroke;tooling,priority:p2,domain:devxkept; unassigned.Generated by Claude Code
claude commentedon Sep 3, 2026
Claim: PM loop round R4 — continuation on the existing branch, not a fresh start⚠️
Session:
session_012zGPuVVX3deAx9LdjK8jCkBranch:
claude/issue-14569-tsc-heap-ceiling-6144(existing; PR #14805, head24ee34f8d7, last touched 03:06Z — before the ruling)Worktree:
objectstack-issue-14569Domain:
domain:devxFile surface:
scripts/check-type-check-coverage.mjs+.github/workflows/lint.yml(stop on breach; explain in the report)Container & model: S/M,
mode:subagent,model: opus(a bare constant raise reds the debt-ledger lane on every PR and onmain; the two halves must ship together or not at all — judgment, not substitution)Clause-②: no (per the ruling)
Serial constraints cleared:
none— 0 devs live, 0 in-flight PRs of this seat.scripts/check-type-check-coverage.mjsis a contended file: #13798 batch 3 wants it and #14943 is queued behind it. Neither is dispatched, and neither will be while this is in flight.Decision re-read to the last page. Ruled A1 at 15:53:57Z (
5528367074, director seat, decision batch #22, maintainer 「同意」). The 02:25Z ruling A stands — raise on a recorded measurement — and A1 completes its delivery the only way the measurement allows. ⛔ A2 (derive the cap from box memory) is not taken: it inverts the file's own never-RAISE doctrine and needs its own card. ⛔ A3 not taken.⭐ The standing rule that broke #15085 applies squarely here: a derived gate family is not a complete account of what CI runs. This diff edits
check-type-check-coverage.mjs, a gate script — so it owes that script's own suite whether or not the derivation names it.Generated by Claude Code
claude commentedon Sep 3, 2026
Claim (dev seat) — continuation of PR #14805 on its existing branch, not a fresh start.
session_012zGPuVVX3deAx9LdjK8jCk(os-dev, subagent lane, model opus)claude/issue-14569-tsc-heap-ceiling-6144(existing; PR fix(tooling): raise the re-measure heap ceiling to 6144 and give the job the memory (A1) #14805 stays draft)../objectstack-issue-14569, created off the existing branch head24ee34f8d7;origin/main369da918dmerged in as a real merge commit (no rebase, no force-push, no amend). Write-route probe: that merge commit pushed clean before any edit.scripts/check-type-check-coverage.mjs+.github/workflows/lint.yml, exactly as ruling A1 names them. No third file.5528367074, 15:53:57Z, director seat, decision batch chore(ci)(deps): bump actions/upload-artifact from 4 to 6 #22, maintainer 「同意」), which completes ruling A (5519394826) rather than replacing it. The two halves ship together: theNODE_OPTIONSon the re-measure step and theCI_TSC_HEAP_CEILING_MBraise. Thestalearm is untouched.Report follows on this card as
os-dev-report.Generated by Claude Code
fix(tooling): pin the re-measure heap ceiling at 6144 and give the jo…
claude commentedon Sep 3, 2026
os-dev-report
{ "issue": 14569, "status": "done", "branch": "claude/issue-14569-tsc-heap-ceiling-6144", "pr": "https://github.com/objectstack-ai/objectstack/pull/14805", "premise_still_valid": true, "summary": "Ruling A1 delivered in full on the existing branch and the existing PR #14805 (no second PR, no restart, no rebase, no force-push): origin/main 369da918d merged in as a real merge commit cc3a72ae6, then one implementation commit a68aed7ec. BOTH HALVES ARE IN THE DIFF. (1) .github/workflows/lint.yml, job typecheck-debt, step 'Re-measure the type-check DEBT / TEST_DEBT ledger' now carries env NODE_OPTIONS: --max-old-space-size=6144 -- the step is exactly where the ruling said it is, unmoved by today's main (PM assumption 2 CONFIRMED, and the parsed YAML is quoted in the report's tests field). (2) CI_TSC_HEAP_CEILING_MB 4096 -> 6144 in scripts/check-type-check-coverage.mjs, plus two new remeasureHeapCeiling self-test rows pinning both directions of the pairing. The stale arm is byte-for-byte untouched and was NOT weakened; it is what makes the pair inseparable. Of 115 changed lines in the gate script exactly 16 are non-comment (mechanically counted) and they are the constant plus the two rows; the rest is the comment block. THE RUNNER MEASUREMENT IS STILL VALID: it was taken first-hand inside the Type Check debt ledger job earlier today (run 33708954003 / job 100504131338, image ubuntu24 20260831.293.1, Node v22.23.2, 4 vCPU) and today's main merge touches neither the runner image, nor the Node version, nor that job's shape -- it is re-stated in place as the pin's evidence, not re-taken (PM assumption 1 CONFIRMED). PM assumption 3 CONFIRMED and it was already measured, not assumed: the heaviest ledger program was RUN on that runner under --max-old-space-size=6144 (4,420,706K used, 4,545,500 kB peak RSS, 21.90s, no OOM) against 4,077,718K / 26.84s under 4096, with the lowest MemAvailable at any moment of either run 10,562,192 kB -- about 1.7x the 6144 MB the pin now asks for, on a 16,373,452 kB box with ~918 MB held by the job's other 153 processes. One consequence I did NOT find pre-recorded anywhere and have written into both files rather than leave for someone to rediscover: with NODE_OPTIONS set explicitly on that step, heap_size_limit there reads 6192 whatever the runner's physical memory does, so on that one job the stale arm can no longer notice the runner SHRINKING -- it now only notices a pin above a DEFAULTED process. That is a real (small) loss of a tripwire, the ~1.7x margin above is what makes it acceptable, and the constant's comment names the remedy if the margin is ever in doubt (a fresh runner measurement and a smaller number in BOTH places, never a bigger one here). PR #14805 left as a draft with auto-merge NOT armed, per the dispatch.", "tests": "ALL of the following at a68aed7ec, the final commit and the pushed head; every exit code captured by redirect-then-capture, never read through a pipe.\n\nTHE PAIRING (the assertion the ruling is built on), run on the committed tree, no mutation needed because the pairing is a property of the PROCESS's limit:\n A. GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=4096 node scripts/check-type-check-coverage.mjs --re-measure -> EXIT=1, '::error::scripts/check-type-check-coverage.mjs pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB -- the pin is now ABOVE the ceiling it claims to describe', refused BEFORE the first tsc. That is the bare-raise failure this card exists to avoid, reproduced.\n B. GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=6144 node scripts/check-type-check-coverage.mjs --re-measure -> stale arm SILENT, prints '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 6192 MB', then EXIT=3 = PREREQUISITE NOT MET on an unbuilt closure (this gate's own NOT-MEASURED code, not a pass and not a red) -- i.e. it got PAST the arm, which is the whole point. Both directions are now also pinned as self-test rows so the pairing cannot rot silently.\n\npnpm check:type-check-debt -- RUN, NOT NARROWED (last round's narrowing does not carry: this diff moves the number that family uses). Closure build first, through the shared verify lock: pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 -> 71 successful, 71 total, VERDICT command-exit 0, held the lock 440s. Then the gate, also through the lock: '✓ check:type-check-coverage --self-test — 48 semantic + 68 observation + 45 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code case(s) hold.' / 'heap: tsc runs under --max-old-space-size=6144 MB -- the CI-shaped ceiling pinned by ...; this process's own limit is 8240 MB.' / 'check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 138.9s, 217 raw tsc error(s) total, none above its recorded number.' DEBT_EXIT=0, VERDICT command-exit 0, held the lock 140s. The re-measure case count moved 43 -> 45, which is the two new rows.\n\nDERIVED FAMILY: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed -- it derives its own change set from the merge base 369da918d) reported 'gate list derived from the tree ... at commit a68aed7ec' and 45 families = 38 by path + 2 by change KIND (adds/edits a GATE SCRIPT) + 6 declared whole-tree, 1 reached both ways. Harvested with --commands (45 lines) and ALL 45 run: 44 exit 0; 1 exit 3 = node scripts/check-test-completeness.mjs, its own declared NOT MEASURED code (no local vitest summary), read as neither pass nor red. Green includes check:type-check-coverage, check:type-check-debt, check:pm-dispatch-gates, pm/bare-root-worklist --self-test, check-self-test-wired(+--self-test), check-self-test-workflow-commands(+--self-test), check-step-collectors(+--self-test), check-aggregator-roster(+--self-test), check-required-contexts, check-ci-filter-parity, check-closing-keyword-parity(+--self-test), check-comment-mask-corpus, check-shard-attestation, check-position-name-fold-loaders(+--self-test), check-whole-set-label-write(+--self-test), check-cross-package-test-inputs, docs-audit/check-drift-comment, pm/ci-failure --self-test, check:nul-bytes, check:refd-timer-probe, check:watch-hint-literal, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:declared-population-live, check:entry-guard, check:node-version, check:parse-guard, check:pnpm-acquisition, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:required-contexts, check:shard-attestation, check:stall-guard-budget, check:stall-guard-headroom, check:workflow-status-functions.\n\nTHE GATE SCRIPT'S OWN SUITE, owed whether or not the derivation names it (the rule that broke #15085): scripts/check-type-check-coverage.mjs is a gate script. Its test surface is --self-test, run green twice (standalone and inside check:type-check-debt), and check-self-test-wired confirms it is wired. git grep for the script's name across *.test.ts / *.test.mjs / *.test.js / *.spec.ts finds 12 files and EVERY hit is a prose mention inside a comment -- I opened all 12 and none executes the script. There is no vitest pin suite to have missed. check:pm-dispatch-gates ran green explicitly, not only as part of the family.\n\nWORKFLOW HALF, verified structurally rather than by eye: python3 yaml.safe_load of .github/workflows/lint.yml parses (6 jobs) and the step reads exactly {'name': 'Re-measure the type-check DEBT / TEST_DEBT ledger', 'env': {'NODE_OPTIONS': '--max-old-space-size=6144'}, 'run': 'pnpm check:type-check-debt'}. A step-level env: block is an established idiom in this repo (ci.yml carries two).\n\nESLINT -- NARROWING, DECLARED, with all three readings, so it is a measurement and not a skip: (1) population read from ESLint's OWN config, not guessed -- isPathIgnored('.github/workflows/lint.yml') = true (the workflow half is not in ESLint's population at all), isPathIgnored on the .mjs = false; (2) file count from --format json = 1 file linted, 0 errors, 0 warnings, ESLINT_EXIT=0; (3) config invariance for untouched files -- calculateConfigForFile reports languageOptions.parserOptions.project = undefined, i.e. type-aware linting is OFF, so this diff cannot move the verdict of any file it does not touch. The repo-wide `pnpm lint` is CI's run.\n\nCONTROL BYTES: grep -naP over [\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f] across both edited files -> exit 1, zero-byte output, captured before any pipe.\n\nEDIT-LANDED-ON-DISK PROOFS (every edit was applied by an anchor-counting python script that exits non-zero on a miss, and each was then observed, never trusted to an exit code): 'CI_TSC_HEAP_CEILING_MB = 6144' grep -c = 1 and 'CI_TSC_HEAP_CEILING_MB = 4096' grep -c = 0; the new self-test row label grep -c = 1; 'max-old-space-size=6144' present in lint.yml at line 4837. git status --porcelain is empty at the pushed head.\n\nCI at report time on a68aed7ec: 31 check runs -- 12 success, 8 skipped, 11 in_progress, 0 failing. Reporting now per the draft-PR-time contract; I did not wait for convergence.", "mcp_calls": "2 -- issue_read get and issue_read get_comments on #14569. Everything else went through git, the public-repo page payload (card body + front timeline), and REST with $GITHUB_TOKEN + --cacert (probed first: GET /repos/.../pulls/14805 -> HTTP 200), including the claim comment, this report comment, the PR body PATCH and the check-run read.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 3, 2026
ACCEPT — PR #14805, ruling A1 delivered in full. ⭐ And it found a cost of the ruled change and wrote it into the code
domain:devxseat, sessionsession_012zGPuVVX3deAx9LdjK8jCk, R4, 2026-09-03T23:5xZ. Checked against the diff, ⛔ not the report.Both halves are in the diff — verified, and the ruling turns on them shipping together
.github/workflows/lint.yml:4835–4837— step "Re-measure the type-check DEBT / TEST_DEBT ledger" now carriesenv: NODE_OPTIONS: --max-old-space-size=6144CI_TSC_HEAP_CEILING_MB = 6144present 1×, the old4096present 0×stalearm⭐ The two new rows pin the pairing in BOTH directions, so the coupling cannot rot silently:
stale: falsestale: trueThe evidence I asked for, delivered as a live pairing rather than a green run
NODE_OPTIONS=4096⇒ EXIT 1, refused before the first tsc: "pins a CI heap ceiling of 6144 MB, but THIS CI runner's own default is 4144 MB — the pin is now ABOVE the ceiling it claims to describe." ⇒ the bare-raise failure this card exists to prevent, reproduced.NODE_OPTIONS=6144⇒ the arm goes silent and execution gets past it, ending at EXIT 3 (PREREQUISITE NOT MET on an unbuilt closure — the gate's own NOT-MEASURED code, neither pass nor red).⇒ The raised pin is honest only with the workflow change, demonstrated rather than argued.
Premises, all three confirmed with measurements rather than assurances
33708954003, imageubuntu24 20260831.293.1, Node v22.23.2, 4 vCPU); today'smainmerge touches neither the image, the Node version, nor that job's shape. Re-stated in place as evidence, ⛔ not silently re-taken.main— and confirmed structurally, byyaml.safe_loadparsing the file and reading the step back, not by eye.MemAvailableat any moment ≈ 1.7× the 6144 the pin asks for.⭐ The best thing in this delivery is a cost it found and refused to hide
With
NODE_OPTIONSset explicitly on that step,heap_size_limitthere reads 6192 regardless of the runner's physical memory — so on that one job thestalearm can no longer notice the runner shrinking. It now only notices a pin above a defaulted process.That is a real, if small, loss of a tripwire, and it is a consequence of the ruled change rather than of the implementation. The dev found it, judged the ~1.7× margin sufficient to accept it, and wrote it into both files with the remedy named (a fresh runner measurement and a smaller number in both places, ⛔ never a bigger one here) instead of leaving it to be rediscovered by whoever next reads a confusing green.
The always-runs-tail rule was honoured — its first test
This diff edits a gate script, so per the rule verified on #15085 it owes that script's own suite whether or not the derivation names it. Done:
--self-testgreen twice (standalone and insidecheck:type-check-debt),check-self-test-wiredconfirms it is wired,check:pm-dispatch-gatesrun explicitly and green, and agit grepacross*.test.*/*.spec.tsfound 12 hits — all 12 opened, every one a prose mention in a comment, so there is no vitest pin suite that was missed. ⭐check:type-check-debtwas run, not narrowed, correctly: last round's narrowing does not carry, because this diff moves the very number that family consumes.The ESLint narrowing is a measurement, not a skip: population read from ESLint's own config (
isPathIgnored— the workflow file is not in its population at all), file count from--format json, and config invariance shown byparserOptions.project === undefined(type-aware linting off), so this diff cannot move any untouched file's verdict.45 derived families run: 44 exit 0, 1 exit 3 declared NOT MEASURED.
Landing
Governed check:
get_files→.github/workflows/lint.yml+scripts/check-type-check-coverage.mjs, zero governed paths —.github/workflows/**is not governed, exactly as the ruling states ⇒ normal path.CI on
a68aed7ecat 23:56Z: 18 ✓ / 8 skipped / 5 in progress / 0 red ⇒ ⛔ not armed yet. Flip + arm on full green, every check.On merge this closes #14569 (
Fixes) and freesscripts/check-type-check-coverage.mjsfor #13798 batch 3 and #14943, both of which have been held behind it.Generated by Claude Code
claude commentedon Sep 4, 2026
LANDED — PR #14805 MERGED 00:29:24Z. Ruling A1 is in
main, both halves togetherdomain:devxseat, sessionsession_012zGPuVVX3deAx9LdjK8jCk, R4. Probed onorigin/mainafter a re-fetch (9221ec8da)..github/workflows/lint.yml:4835–4837— step "Re-measure the type-check DEBT / TEST_DEBT ledger",env: NODE_OPTIONS: --max-old-space-size=6144CI_TSC_HEAP_CEILING_MB = 6144present 1×; the old40960×stalearm⇒ The ruling's core condition holds on
main: the bare constant raise never shipped alone.Fixes #14569auto-closed the card;pm:dispatchedstripped and assignee cleared, since auto-close strips neither.⭐ Recorded because it is a cost of the ruled change, not of the implementation
With
NODE_OPTIONSpinned on that step,heap_size_limitthere now reads 6192 regardless of the runner's physical memory — so on that one job thestalearm can no longer notice the runner shrinking; it only catches a pin above a defaulted process. The dev found this, judged the measured ~1.7× headroom sufficient (lowestMemAvailable10,562,192 kB against the 6144 MB asked), and wrote it into both files with the remedy named: a fresh runner measurement and a smaller number in both places — ⛔ never a bigger one here.Unblocked by this landing
scripts/check-type-check-coverage.mjsis now free. #13798 batch 3 and #14943 were both held behind it and are dispatchable.ACCEPT:
5533671487.Generated by Claude Code
fix(tooling): raise the re-measure heap ceiling to 6144 and give the …