From Copilot's fifteenth pass of #1962. `installDirs` filtered on the presence
of a lockfile, so a missing one silently removed that install from the
comparison rather than failing. For the root — the install every shared source
resolves from — that meant the guard could report success from client locks
alone, which is the deny-by-default contract inverted.
Enrolment is now by `package.json` (an install we are meant to compare), with
the root always enrolled, and a missing lockfile is a named, loud failure. A
`clients/` directory with no `package.json` is not an install and is skipped,
so a stray scratch dir isn't asked for a lockfile it should never have.
Three regression cases: a missing client lockfile in a fixture that IS skewed
(so failing open would show as a pass), a missing ROOT lockfile, and a stray
`clients/` dir that must not be enrolled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
From Copilot's eleventh pass of #1962, and the most consequential kind of bug
for a gate: it failed *open*.
`topLevelLockVersions` returns an empty map for anything without a v2+
`packages` table — a lockfileVersion 1 file, or a malformed one. `main()` fed
that straight into the comparison, so such an install contributed no holders
at all, and a genuine skew among the remaining installs was reported as
aligned. The guard would have said OK to exactly what it exists to catch.
`hasReadableLockShape` now gates every discovered lockfile before any
comparison, requiring a `packages` object carrying the `""` root entry npm
always writes — "has a packages key" is not enough. An unreadable one is named
and exits 1, telling you to regenerate it. A JSON parse failure likewise
reports the file rather than dying on a bare SyntaxError with no path in it.
The empty-map behavior of the pure helper is kept and its test now says why it
is safe: the shape check rejects those inputs before they can reach
`findSkew`.
Three regression cases: a v1 lockfile in a fixture that IS skewed (so failing
open would show as a pass), a `packages` table with no root entry, and the
unit table for the predicate. Fixtures now write the `""` root entry, matching
real npm output.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
From Copilot's tenth pass of #1962. The suite covered only pure helpers, so a
regression in source enumeration, install discovery, lockfile loading, the
sibling-guard vouch, or the nonzero exit on real skew would have left it
green — and the negative cases had only ever been run by hand, once per review
round.
Five end-to-end cases build a throwaway repo and run the guard as a
subprocess: aligned installs exit 0; a skewed one exits 1 naming the package
and every holder while leaving the agreeing package unreported; a package
imported only by `test-servers/src` still fails (so dropping the second shared
tree can't pass); a shared source matching no tracked file fails; and a
`validate` chain that stopped running `verify:format-coverage` fails the vouch
even when the versions are fine.
The fixture must be realpath'd. On macOS `tmpdir()` is `/var/…`, a symlink to
`/private/var/…`, and the guard runs `main()` only when `import.meta.url` (the
resolved path) matches `process.argv[1]` — so launching it via the unresolved
path loads the module and does nothing, exiting 0 silently. That is how all
five first "passed" as no-ops, and the comment records it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3