Skip to content

fix(deps): clear high-severity audit baseline - #50

Merged
Arcanada merged 6 commits into
mainfrom
arca-0209/muneral-audit-baseline
Sep 9, 2026
Merged

Arcanada merged 6 commits into
mainfrom
arca-0209/muneral-audit-baseline

Conversation

@Arcanada

Copy link
Copy Markdown
Contributor

Summary

ARCA-0209 task-owned baseline dependency repair. No product source, workflow, deploy, or infrastructure changes.

  • apps/web: axios ^1.8.4 → ^1.18.0
  • apps/web: Next.js 15.5.19 → 15.5.21
  • Version-scoped pnpm overrides added for vulnerable transitive ranges only
  • pnpm lockfile refreshed; repository-required mutation evidence binding regenerated

Verification

  • pnpm audit --audit-level high: 27 high advisories on main → 0 high advisories
  • Frozen install: passed
  • Lint: passed
  • Typecheck/build: passed
  • API tests: 1034/1034 passed
  • Web tests: 50/50 passed
  • Mutation campaign: 85 sites, 76 killed by tests, 9 killed by compiler, 0 survived
  • Production Docker image build: passed
  • Local health and auth-boundary smoke tests: passed

Arcanada and others added 4 commits September 9, 2026 16:58
#44 and #45 landed the same class of floors first, so most of this branch is
now already on main. Measured against main 85adee7 rather than assumed: of the
13 overrides this branch introduced, 12 are satisfied by what is there —
brace-expansion 1.1.18/5.0.9, fast-uri 3.1.5, form-data 4.0.6,
js-yaml 3.15.1/4.3.1, multer 2.3.0, nanoid 3.3.18, postcss 8.5.26,
sharp 0.35.4, socket.io-parser 4.2.7, ws 8.21.0 — and `next` is already
15.5.21.

The one floor main does NOT satisfy is deepmerge-ts: @prisma/config 7.8.0 pins
7.1.5 and the advisory is fixed only in 8.0.0. That is what this merge carries
forward, in main's own union file, alongside this branch's
apps/web/package.json axios declaration.

Resolution: main's override set is taken whole and the deepmerge-ts floor is
ADDED to it. This branch's narrower re-statements of the other twelve are
dropped as duplicates, not as disagreements — none of them would have changed
a resolved version. No hunk that carries something main lacks is discarded.
pnpm-lock.yaml is main's lockfile re-resolved by pnpm 11.5.2 against the
merged file, not a hand merge.

Verified before taking the override, because it crosses a major boundary
inside a dependency Prisma pins: deepmerge-ts resolves 8.0.2,
`prisma generate` rc 0 (Prisma Client v7.8.0), `prisma validate` rc 0,
pnpm lint rc 0, pnpm build rc 0 (3/3 tasks).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFqkBcMs3UH6LP3avgi9Q8
…pmerge-ts tree

pnpm-lock.yaml is a binding.files entry of the MUN-0022 evidence, so forcing
deepmerge-ts to 8.0.2 invalidates the record by design. Full campaign against
the merged tree, not a rebind:

  node v22.23.2 / pnpm 11.5.2 / jest 29.7.0 / typescript 5.9.3
  baseline 394/394 tests, 20/20 suites
  85 sites: 76 killed by test, 9 killed by the compiler, 0 survived

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFqkBcMs3UH6LP3avgi9Q8
DEC-AUP-0008 / AUP-GRAPH-006. Bound to 85adee7..d968284.

  305 verified, 151 not_measured, 0 failed; every non-verified entity carries
  an exemption with an owner and a 2026-09-23 expiry
  -> admitted_with_exemptions.

130 of the 151 are not_measured only because v-canary is unconditional for
inferred boundaries and no canary exists before the deploy. 9 are work items
(I14), 8 live outside every tsconfig include and are unchanged here, 4 are
historical receipts, one of which (#82's) this change archives out of the
gate's discovery globs — the convention #82, #45 and #77 each followed.

The notes record what this change does NOT do: it closes deepmerge-ts and
nothing else, and next >=15.5.24 (critical), fast-uri, js-yaml, browserslist
and mysql2 advisories remain open on main.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFqkBcMs3UH6LP3avgi9Q8
…t carries the receipt

Adding the receipt and archiving #82's changed the tracked tree, and
trackedTreeWithoutEvidence hashes all of it. Same campaign, same toolchain
(node v22.23.2 / pnpm 11.5.2): 85 sites, 76 killed by test, 9 killed by the
compiler, 0 survived; --verify-structure exits 0 on this tree.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFqkBcMs3UH6LP3avgi9Q8
@Arcanada

Arcanada commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Re-measured against main 85adee7 — scope reduced to the one floor that is still needed

The verification block above was measured on 2026-09-09 against an older main and an older advisory database. Neither holds now, so here is what was actually measured today, after #44 and #45 landed the same class of floors:

12 of this branch's 13 overrides are already satisfied on main — brace-expansion 1.1.18/5.0.9, fast-uri 3.1.5, form-data 4.0.6, js-yaml 3.15.1/4.3.1, multer 2.3.0, nanoid 3.3.18, postcss 8.5.26, sharp 0.35.4, socket.io-parser 4.2.7, ws 8.21.0 — and next is already 15.5.21.

The one that is not: deepmerge-ts. @prisma/[email protected] pins 7.1.5 and the high advisory is fixed only in 8.0.0. That is what this branch now carries, added to main's union override set rather than replacing it — the other twelve are dropped as duplicates, not as disagreements.

Because the floor crosses a major boundary inside a dependency Prisma pins, it was verified before being taken: deepmerge-ts resolves 8.0.2, prisma generate rc 0 (Prisma Client v7.8.0), prisma validate rc 0, pnpm lint rc 0, pnpm build rc 0 (3/3 tasks). The MUN-0022 mutation evidence was re-run in full (pnpm-lock.yaml is a binding.files entry, so a rebind would have asserted an unmeasured result): 85 sites, 76 killed by test, 9 killed by the compiler, 0 survived, on node v22.23.2 / pnpm 11.5.2, baseline 394/394 tests and 20/20 suites.

Correction to the original claim of "0 high advisories": pnpm audit on this head still reports advisories this change does not clear — next >=15.5.24 (critical), fast-uri >=3.1.6, js-yaml >=3.15.2 / >=4.3.2, browserslist >=4.28.7, mysql2 >=3.22.0. The advisory database moved after these floors were written. This change closes deepmerge-ts and nothing else, and that residue is recorded in the admission receipt rather than claimed away.

ChangeAdmissionReceipt/v1 bound to 85adee7..d968284: 305 verified, 151 not_measured (all exempted, owner + 2026-09-23 expiry), 0 failed → admitted_with_exemptions; the vendored bundle's own ci_gate.py at program_ref 446e8dce reproduces that locally. gitleaks 8.30.1 over the range: no leaks found.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RFqkBcMs3UH6LP3avgi9Q8

@Arcanada
Arcanada merged commit d5c7de4 into main Sep 9, 2026
9 checks passed
@Arcanada
Arcanada deleted the arca-0209/muneral-audit-baseline branch September 9, 2026 17:23
Arcanada added a commit that referenced this pull request Sep 9, 2026
…h-admission discovery globs

CI's graph-admission check refused this PR with a code unrelated to this
change: change-admission-arca0209-deepmerge-20260909T171700Z.json (landed
by #50, ARCA-0209's deepmerge work) has a receipt head that is not an
ancestor of THIS PR's range, and the check discovers it anyway because
ci_gate.py globs receipts/graph/**/*.json and receipts/**/change-admission-*.json
unconditionally -- so it would poison every PR against main until archived,
this one included. Same defect class as commit 1006f41 (the SEC-0052
receipt), same fix: move out of both globs (drop the change-admission-
prefix, move out of receipts/graph/) into receipts/archive/, which is
outside both.

Regenerates apps/api/test/assembly/mutation-results.json once more: the
rename changes the tracked tree, so trackedTreeWithoutEvidence must
reflect it (0 survived / 76 killed-by-test / 9 killed-by-typecheck across
the same 85 sites; --verify-structure rc 0).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01H4ywto9ubXNtjT5aLUt3Fr
Arcanada added a commit that referenced this pull request Sep 9, 2026
Rebasing onto main (post #80's Node-pin reconciliation) moved the tracked
tree, so the checked-in evidence's git-supplement binding went stale —
same pattern #44/#45/#50/#80/#82 each hit. Regenerated: 0 survived / 76
killed-by-test / 9 killed-by-typecheck across the same 85 sites;
--verify-structure rc 0. No source changes, evidence only.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY
Arcanada added a commit that referenced this pull request Sep 9, 2026
* chore(deps): bump next from 15.5.21 to 15.5.24

Bumps [next](https://github.com/vercel/next.js) from 15.5.21 to 15.5.24.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](vercel/next.js@v15.5.21...v15.5.24)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.24
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>

* test: regenerate assembly mutation evidence for the rebased tree

Rebasing onto main (post #80's Node-pin reconciliation) moved the tracked
tree, so the checked-in evidence's git-supplement binding went stale —
same pattern #44/#45/#50/#80/#82 each hit. Regenerated: 0 survived / 76
killed-by-test / 9 killed-by-typecheck across the same 85 sites;
--verify-structure rc 0. No source changes, evidence only.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* test: regenerate assembly mutation evidence for the reconciled merge tree

Reconciles a concurrent-push collision on this branch (another session also
regenerated evidence for the same rebase and pushed first) -- the merge of
the two updates left the git-supplement binding stale again, so a third,
fresh regen against the actual merged tree. 0 survived / 76 killed-by-test /
9 killed-by-typecheck across the same 85 sites; --verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* receipt(next-critical-rce): file the ChangeAdmissionReceipt/v1 for this change (DEC-AUP-0008)

Bound to c048c13..397a538 (the substantive commit, PR #79 next 15.5.21->
15.5.24): global_fallback triggered on pnpm-lock.yaml, 457-entity impact set,
all verified by the single whole-repository verifier appropriate to a
dependency patch bump -- PR #79's own CI lint-and-test job on the real fleet
runner (run 34396752144, job 102618256943, exit 0), not a local claim.
Closes GHSA-2xp9-vwfh-vxw4 and GHSA-p293-qw3h-jr36 (both critical,
unauthenticated RCE), first_patched_version 15.5.24 exactly matched.
admission.verdict: admitted.

Not eligible for the automated-author exemption despite dependabot[bot]
authorship: the evidence-rebind commits touch
apps/api/test/assembly/mutation-results.json, outside the dependency-
manifest allowlist, so this is an agent-authored receipt like #80's/#83's.

change_set includes the receipt's own path (kind=receipt, status=A,
new=true) -- learned from #83's CHANGE_SET_INCOMPLETE round-trip: impact.py's
skeleton predates the receipt file, so it never lists itself.

Also regenerates apps/api/test/assembly/mutation-results.json for the third
time on this branch: the harness's gitSupplement() seeds its git-supplement
binding from `git write-tree` on the REAL INDEX, so the receipt had to be
`git add`-ed before this run, not after -- learned the hard way on #83's
first attempt. 0 survived / 76 killed-by-test / 9 killed-by-typecheck across
the same 85 sites; --verify-structure rc 0. Locally confirmed with
`admit_change.py gate --receipt ...`.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* chore(receipts): archive #80's stale MUN-node-pin receipt out of the discovery globs

Same fix as muneral#83: CI's real graph-admission run (discovery mode) was
refusing this PR with RECEIPT_NOT_BOUND_TO_RANGE for #80's own receipt,
merged but never archived (36c7f46 archived an earlier one; this one was
missed). Renamed off the change-admission- prefix to escape both discovery
globs, added the rename itself as a change_set entry (kind=receipt,
status=R) in this PR's own receipt.

Regenerates evidence for the same tracked-tree reason as every commit on
this branch: 0 survived / 76 killed-by-test / 9 killed-by-typecheck,
--verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Arcanada <[email protected]>
Co-authored-by: Arcanada <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
Arcanada added a commit that referenced this pull request Sep 9, 2026
… js-yaml, mysql2, qs, hono, baseline-browser-mapping) (#83)

* fix(deps): raise 7 unmatched security floors — fast-uri, browserslist, js-yaml, mysql2, qs, hono, baseline-browser-mapping

71 open muneral dependabot alerts closed to 21 by earlier PRs (#44/#45/#82/#80);
these 7 packages had no dependabot PR covering their advisories at all:

- fast-uri: prior floor ^3.1.5 resolved to exactly 3.1.5, which a newer advisory
  (GHSA-jqff-g426-hqxp / GHSA-f65p-4m7j-42xc / GHSA-fph4-wmhf-6fwf /
  GHSA-5jgf-p345-68v8, patched 3.1.6) outran — a real defect in the prior fix,
  not a gap. Raised to ^3.1.6, resolves to 3.1.7.
- hono: same shape, ^4.12.34 -> ^4.13.5 (GHSA-crvj-82cr-hjcx / GHSA-gqvv-2mrq-wpjv).
- js-yaml: ^3.15.1/^4.3.1 -> ^3.15.2/^4.3.2 (GHSA-2883-xcg3-v3hh, both major lines).
- browserslist, baseline-browser-mapping, mysql2, qs: no override existed;
  added floors matching each advisory's first_patched_version
  (GHSA-73wf-gq98-2v4g/GHSA-c83g-rgw3-j3cx, GHSA-w5vr-8v7q-w6rv,
  GHSA-3f6p-5ww8-9rcr, GHSA-x5fp-wj9c-mxmx/GHSA-4mjr-xmp4-gh2g).

Regenerates apps/api/test/assembly/mutation-results.json: the lockfile change
moves the tracked tree, so the evidence document's git-supplement binding goes
stale the same way it did for #44/#45/#50/#80/#82. 0 survived / 76
killed-by-test / 9 killed-by-typecheck across the same 85 sites; --verify-structure rc 0.

apps/web and apps/api build clean; full non-e2e unit suite green (1280/1283,
only the expected evidence-staleness failures before this regen); e2e suites
need a live Postgres this shell doesn't have — deferred to CI's own service
container, consistent with how this session has treated CI as the authority
throughout this task.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* receipt(sec-floors-round2): file the ChangeAdmissionReceipt/v1 for this change (DEC-AUP-0008)

Bound to c048c13..65b45b0 (the substantive commit): global_fallback triggered
on pnpm-lock.yaml/pnpm-workspace.yaml, 458-entity impact set, all verified by
the single whole-repository verifier appropriate to a dependency-floor change --
PR #83's own CI lint-and-test job on the real fleet runner (run 34395035927,
job 102612514884, exit 0), not a local claim. admission.verdict: admitted.
Confirmed via `admit_change.py gate --receipt ...`: ADMIT.

Regenerates apps/api/test/assembly/mutation-results.json once more: the
receipt itself is a newly tracked file, so trackedTreeWithoutEvidence must
reflect its presence too (0 survived / 76 killed-by-test / 9 killed-by-
typecheck across the same 85 sites; --verify-structure rc 0).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* fix: correct the evidence binding — regenerate after the receipt was staged, not before

The prior commit ran mutation-harness.js while the receipt file was still
untracked on disk: gitSupplement()'s git write-tree seeds from the real
index, so an unstaged file is invisible to it, and the binding it recorded
matched the tree WITHOUT the receipt rather than the tree the commit
actually produced. CI caught it (real lint-and-test failure, not a flake).
Regenerated again with nothing else changed: 0 survived / 76 killed-by-test
/ 9 killed-by-typecheck across the same 85 sites, --verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* fix: add the receipt's own path to its change_set — CHANGE_SET_INCOMPLETE

impact.py's skeleton is computed before the receipt file exists, so it never
lists the receipt itself as an added file. graph-admission caught it for
real: "1/4 changed file(s) are absent from the receipt change_set". Added
the receipt's own entry (kind=receipt, status=A, new=true), matching #80's
own receipt's pattern. Locally re-verified with `admit_change.py gate
--receipt ...`: ADMIT, c048c13..b66c46e, files=4.

Regenerates the evidence once more for the same reason as before (the
receipt content changed, moving the tracked tree): 0 survived / 76
killed-by-test / 9 killed-by-typecheck, --verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* chore(receipts): archive #80's stale MUN-node-pin receipt out of the discovery globs

CI's real graph-admission run (discovery mode, no explicit --receipt) was
refusing this PR with RECEIPT_NOT_BOUND_TO_RANGE for
change-admission-MUN-node-pin-20260909T175500Z.json -- #80's own receipt,
merged days^H^Hhours ago, never archived afterward the way #77's and the
ARCA-0209 one were (36c7f46). It sat in receipts/graph/, matched by both
discovery globs, and its unrelated unbound-ness poisoned every subsequent
PR's overall verdict even when that PR's own receipt was valid -- confirmed
locally: `gate --receipt <mine>` (explicit, bypasses discovery) said ADMIT
all along, while `gate` with no --receipt (discovery, what CI actually runs)
said refused, for exactly this reason.

Renamed off the change-admission- prefix (not just moved) so it also escapes
receipts/**/change-admission-*.json, the second glob. Locally reconfirmed
discovery-mode ADMIT after the move.

Regenerates the evidence once more for the same tracked-tree reason as
every other commit on this branch: 0 survived / 76 killed-by-test / 9
killed-by-typecheck, --verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* fix: add the archive-rename to the receipt's change_set — CHANGE_SET_INCOMPLETE

The prior archival commit's receipt still only listed its original 4 paths;
the rename itself (receipts/graph/change-admission-MUN-node-pin-...json ->
receipts/archive/node-pin-...json) is a 5th changed path in the range that
the receipt didn't cover. Added it (kind=receipt, status=R). Regenerated
evidence for the same tracked-tree reason as every commit on this branch:
0 survived / 76 killed-by-test / 9 killed-by-typecheck, --verify-structure
rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* fix: correct the truncated commit sha in the receipt's change_set.head

Wrote a 12-char abbreviation (4cef876) instead of the full 40-char sha
-- schema_check.py didn't catch it (no format constraint on that field), but
the range-membership check does exact/canonical comparison, not prefix
resolution: RECEIPT_MISSING, 0/7 candidates bound. Full sha now.

Regenerates evidence once more for the same reason as every commit on this
branch: 0 survived / 76 killed-by-test / 9 killed-by-typecheck,
--verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

* chore(receipts): archive #79's own receipt too — same discovery-poisoning pattern

#79 (critical next.js RCE) merged into main via squash, and its receipt
(change-admission-next-critical-rce-...json) was never archived -- same
oversight as #80's. Squash-merging also disconnects the receipt's declared
head (397a538) from main's real ancestry, so even a same-day archival
attempt referencing it correctly would still read as unbound; archiving it
out of the discovery globs sidesteps that entirely, same as the MUN-node-pin
fix earlier on this branch.

Updated this branch's own receipt: dropped the now-irrelevant
receipts/archive/node-pin-... entry from change_set (that rename is no
longer part of the diff against a shifted base) and added this new rename.
Kept change_set.base at c048c13 -- the commit the vendored graph was
actually built against (graph.source_commit / graph_digest must match it;
changing base to CI's now-later computed base without rebuilding the graph
trips RECEIPT_ON_STALE_GRAPH). The range-membership check only requires
receipt.head to be an ancestor of the current HEAD, not that declared base
equals whatever CI happens to compute.

Regenerates evidence once more: 0 survived / 76 killed-by-test / 9
killed-by-typecheck, --verify-structure rc 0.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NFNpX4JV82FtEi993EiUFY

---------

Co-authored-by: Claude Sonnet 5 <[email protected]>
Arcanada added a commit that referenced this pull request Sep 14, 2026
…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.
Arcanada added a commit that referenced this pull request Sep 14, 2026
…ESM move dropped (#114)

* fix: stay on the Node LTS line, and restore the mutation defence the 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.

* receipt(#103): file the ChangeAdmissionReceipt/v1 for the Node LTS pin

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.
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