Skip to content

fix: stay on the Node LTS line, and restore the mutation defence the ESM move dropped - #114

Merged
Arcanada merged 2 commits into
mainfrom
fix/node-24-21
Sep 14, 2026
Merged

Arcanada merged 2 commits into
mainfrom
fix/node-24-21

Conversation

@Arcanada

@Arcanada Arcanada commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Answers Dependabot #103 rather than merging it, and repairs two defects that #113 left behind.

Why not Node 26

Measured against nodejs.org/dist/index.json:

Line Latest lts
26.x v26.8.2 (2026-09-09) false — Current
24.x v24.21.0 (2026-09-07) Krypton — active LTS

#103 would move production off a supported line. That is not a dependency bump, so this takes the security patch within the LTS line instead.

There is a concrete reason to take it: v24.21.0 fixes an out-of-bounds write in mkdtemp for long prefixes (nodejs/node#64770), and this repository calls fs.mkdtempSync in three places — one of them the evidence path itself (mutation-harness.js gitSupplement).

Five pins move together, because regenerate-derived-artefacts.yml refuses a mismatch between ci.yml and .github/workflows/.node-version-for-derived.

The mutation defence that was lost

Both defects came in with #113 and were invisible until a full mutation run was attempted — the only caller of one is regenerate-derived-artefacts.yml, gated on author == 'dependabot[bot]', which never ran for #113.

1. The harness could not run at all. It spawns jest directly and did not pass --experimental-vm-modules, so under ESM every battery failed with SyntaxError: Cannot use import statement outside a module and the run stopped at baseline: FAIL.

Suites Tests
With the flag 20 passed 394 passed
Without it 20 failed 0

2. isolatedModules: true silenced nine mutants. It removes type-checking at transform time, and nine of the 85 mutants were KILLED_BY_TYPECHECK. With the flag in place:

Site main With isolatedModules
#6, #13, #21 KILLED_BY_TYPECHECK KILLED_BY_TEST
#11, #15, #16, #50, #52, #75 KILLED_BY_TYPECHECK SURVIVED

Six mutants of the validator and canonical-JSON paths that nothing catches. A baseline recording 0 survivors while the tree has 6 is worse than no baseline, so the flag is removed. Its cost, measured: local suite 26s → 58s, CI job 3min → 5min measured on this pull request (my own estimate of 3.5–4 was low). 67 suites / 1372 tests, all passing.

Measured

  • Mutation run from the repository root: 85 sites, 76 killed by test, 9 killed by compiler, 0 survived — identical to main.
  • --verify-structure: "Structurally verified 85 source-bound mutation records", no TOOLCHAIN_MISMATCH (tools.node now reads v24.21.0).
  • tsc --noEmit 0 under both tsconfig.json and tsconfig.test.json.
  • ChangeAdmissionReceipt/v1: verdict admitted, 3 entities, all verified, zero exemptions.
  • Local gate with the caller's own flag: ADMIT … codes=-.

One finding from the receipt was fixed rather than exempted: v-config-schema reported UNDECLARED_CONFIG_KEY NODE_OPTIONS, because the first version of the harness fix read process.env.NODE_OPTIONS. It no longer reads it.

Closes #103.


GOVERNANCE-CHANGE: the Node pin lives in four places that regenerate-derived-artefacts.yml refuses to see disagree — ci.yml, .github/workflows/.node-version-for-derived, apps/api/Dockerfile and the root engines field — so taking the LTS security patch (v24.21.0, out-of-bounds write in mkdtemp, nodejs/node#64770) cannot be done without touching ci.yml. No job, permission, runner label, ownership or branch-protection rule changes: the diff on both governance paths is the version string alone, 24.20.0 to 24.21.0.

…ESM move dropped

Dependabot #103 proposes node 24.20.0 -> 26.8-alpine. Measured against
nodejs.org/dist/index.json: the 24.x line is the CURRENT LTS (Krypton, v24.21.0
released 2026-09-07) and every 26.x release carries `lts: false` — 26 is Current,
not LTS. Moving production off a supported line is not a dependency bump, so this
takes the security patch WITHIN the LTS line instead: 24.21.0 everywhere.

There is a concrete reason to take it. v24.21.0 fixes an out-of-bounds write in
`mkdtemp` for long prefixes (nodejs/node#64770), and this repository calls
`fs.mkdtempSync` in three places, one of them the evidence path itself
(mutation-harness.js gitSupplement). Five pins move together, because
regenerate-derived-artefacts.yml REFUSES a mismatch between ci.yml and
.github/workflows/.node-version-for-derived: apps/api/Dockerfile (x2), ci.yml,
that pin file, and the root engines field.

THE DEFENCE THAT WAS LOST. Two defects, both introduced by the ESM move in #113
and both invisible until a full mutation run was attempted:

1. The harness spawns jest directly and did not pass --experimental-vm-modules,
   so under ESM every battery failed with `SyntaxError: Cannot use import
   statement outside a module` and the run stopped at `baseline: FAIL`. Measured:
   20 suites / 394 tests pass with the flag, 20 suites fail with 0 tests without
   it. `pnpm test` sets the flag; the harness never did. It went unnoticed because
   the only caller of a full run is regenerate-derived-artefacts.yml, gated on
   author == 'dependabot[bot]' — it never ran for #113. NODE_OPTIONS is appended,
   not assigned, so an inherited flag survives.

2. `isolatedModules: true` on the ts-jest transform removes type-checking at
   transform time, and nine of the 85 mutants were KILLED_BY_TYPECHECK. Measured
   with the flag: 79 killed by test, 6 SURVIVED — #11, #15, #16, #50, #52, #75,
   each a mutant the compiler used to reject and the tests do not catch. Three of
   the nine (#6, #13, #21) turned out to be caught by tests after all; six were
   not. A mutation baseline that records 0 survivors while the tree actually has 6
   is worse than no baseline. The flag is removed.

Its cost, measured: the local suite goes from 26s to 58s, and the CI job from
about 3 minutes to an expected 3.5-4. Six mutants of the canonical-JSON and
validator paths are worth thirty seconds.

Measured after, from the repository root (the invocation the evidence records):
85 sites, 76 killed by test, 9 killed by compiler, 0 survived — identical to the
counts on main. `--verify-structure` prints "Structurally verified 85
source-bound mutation records" with no TOOLCHAIN_MISMATCH, because tools now read
node v24.21.0. tsc --noEmit 0 under both tsconfig.json and tsconfig.test.json.

Closes #103 by answering it: the upgrade it asks for is refused with a measurement
and the security patch it was reaching for is taken.
Draft by the bundle's own verify.py in diff mode: verdict admitted, 3 entities,
all three verified, zero exemptions — nothing needed exempting, which is the
first receipt in this series that can say so.

One finding came out of it and was fixed rather than exempted. The first draft
reported v-config-schema UNDECLARED_CONFIG_KEY NODE_OPTIONS as failed, because
the harness fix read process.env.NODE_OPTIONS to append to an inherited value.
The verifier was right: reading it makes it a configuration key this file
consumes, and declaring a test-harness runtime flag in .env.example — the
application's own configuration — would have been the worse answer. The flag is
now set outright, and regenerate-derived-artefacts.yml (the only caller of a full
run) sets no NODE_OPTIONS, so nothing is lost.

Filed in the tree with the receipt included in its own change_set, and the
evidence's trackedTreeWithoutEvidence rebound after staging it — the MUN-0049
precedent. schema_check.py: CONFORMANT.
@Arcanada
Arcanada merged commit 4fcac08 into main Sep 14, 2026
8 of 9 checks passed
@Arcanada
Arcanada deleted the fix/node-24-21 branch September 14, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant