What
36 package.json files declare "engines" twice. Found incidentally while bumping axios (#754) — npm normalised one manifest down to a single block, which is what exposed it.
$ for f in $(git ls-files '*package.json' | grep -v node_modules); do
n=$(grep -c '"engines"' "$f"); [ "$n" -gt 1 ] && echo "$n x $f"; done
All 36
FuzeQuality, api-client, apps-client, backend, backend/applications, backend/core, backend/security, billing-client, clock-app, custom-hostname-client, design-system, frontend, packages/auth, packages/billing-ui, packages/chat-client, packages/chat-ui, packages/config-ui, packages/feature-flags, packages/i18n, packages/i18n-translate, packages/onboarding-kit, packages/portal-admin-ui, packages/security, packages/selection-lists-ui, portal-client, sdk, services/billing-service, services/chat-service, services/email-service, services/notification-service, services/payment-service, services/provisioning-service, services/sms-service, shared, tests/e2e/billing-invoices, tests/selection-list-service
Why it is not currently breaking
Every duplicate pair is identical (node: ">=24.0.0", npm: ">=10.0.0"), so whichever copy a parser picks, the value is the same. Nothing misbehaves today.
Why it still matters
gate-toolchain cannot detect this. It does JSON.parse, which silently keeps the last occurrence — so the gate sees one well-formed value and passes. The gate whose entire job is policing engines is blind to engines being declared twice.
The hazard is divergence. The moment the two copies disagree — a partial sweep, a bad merge resolution, a tool that rewrites only the first block — behaviour depends on which copy the reader takes:
JSON.parse (the gate, most tooling) → last
- some streaming/strict parsers → first, or reject outright
npm → normalises to one, silently discarding the other
That is precisely the failure shape this repo's CLAUDE.md already documents twice: a check that passes while the underlying state is wrong. Duplicate keys are legal enough that nothing complains, and wrong enough that they will eventually bite.
Likely origin
The #646/#647 sweep added engines after "version" in manifests that lacked it. These files appear to have received a second block appended by another pass over the same worklist. Worth confirming from history rather than assuming — I have not traced it.
Suggested fix
- De-duplicate — collapse each to a single block. Mechanical;
npm pkg fix, or a scripted rewrite, or letting npm normalise each manifest.
- Teach
gate-toolchain to catch it. Parse the raw text for repeated top-level keys instead of relying solely on JSON.parse. Worth generalising beyond engines — any duplicated top-level key in a manifest is a latent version of this bug.
- Consider whether
gate-line-endings or a sibling formatting gate is the more natural home for a "manifests are well-formed" check.
Related
What
36
package.jsonfiles declare"engines"twice. Found incidentally while bumping axios (#754) — npm normalised one manifest down to a single block, which is what exposed it.All 36
FuzeQuality,api-client,apps-client,backend,backend/applications,backend/core,backend/security,billing-client,clock-app,custom-hostname-client,design-system,frontend,packages/auth,packages/billing-ui,packages/chat-client,packages/chat-ui,packages/config-ui,packages/feature-flags,packages/i18n,packages/i18n-translate,packages/onboarding-kit,packages/portal-admin-ui,packages/security,packages/selection-lists-ui,portal-client,sdk,services/billing-service,services/chat-service,services/email-service,services/notification-service,services/payment-service,services/provisioning-service,services/sms-service,shared,tests/e2e/billing-invoices,tests/selection-list-serviceWhy it is not currently breaking
Every duplicate pair is identical (
node: ">=24.0.0",npm: ">=10.0.0"), so whichever copy a parser picks, the value is the same. Nothing misbehaves today.Why it still matters
gate-toolchaincannot detect this. It doesJSON.parse, which silently keeps the last occurrence — so the gate sees one well-formed value and passes. The gate whose entire job is policingenginesis blind toenginesbeing declared twice.The hazard is divergence. The moment the two copies disagree — a partial sweep, a bad merge resolution, a tool that rewrites only the first block — behaviour depends on which copy the reader takes:
JSON.parse(the gate, most tooling) → lastnpm→ normalises to one, silently discarding the otherThat is precisely the failure shape this repo's CLAUDE.md already documents twice: a check that passes while the underlying state is wrong. Duplicate keys are legal enough that nothing complains, and wrong enough that they will eventually bite.
Likely origin
The #646/#647 sweep added
enginesafter"version"in manifests that lacked it. These files appear to have received a second block appended by another pass over the same worklist. Worth confirming from history rather than assuming — I have not traced it.Suggested fix
npm pkg fix, or a scripted rewrite, or letting npm normalise each manifest.gate-toolchainto catch it. Parse the raw text for repeated top-level keys instead of relying solely onJSON.parse. Worth generalising beyondengines— any duplicated top-level key in a manifest is a latent version of this bug.gate-line-endingsor a sibling formatting gate is the more natural home for a "manifests are well-formed" check.Related
enginessweep that most likely introduced it