chore: address Copilot review round 1 (#2226)
- Include `optionalDependencies` in the declared tier's candidate union. A peer range constrains the consumer's host; an optional range is a direct declaration npm attempts to install, so omitting it left an optional-only package free to skew unseen. `peerDependencies` stays excluded, with the reason stated. - Make the end-to-end fixture's client copy UNDECLARED — only the root manifest names `solo`, while `clients/web` merely holds a top-level copy. That is #2226's defining shape, and it is what makes the candidate set a union across installs rather than a per-install intersection; the old fixture would have passed a per-install implementation. Adds the mirror case (declared only by a client, held by the root) so the union is pinned in both directions. - Update the two docs that still described the single-tier guard: `.claude/skills/local-dev/SKILL.md` said "nothing gates either of those, and `verify:dep-lockstep` is not it" and cited the live `@types/node` drift in the present tense; `docs/quality-gate.md` described only the program tier. Both now state what each tier covers and the two limits that remain. 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
1e6bf6425a
commit
8e2c4d49fa
3 changed files with 110 additions and 50 deletions
|
|
@ -528,16 +528,20 @@ test("main: exits 1 when the root validate no longer runs the sibling guard", ()
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
// The exact shape the issue reported, and the client copy is deliberately
|
||||
// UNDECLARED (Copilot): only the ROOT manifest names `solo`, while
|
||||
// `clients/web` merely holds a top-level copy — the way cli's `@types/node`
|
||||
// arrives, hoisted via `@types/express` with no range of ours governing it.
|
||||
// That is what makes the candidate set a UNION across installs rather than a
|
||||
// per-install intersection, and declaring it in both manifests would let a
|
||||
// per-install implementation pass this test.
|
||||
//
|
||||
// Only the shared `core/` imports `solo`, so the client's program resolves the
|
||||
// root's copy alone and the program tier is silent — correctly. The declared
|
||||
// tier is the one that sees it, which is the whole reason it exists.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.0.0",
|
||||
},
|
||||
(dir) => {
|
||||
|
|
@ -553,10 +557,11 @@ test("main: a DECLARED package skewed across installs fails even when no program
|
|||
});
|
||||
|
||||
test("main: a declared package held at the same version in both installs passes and is counted", () => {
|
||||
// Same union shape as above — root-declared, client copy undeclared — so the
|
||||
// pass and the fail differ only in the version, not in how the name is found.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.8.9",
|
||||
},
|
||||
(dir) => {
|
||||
|
|
@ -567,6 +572,26 @@ test("main: a declared package held at the same version in both installs passes
|
|||
);
|
||||
});
|
||||
|
||||
test("main: the union runs the other way too — declared only by a CLIENT, held by the root", () => {
|
||||
// The mirror of the case above, and the reason `declaredPackages` unions every
|
||||
// install's manifest rather than reading the root's. `@types/react` is the
|
||||
// live shape: web and tui declare it, the root does not, and the two clients
|
||||
// disagreed. A root-manifest-only candidate set would report success here.
|
||||
withFixture(
|
||||
{
|
||||
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\)/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -278,18 +278,33 @@ export function toleratesSkew(name, holders, tolerated) {
|
|||
* Every package name any install declares, across `dependencies` and
|
||||
* `devDependencies`.
|
||||
*
|
||||
* `manifests` is `[{ dir, manifest }]`. Both fields count because the boundary
|
||||
* this tier polices is "does the repo name it", not "does it ship": a
|
||||
* devDependency skew is exactly the `@types/*` and toolchain case #2226 is
|
||||
* about. `peerDependencies` and `optionalDependencies` are NOT declarations of
|
||||
* what this repo installs — a peer range is a constraint on the consumer, and
|
||||
* the copy npm auto-installs to satisfy one is caught anyway, because it lands
|
||||
* top-level in an install whose sibling declares the same name.
|
||||
* `manifests` is `[{ dir, manifest }]`. All three fields count because the
|
||||
* boundary this tier polices is "does the repo name a package npm will install
|
||||
* for us", not "does it ship":
|
||||
*
|
||||
* • `devDependencies` — the `@types/*` and toolchain case #2226 is about; every
|
||||
* one of the four skews this tier first surfaced was a devDependency
|
||||
* somewhere.
|
||||
* • `optionalDependencies` — a direct declaration npm attempts to install like
|
||||
* any other. Omitting it would leave an optional-only package free to hold
|
||||
* conflicting top-level copies in two installs while this tier reported
|
||||
* success (Copilot, #2226).
|
||||
*
|
||||
* `peerDependencies` is the one exclusion, and it is not an oversight: a peer
|
||||
* range is a constraint the consumer places on its *host*, not a statement that
|
||||
* this repo installs the package. The copy npm auto-installs to satisfy an unmet
|
||||
* peer is caught anyway whenever some manifest declares that name — which is the
|
||||
* `eslint`/`typescript`/`vitest` shadow case, since all three are root
|
||||
* devDependencies.
|
||||
*/
|
||||
export function declaredPackages(manifests) {
|
||||
const names = new Set();
|
||||
for (const { manifest } of manifests)
|
||||
for (const field of ["dependencies", "devDependencies"])
|
||||
for (const field of [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"optionalDependencies",
|
||||
])
|
||||
for (const name of Object.keys(manifest?.[field] ?? {})) names.add(name);
|
||||
return names;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,10 +413,12 @@ test("majorOf: prerelease and build metadata are irrelevant", () => {
|
|||
// The declared tier (#2226). One case per rule, same convention as above.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("declaredPackages: dependencies and devDependencies from every install", () => {
|
||||
// devDependencies count because the boundary is "does the repo name it", not
|
||||
// "does it ship" — `@types/node` and the toolchain are exactly the case #2226
|
||||
// is about, and all of them are devDependencies.
|
||||
test("declaredPackages: dependencies, devDependencies and optionalDependencies from every install", () => {
|
||||
// devDependencies count because the boundary is "does the repo name a package
|
||||
// npm installs for us", not "does it ship" — `@types/node` and the toolchain
|
||||
// are exactly the case #2226 is about, and all of them are devDependencies.
|
||||
// optionalDependencies count for the same reason: npm attempts to install one
|
||||
// like any other direct declaration (Copilot, #2226).
|
||||
assert.deepEqual(
|
||||
[
|
||||
...declaredPackages([
|
||||
|
|
@ -426,28 +428,24 @@ test("declaredPackages: dependencies and devDependencies from every install", ()
|
|||
manifest: {
|
||||
dependencies: { "react-dom": "^19.2.4" },
|
||||
devDependencies: { "@types/react": "^19.2.14" },
|
||||
optionalDependencies: { fsevents: "^2.3.3" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
].sort(),
|
||||
["@types/react", "react", "react-dom"],
|
||||
["@types/react", "fsevents", "react", "react-dom"],
|
||||
);
|
||||
});
|
||||
|
||||
test("declaredPackages: peer and optional ranges are not declarations", () => {
|
||||
// A peer range constrains the consumer rather than naming what we install;
|
||||
// the copy npm auto-installs to satisfy one is still caught, because it lands
|
||||
// top-level in an install whose sibling declares the same name.
|
||||
test("declaredPackages: a peer range is not a declaration", () => {
|
||||
// A peer range constrains the consumer's HOST rather than naming what this
|
||||
// repo installs; the copy npm auto-installs to satisfy an unmet one is still
|
||||
// caught whenever some manifest declares that name, which is the
|
||||
// eslint/typescript/vitest shadow case (all three are root devDependencies).
|
||||
assert.deepEqual(
|
||||
[
|
||||
...declaredPackages([
|
||||
{
|
||||
dir: ".",
|
||||
manifest: {
|
||||
peerDependencies: { react: "^19.0.0" },
|
||||
optionalDependencies: { fsevents: "^2.3.3" },
|
||||
},
|
||||
},
|
||||
{ dir: ".", manifest: { peerDependencies: { react: "^19.0.0" } } },
|
||||
]),
|
||||
],
|
||||
[],
|
||||
|
|
|
|||
Loading…
Reference in a new issue