Skip to content

Guard the two manifests against dependency drift #160

Closed

Description

@lmcorbalan

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:deps exists and runs scripts/check-registry-deps.mjs
  • It fails when a package in registry/package.json's dependencies is
    missing from the root dependencies
  • 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 dependencies from registry/package.json at 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/express and @types/node without 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 .mjs script is the
    right shape and matches scripts/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 that registry/ 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.json first, so
    that git checkout -- package.json restores it from the index rather than from
    HEAD, which would also discard the new script entry and quietly leave the
    mutation half-reverted.

Activity

  1. moved this from Backlog to Ready in Canton - dAppBooster (#390)on Sep 2, 2026
  2. lmcorbalan commented on Sep 2, 2026

    @lmcorbalan
    CollaboratorAuthor

    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/node at ^26.1.1 while the root resolved 26.4.1 and registry/ resolved 26.1.1. The fix (93477b9, in #158's review round) touched only package-lock.json and neither manifest, so a guard reading dependencies blocks 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:

    • It fails when a package named in both manifests resolves to different versions in the two lockfiles, and the message names both resolved versions

    A fourth mutation follows from it: edit a resolved version in 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 engines and type, and nothing compares them.

    #159 gave the root manifest "type": "module" and "engines": { "node": ">=20" }, both of which registry/package.json also 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:

    • It fails when engines.node or type differ between the two manifests

    Neither correction changes the shape of the script or any of the rejected alternatives; they widen what it reads.

  3. lmcorbalan commented on Sep 2, 2026

    @lmcorbalan
    CollaboratorAuthor

    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:

    package root registry/
    fast-uri 3.1.7 3.1.3
    media-typer 1.1.1 1.1.0
    multer 2.3.0 2.2.0
    process-warning 5.1.0 5.0.0
    js-yaml 4.3.2 5.2.1

    Four of the five are transitive and appear in no dependencies or devDependencies block on either side. They all arrive under runtime dependencies: fast-uri under ajv, media-typer and multer under express-openapi-validator, process-warning under pino. 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.json declares js-yaml ^5.2.1 as a test-only devDependency, so registry/'s top-level js-yaml is 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 that express-openapi-validator boots with resolves js-yaml 4.3.2 in the root tree and 4.3.0 in registry/, the latter nested at node_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:

    • It walks the runtime dependency closure of both lockfiles and fails when the same dependent resolves a dependency to different versions in the two trees, naming the dependent, the dependency and both versions
    • A package that is a devDependency on one side only is not reported as drift

    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.

  4. moved this from Ready to In progress in Canton - dAppBooster (#390)on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: highMust be addressed in current sprint

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions