Finding
The repository pins one Node major in four places and guards three of them.
The fourth — the only declaration npm itself reads — does not exist.
What is pinned at 03cfcfe:
| file |
pin |
.devcontainer/devcontainer.json |
"features": { "node": { "version": "22" } } |
.github/workflows/ci.yml:33,98,121,192 |
node-version: 22 |
.github/workflows/pr-queue-hygiene.yml:28 |
node-version: 22 |
architecture-submission.yml:18, import-architectures.yml:14, refresh-community-people.yml:13, refresh-radar-reports.yml:13, deploy-gh-pages.yml:17 |
NODE_VERSION: '22' |
CONTRIBUTING.md / developer docs |
prose Node prerequisite |
And what already guards them, in tests/dev-environment.test.mjs:
:364 — devcontainer Node major matches the Node major every workflow pins
:386 — the documented Node prerequisite matches the pinned Node major
That chain is deliberate and careful. It has one hole: package.json declares
no engines field, and the repository has no .nvmrc.
$ node -p "JSON.stringify(require('./package.json').engines)"
undefined
$ ls -a | grep -i nvm
$ echo $?
1
So every artifact that states the supported Node major is one a contributor
reads voluntarily — by opening the devcontainer, by reading CONTRIBUTING, or
by looking at a workflow. Nothing states it to the tool that actually installs
the dependencies. npm ci on any Node major prints no warning, and the
mismatch surfaces later as a test or build difference that CI cannot reproduce.
Evidence and provenance
main = 03cfcfe, checked 2026-10-08.
grep -rn 'node-version' .github/workflows/ and grep -rn 'NODE_VERSION:' .github/workflows/
produce the table above; every literal is 22.
grep -rn "engines" package.json returns nothing; ls -a shows no .nvmrc.
- This is not a live break. The full unit suite was run locally on
node v26.10.0 on 2026-10-08 at 03cfcfe
(TZ=UTC node tests/tools/coverage-report.mjs) and is green:
src files | 100.00 | 99.92 | 8984/8984 lines | 2545/2547 regions. The two
sub-100% files are scripts/lib/architecture-content.mjs (region 155) and
scripts/lib/svg-active-content.mjs (region 251), both already claimed by
open PRs. A four-major drift from CI therefore passes today — which is
precisely why the drift is invisible.
- This is a test-infrastructure finding, not a coverage gap: no source path
is under-tested, so it carries no coverage-gap priority.
Recommendation
One deliverable, two files, both outside .github/workflows/:
The guard belongs beside the two that already exist, reusing
devcontainerNodeMajor():
// package.json's engines field is the only Node pin npm itself reads. A
// contributor who clones outside the devcontainer and never opens
// CONTRIBUTING.md gets no other signal, so a bump that misses this field
// leaves `npm ci` silently endorsing a major CI never runs.
test('package.json engines.node matches the pinned Node major', () => {
const devcontainerMajor = devcontainerNodeMajor();
const engines = packageJson.engines?.node;
assert.ok(
engines,
'package.json declares no engines.node; npm has no Node pin to enforce',
);
const major = String(engines).match(/\d+/)?.[0];
assert.equal(
major,
devcontainerMajor,
`package.json engines.node (${engines}) pins a different Node major than the devcontainer (${devcontainerMajor})`,
);
});
Deliberately out of scope
- Adding
.nvmrc. It would be a fifth pin needing a fifth guard, and nvm
is not mentioned anywhere in the repository's documented setup. engines
is read by the package manager the repository already requires.
- Setting
engine-strict=true. That turns the warning into a hard install
failure for every contributor on a different major, which is a maintainer
policy call, not a testing gap.
- Bumping the pinned major. This issue aligns the fifth declaration with
the existing 22; it does not propose changing it.
Coordination
Touches package.json and tests/dev-environment.test.mjs. Checked against
every open hold-gated PR at the time of filing — #1161 (scripts/audit-gate.mjs),
#1163 and #1174 (tests/tools/e2e-coverage-report.mjs, tests/e2e-coverage-run.test.mjs),
#1165 (tests/architecture-content-mirror.test.mjs), #1166 (CONTRIBUTING.md),
#1168 and #1176 (scripts/lib/svg-active-content.mjs), #1171
(tests/tools/e2e-data-fixtures.cjs, tests/e2e-data-fixtures.test.mjs,
tests/e2e/fixtures/**), #1178 (tests/e2e/focus-trap-trigger-unmounted.spec.js) —
none of them touches either file. No workflow file is involved.
Priority
- Impact: medium (the one Node pin a tool enforces is absent, so the three
guarded pins can all be satisfied while an actual working copy runs a major
CI never exercises)
- Effort: low
Filed by quality agent (hold-gated mode)
🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: 03cfcfe
— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88
Finding
The repository pins one Node major in four places and guards three of them.
The fourth — the only declaration
npmitself reads — does not exist.What is pinned at
03cfcfe:.devcontainer/devcontainer.json"features": { "node": { "version": "22" } }.github/workflows/ci.yml:33,98,121,192node-version: 22.github/workflows/pr-queue-hygiene.yml:28node-version: 22architecture-submission.yml:18,import-architectures.yml:14,refresh-community-people.yml:13,refresh-radar-reports.yml:13,deploy-gh-pages.yml:17NODE_VERSION: '22'CONTRIBUTING.md/ developer docsAnd what already guards them, in
tests/dev-environment.test.mjs::364— devcontainer Node major matches the Node major every workflow pins:386— the documented Node prerequisite matches the pinned Node majorThat chain is deliberate and careful. It has one hole:
package.jsondeclaresno
enginesfield, and the repository has no.nvmrc.So every artifact that states the supported Node major is one a contributor
reads voluntarily — by opening the devcontainer, by reading CONTRIBUTING, or
by looking at a workflow. Nothing states it to the tool that actually installs
the dependencies.
npm cion any Node major prints no warning, and themismatch surfaces later as a test or build difference that CI cannot reproduce.
Evidence and provenance
main=03cfcfe, checked 2026-10-08.grep -rn 'node-version' .github/workflows/andgrep -rn 'NODE_VERSION:' .github/workflows/produce the table above; every literal is
22.grep -rn "engines" package.jsonreturns nothing;ls -ashows no.nvmrc.node v26.10.0 on 2026-10-08 at
03cfcfe(
TZ=UTC node tests/tools/coverage-report.mjs) and is green:src files | 100.00 | 99.92 | 8984/8984 lines | 2545/2547 regions. The twosub-100% files are
scripts/lib/architecture-content.mjs(region 155) andscripts/lib/svg-active-content.mjs(region 251), both already claimed byopen PRs. A four-major drift from CI therefore passes today — which is
precisely why the drift is invisible.
is under-tested, so it carries no coverage-gap priority.
Recommendation
One deliverable, two files, both outside
.github/workflows/:package.jsongains"engines": { "node": "22.x" }, matching the majorthe devcontainer and every workflow already pin. Advisory by default —
the repository ships no
.npmrcwithengine-strict, so this warns acontributor on the wrong major without failing anyone's install.
tests/dev-environment.test.mjspins that field into the existingparity chain, so a future Node bump that misses
package.jsonfails thesuite the same way one that misses the devcontainer already does.
The guard belongs beside the two that already exist, reusing
devcontainerNodeMajor():Deliberately out of scope
.nvmrc. It would be a fifth pin needing a fifth guard, and nvmis not mentioned anywhere in the repository's documented setup.
enginesis read by the package manager the repository already requires.
engine-strict=true. That turns the warning into a hard installfailure for every contributor on a different major, which is a maintainer
policy call, not a testing gap.
the existing
22; it does not propose changing it.Coordination
Touches
package.jsonandtests/dev-environment.test.mjs. Checked againstevery open hold-gated PR at the time of filing — #1161 (
scripts/audit-gate.mjs),#1163 and #1174 (
tests/tools/e2e-coverage-report.mjs,tests/e2e-coverage-run.test.mjs),#1165 (
tests/architecture-content-mirror.test.mjs), #1166 (CONTRIBUTING.md),#1168 and #1176 (
scripts/lib/svg-active-content.mjs), #1171(
tests/tools/e2e-data-fixtures.cjs,tests/e2e-data-fixtures.test.mjs,tests/e2e/fixtures/**), #1178 (tests/e2e/focus-trap-trigger-unmounted.spec.js) —none of them touches either file. No workflow file is involved.
Priority
guarded pins can all be satisfied while an actual working copy runs a major
CI never exercises)
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5n31| SHA:03cfcfe— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88