Repository navigation
Guard the two manifests against dependency drift #160
Copy link
Copy link
Closed
Labels
enhancementNew feature or requestNew feature or requestpriority: highMust be addressed in current sprintMust be addressed in current sprint
Description
User story / Problem statement
Once the root manifest declares the service's runtime dependencies, the same
packages are named in two files. registry/ stays independently installable
with its own lockfile and its own CI job, so the duplication is deliberate, but
nothing stops the two lists from diverging. A range bumped in
registry/package.json and not at the root means every suite runs against one
version of express while every consumer resolves another, and nothing anywhere
reports it. The failure surfaces in a downstream repository, at runtime.
Expected outcome
A single command fails, naming the package and both ranges, whenever the two
manifests disagree.
Acceptance criteria
-
npm run check:depsexists and runsscripts/check-registry-deps.mjs - It fails when a package in
registry/package.json'sdependenciesis
missing from the rootdependencies - It fails when a package named in both manifests carries different version
ranges, in any section on either side - The failure message names the package, both ranges, both sections, and
which file to edit - It exits 0 on the current tree and reports how many packages it compared
- Every rule is demonstrated by a mutation that reddens it, not by a green
run alone
Alternatives considered
- npm workspaces, so there is only one list. Rejected: a consumer installing
a git dependency gets that package's own dependencies and nothing else, so a
workspace root would still need the runtime dependencies declared where the
tarball can carry them. The duplication does not go away. - One manifest, dropping
registry/package.json. Rejected:registry/is
independently installable and testable today, its CI job depends on that, and
collapsing it would drag the whole test toolchain (vitest, supertest, biome,
tsx) into the consumer-facing manifest. - Generate the root's
dependenciesfromregistry/package.jsonat build
time. Rejected: it makes a tracked file generated, so a reviewer can no
longer see the consumer's dependency list in the diff.
Technical notes
- The "identical ranges for anything named in both manifests" rule covers
typescript,@types/expressand@types/nodewithout needing a second list,
and it should deliberately compare across sections: a package that is a
runtime dependency of one manifest and a devDependency of the other still has
to carry the same range. - The root manifest is ESM once the bin work lands, so a
.mjsscript is the
right shape and matchesscripts/seed.mjs. - Verify by mutation, at least three: drift a shared range; delete a runtime
dependency from the root; add a package to the root thatregistry/carries at
a different range in a different section. A guard that only reddens on the case
its author had in mind is worth less than the run that proves otherwise. - When mutating, stage the good manifest with
git add package.jsonfirst, so
thatgit checkout -- package.jsonrestores it from the index rather than from
HEAD, which would also discard the new script entry and quietly leave the
mutation half-reverted.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestpriority: highMust be addressed in current sprintMust be addressed in current sprint
Type
Projects
- StatusShow more project fieldsDone
Activity
lmcorbalan commentedon Sep 2, 2026
Two corrections found while doing #159, both of which change what the guard has to compare.
1. Comparing manifest ranges is not enough: it has to compare resolutions.
Two lockfiles can hold the same caret range and resolve to different versions, and that is exactly how the two trees drifted. Both manifests declared
@types/nodeat^26.1.1while the root resolved26.4.1andregistry/resolved26.1.1. The fix (93477b9, in #158's review round) touched onlypackage-lock.jsonand neither manifest, so a guard readingdependenciesblocks alone was green on both sides of the drift and reported nothing.The current acceptance criteria only ever compare ranges, so as written the guard cannot detect the one real drift this repository has actually had. Suggested addition:
A fourth mutation follows from it: edit a resolved
versionin one lockfile only, leaving both manifests untouched, and watch it redden. A range mutation cannot stand in for it.2. The two manifests now share
enginesandtype, and nothing compares them.#159 gave the root manifest
"type": "module"and"engines": { "node": ">=20" }, both of whichregistry/package.jsonalso carries. The root's copies are the ones that govern the package a consumer installs, and they can now disagree silently. That is not hypothetical: #159's own node-floor fix had to edit the floor in both files by hand, and nothing would have complained if it had changed only one.Both manifests are already parsed by the script, so this costs nothing:
engines.nodeortypediffer between the two manifestsNeither correction changes the shape of the script or any of the rejected alternatives; they widen what it reads.
lmcorbalan commentedon Sep 2, 2026
A third correction from #159's review round, on top of the two above: the packages that actually differ between the two trees today are named in neither manifest, so a guard over declared packages reports green on all of them.
Comparing the two lockfiles entry by entry at the same path gives five disagreements:
registry/fast-urimedia-typermulterprocess-warningjs-yamlFour of the five are transitive and appear in no
dependenciesordevDependenciesblock on either side. They all arrive under runtime dependencies:fast-uriunderajv,media-typerandmulterunderexpress-openapi-validator,process-warningunderpino. They are part of what the shipped bin loads.The fifth needs care, because a naive same-path comparison gets it wrong in both directions.
registry/package.jsondeclaresjs-yaml ^5.2.1as a test-only devDependency, soregistry/'s top-leveljs-yamlis 5.2.1 while the root's top-level entry is the copy hoisted out of@apidevtools/json-schema-ref-parser. Those two entries are different packages doing different jobs, and flagging them is a false alarm. The real disagreement is one level down: the spec parser thatexpress-openapi-validatorboots with resolvesjs-yaml4.3.2 in the root tree and 4.3.0 inregistry/, the latter nested atnode_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml.So the comparison that holds is per-dependent over the runtime closure, not per top-level path and not over the declared set:
This collides with an existing acceptance criterion. "It exits 0 on the current tree" and a closure comparison cannot both hold as things stand, because the drift above is present right now. Whichever way that is resolved is a decision this issue has to make: regenerate one of the lockfiles so the closures agree before the guard lands, or narrow the comparison to a subset and say in the failure message what is deliberately not compared.