chore: widen verify:dep-lockstep to declared cross-install copies (#2226)
`clients/cli` resolved `@types/node` 24.13.1 against the root's 24.13.3. Neither client declares it — #2196 consolidated it to the root — so the cli copy arrived transitively via `@types/express` and was constrained by no range of ours. `verify:dep-lockstep` reported OK and was right to: it compares only packages that reach one `tsc` **program** from two installs, and no one program sees both copies. Fixes the instance and the class, which the issue notes are not exclusive. **The class.** The guard grows a second tier alongside the program-derived one, asking the weaker but broader question `AGENTS.md`'s "one version per install-crossing dependency" rule actually states: does a package this repo declares anywhere resolve to two versions across our installs at all? The candidate set is every name in any install's `dependencies`/`devDependencies` that more than one install holds a **top-level** copy of — 17 packages today. Nested copies are excluded: one exists because some dependency asked for a different version, so it is that dependency's range, not ours. Neither tier subsumes the other. The program tier sees a copy no manifest names (the `@modelcontextprotocol/sdk` case, arriving through another package's `.d.ts`). The declared tier sees a transitive copy no program loads (this issue), two *clients* disagreeing with no root copy involved (`@types/react`, web against tui), and the peer shadows `AGENTS.md` calls out as ungated — `eslint`, `typescript`, `vitest`. Deny-by-default, with its own empty `TOLERATED_DECLARED_SKEW` and the same within-a-major rule, now stated once in a shared `toleratesSkew` so the two tiers cannot drift. **The instance, and three others the new tier surfaced.** All four were patch-level: - `@types/node` — cli 24.13.1 vs root 24.13.3. Transitive, so it takes an `overrides` entry in `clients/cli` (the repo's sanctioned lever for a transitive pin), at the root's own `^24.12.4` rather than a frozen number. - `@types/react` — web 19.2.18 vs tui 19.2.17. - `react` — web 19.2.8 vs root/tui 19.2.7. - `@vitejs/plugin-react` — web 6.0.5 vs root 6.0.2. The last three are `npm update` in the stale install: it moves the lockfile within the declared range without widening a range, which matters for the root `react ^19.0.0` that `ink`'s externalized-bundle exemption depends on. No declaration or range changed. Tests: 4 end-to-end cases (the fixture gains manifest deps and a client-side `solo` install, so the skew is real and no program holds both copies) and 13 helper cases. Closes #2226 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J86a6vMcA9csPtdsg7ZcCZ Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ed2a188069
commit
1e6bf6425a
1 changed files with 583 additions and 27 deletions
|
|
@ -82,6 +82,9 @@ function makeFixture({
|
|||
webTsconfig,
|
||||
rootNestedInner,
|
||||
cliDeps,
|
||||
rootManifestDeps,
|
||||
webManifestDeps,
|
||||
webSolo,
|
||||
} = {}) {
|
||||
// realpath matters: on macOS `tmpdir()` is `/var/...`, a symlink to
|
||||
// `/private/var/...`. The guard only runs `main()` when `import.meta.url`
|
||||
|
|
@ -101,6 +104,7 @@ function makeFixture({
|
|||
|
||||
write("package.json", {
|
||||
name: "fixture",
|
||||
...(rootManifestDeps ? { dependencies: rootManifestDeps } : {}),
|
||||
scripts: scripts ?? {
|
||||
validate: "npm run verify:format-coverage && npm run verify:dep-lockstep",
|
||||
"verify:format-coverage": "node scripts/verify-format-coverage.mjs",
|
||||
|
|
@ -121,6 +125,7 @@ function makeFixture({
|
|||
);
|
||||
write("clients/web/package.json", {
|
||||
name: "web",
|
||||
...(webManifestDeps ? { devDependencies: webManifestDeps } : {}),
|
||||
scripts: webScripts ?? { typecheck: "tsc --noEmit -p tsconfig.json" },
|
||||
});
|
||||
write(
|
||||
|
|
@ -166,6 +171,20 @@ function makeFixture({
|
|||
stub(installRoot, versions, "inner", "export type Inner = number;\n");
|
||||
}
|
||||
stub("", root, "solo", "export type Solo = string;\n");
|
||||
// `solo` under the CLIENT install as well. Nothing in `clients/web/src`
|
||||
// imports it, so the program still resolves exactly one copy (the root's, via
|
||||
// the shared `core/`) — the declared tier's whole point is that it sees this
|
||||
// second copy anyway (#2226).
|
||||
const webLockDeps = { ...web };
|
||||
if (webSolo) {
|
||||
stub(
|
||||
"clients/web/",
|
||||
{ solo: webSolo },
|
||||
"solo",
|
||||
"export type Solo = string;\n",
|
||||
);
|
||||
webLockDeps.solo = webSolo;
|
||||
}
|
||||
|
||||
// The root reaches `inner` only through a copy NESTED under `outer`, at a
|
||||
// version its top-level entry does not carry. npm's own conflict resolution,
|
||||
|
|
@ -182,7 +201,7 @@ function makeFixture({
|
|||
}
|
||||
|
||||
write("package-lock.json", lock(rootLockDeps));
|
||||
write("clients/web/package-lock.json", rawWebLock ?? lock(web));
|
||||
write("clients/web/package-lock.json", rawWebLock ?? lock(webLockDeps));
|
||||
|
||||
// A second client, to prove a third install's copy is not dragged into a
|
||||
// comparison it never took part in. Its program spans only its own install.
|
||||
|
|
@ -503,3 +522,75 @@ test("main: exits 1 when the root validate no longer runs the sibling guard", ()
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The declared tier (#2226) — skew across installs that no ONE program sees.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("main: a DECLARED package skewed across installs fails even when no program holds both copies (#2226)", () => {
|
||||
// The exact shape the issue reported. `solo` is installed under the root and
|
||||
// under `clients/web`, and both manifests name it — but only the shared
|
||||
// `core/` imports it, so the client's program resolves the root's copy alone
|
||||
// and the program tier is silent, correctly. The declared tier is what sees
|
||||
// it, which is the whole reason it exists: cli's `@types/node` was hoisted in
|
||||
// via `@types/express` and no program ever met the root's copy.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.0.0",
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 1, out);
|
||||
assert.match(out, /1 declared dependency resolves/);
|
||||
assert.match(out, /7\.8\.9\s+\(\.\/node_modules\/solo\)/);
|
||||
assert.match(out, /7\.0\.0\s+\(clients\/web\/node_modules\/solo\)/);
|
||||
// The program tier must NOT have claimed it — its message names a program.
|
||||
assert.doesNotMatch(out, /in clients\/web\/tsconfig\.json/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("main: a declared package held at the same version in both installs passes and is counted", () => {
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.8.9",
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /1 declared dependencies agree/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("main: an installed package NO manifest declares is not a declared-tier candidate", () => {
|
||||
// `outer` and `inner` sit in both installs in every fixture, but no manifest
|
||||
// names them. The declared tier's boundary is what the repo declares, so its
|
||||
// candidate count is zero here — the program tier is what covers those.
|
||||
withFixture({}, (dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /0 declared dependencies agree/);
|
||||
});
|
||||
});
|
||||
|
||||
test("main: a declared package only ONE install holds cannot skew", () => {
|
||||
// `solo` is declared at the root and listed in the root lockfile only. One
|
||||
// holder is not an install-crossing dependency, so its version is nobody
|
||||
// else's business.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webDeps: { outer: "1.2.3", inner: "4.5.6" },
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /0 declared dependencies agree/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue