chore(scripts): compare only the copies a program actually met

Two findings from the Copilot review of #1965, both real.

1. The candidates were flattened to package names before the version
   comparison, so a third install holding a different copy failed the guard
   even when no program loads that copy beside another — and the diagnostic
   named an install that never took part. `crossInstallPackages` now returns
   the co-occurrences themselves (name → program → install → entry paths) and
   `findSkew` compares per program, across only the installs that met there.

2. Folding a nested copy onto its outermost install lost the path that
   identifies which copy was loaded, and the version was then read from the
   install's top-level entry — absent or differently versioned, either way
   letting a real pair pass. `classifyModulePath` now also returns the
   `entryPath` npm keys the lockfile by, `topLevelLockVersions` becomes
   `lockVersionsByPath`, and each holder is priced from the entry the program
   resolved. A resolved copy with no lockfile entry is now a hard failure
   rather than a silently dropped holder.

The failure output prints `in <program>` with each holder's install path
beside its version, so a nested copy is distinguishable from a top-level one.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 12:35:38 -04:00
commit a9b9a917e3

View file

@ -44,14 +44,18 @@ const typescriptDir = path.dirname(
),
);
/** A lockfileVersion 3 lockfile, including the `""` root entry npm always writes. */
/**
* A lockfileVersion 3 lockfile, including the `""` root entry npm always writes.
* A key already containing `node_modules/` is used verbatim, so a nested install
* path (`node_modules/outer/node_modules/inner`) can be expressed.
*/
const lock = (deps) => ({
lockfileVersion: 3,
packages: {
"": { name: "fixture" },
...Object.fromEntries(
Object.entries(deps).map(([name, version]) => [
`node_modules/${name}`,
name.includes("node_modules/") ? name : `node_modules/${name}`,
{ version },
]),
),
@ -76,6 +80,8 @@ function makeFixture({
webScripts,
rawWebLock,
webTsconfig,
rootNestedInner,
cliDeps,
} = {}) {
// realpath matters: on macOS `tmpdir()` is `/var/...`, a symlink to
// `/private/var/...`. The guard only runs `main()` when `import.meta.url`
@ -161,9 +167,54 @@ function makeFixture({
}
stub("", root, "solo", "export type Solo = string;\n");
write("package-lock.json", lock(root));
// 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,
// and the case that must still be priced from the entry the program resolved.
const rootLockDeps = { ...root };
if (rootNestedInner) {
stub(
"node_modules/outer/",
{ inner: rootNestedInner },
"inner",
"export type Inner = number;\n",
);
rootLockDeps["node_modules/outer/node_modules/inner"] = rootNestedInner;
}
write("package-lock.json", lock(rootLockDeps));
write("clients/web/package-lock.json", rawWebLock ?? lock(web));
// 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.
if (cliDeps) {
write("clients/cli/package.json", {
name: "cli",
scripts: { typecheck: "tsc --noEmit -p tsconfig.json" },
});
write("clients/cli/tsconfig.json", {
compilerOptions: {
noEmit: true,
module: "esnext",
target: "esnext",
moduleResolution: "bundler",
types: [],
},
include: ["src/**/*.ts"],
});
write(
"clients/cli/src/main.ts",
'import type { Outer } from "outer";\nexport type FromCli = Outer;\n',
);
stub(
"clients/cli/",
cliDeps,
"outer",
'import type { Inner } from "inner";\nexport type Outer = Inner;\n',
);
stub("clients/cli/", cliDeps, "inner", "export type Inner = number;\n");
write("clients/cli/package-lock.json", lock(cliDeps));
}
// The guard resolves its repo root from its own location, so it has to live
// inside the fixture; its `lib/` imports come along.
mkdirSync(path.join(dir, "scripts", "lib"), { recursive: true });
@ -236,10 +287,10 @@ test("main: exits 1 and names the skewed package and every holder", () => {
const { status, out } = runGuard(dir);
assert.equal(status, 1, out);
assert.match(out, /\bouter\b/);
assert.match(out, /1\.0\.0\s+\(\.\)/);
assert.match(out, /1\.2\.3\s+\(clients\/web\)/);
assert.match(out, /1\.0\.0\s+\(\.\/node_modules\/outer\)/);
assert.match(out, /1\.2\.3\s+\(clients\/web\/node_modules\/outer\)/);
// The program that saw both copies is named, so the claim is checkable.
assert.match(out, /seen in clients\/web\/tsconfig\.json/);
assert.match(out, /in clients\/web\/tsconfig\.json/);
// `inner` agrees, so it must not be reported.
assert.doesNotMatch(out, /^\s+inner$/m);
},
@ -263,8 +314,8 @@ test("main: a package reached only through another package's .d.ts is caught (#1
const { status, out } = runGuard(dir);
assert.equal(status, 1, out);
assert.match(out, /^\s+inner$/m);
assert.match(out, /4\.0\.0\s+\(\.\)/);
assert.match(out, /4\.5\.6\s+\(clients\/web\)/);
assert.match(out, /4\.0\.0\s+\(\.\/node_modules\/inner\)/);
assert.match(out, /4\.5\.6\s+\(clients\/web\/node_modules\/inner\)/);
},
);
});
@ -284,6 +335,45 @@ test("main: a package that reaches the program from ONE install is not a candida
);
});
test("main: a third install's copy is not dragged into a comparison it never joined (Copilot, #1965 r1)", () => {
// `clients/cli` holds `outer` at a different version, but its own program is
// the only one that loads that copy and no second install meets it there.
// Flattening the candidates to names would compare cli against web's aligned
// pair and fail — naming an install that never took part.
withFixture(
{ cliDeps: { ...ALIGNED, outer: "9.9.9", inner: "9.9.9" } },
(dir) => {
const { status, out } = runGuard(dir);
assert.equal(status, 0, out);
assert.doesNotMatch(out, /clients\/cli/);
},
);
});
test("main: a nested copy is priced from its own lockfile entry (Copilot, #1965 r1)", () => {
// The root reaches `inner` only through `node_modules/outer/node_modules/inner`
// at 9.9.9, while its top-level entry still reads 4.5.6 — the version web
// holds. Pricing the copy from the top-level entry would report the pair as
// agreeing and let a real 9.9.9-vs-4.5.6 program through.
withFixture(
{
rootDeps: { ...ALIGNED, outer: "1.0.0" },
webDeps: ALIGNED,
rootNestedInner: "9.9.9",
},
(dir) => {
const { status, out } = runGuard(dir);
assert.equal(status, 1, out);
assert.match(out, /^\s+inner$/m);
assert.match(
out,
/9\.9\.9\s+\(\.\/node_modules\/outer\/node_modules\/inner\)/,
);
assert.match(out, /4\.5\.6\s+\(clients\/web\/node_modules\/inner\)/);
},
);
});
test("main: exits 1 when a client names no tsconfig project", () => {
// No `typecheck` script and no `tsconfig.json` references: none of that
// client's programs is measured, so a skew reaching only them would pass.