Repository navigation
[quality] package.json declares no engines.node: the Node-major parity chain has no link npm reads #1182
Description
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/andgrep -rn 'NODE_VERSION:' .github/workflows/
produce the table above; every literal is22.grep -rn "engines" package.jsonreturns nothing;ls -ashows no.nvmrc.- This is not a live break. The full unit suite was run locally on
node v26.10.0 on 2026-10-08 at03cfcfe
(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 arescripts/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/:
-
package.jsongains"engines": { "node": "22.x" }, matching the major
the devcontainer and every workflow already pin. Advisory by default —
the repository ships no.npmrcwithengine-strict, so this warns a
contributor on the wrong major without failing anyone's install. -
tests/dev-environment.test.mjspins that field into the existing
parity chain, so a future Node bump that missespackage.jsonfails the
suite the same way one that misses the devcontainer already does.
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 existing22; 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
Activity