The audit gate walked 4 of 6 lockfiles

The two it skipped were not idle. They held seven advisories, six of them high severity, and the gate reported green the whole time.

Coverage, before and after

tracked lockfilewalked beforeadvisories foundnow
package-lock.json (root)yes0walked
docs/site/yes0walked
src/hooks/yes1 (fixed in #3272)walked
src/mcp-server/yes6 (fixed in #3272)walked
orchestkit-demos/NO6 (5x brace-expansion HIGH, fast-uri HIGH)walked
packages/hook-contract/examples/generic-client/NO1 (postcss)walked
Why this is worse than a red build

An unaudited workspace does not report "unknown". It contributes nothing to the failure count, so the summary line prints all-green and a reader concludes the tree is clean. The gate was not wrong about what it checked; it was silent about what it did not check.

How the gap surfaced

Dependabot saw what the gate could not

Dependabot reports per manifest, so it flagged orchestkit-demos and the hook-contract example client independently. The gate walks a hand-written list of directories. The two views disagreed, and the disagreement was the signal: after #3272 cleared src/mcp-server, the gate went green while Dependabot still showed three open alerts. That mismatch is what exposed the missing entries.

The fix, in two parts

$ npm update --no-audit --no-fund     # in-range only, no --force

orchestkit-demos                 added 516 packages in 39s
  1123896 brace-expansion HIGH        cleared
  1130589 brace-expansion HIGH        cleared
  1130591 brace-expansion HIGH        cleared
  1130734 brace-expansion HIGH        cleared
  1130736 brace-expansion HIGH        cleared
  1130720 fast-uri        HIGH        cleared

hook-contract/examples/generic-client   added 151 packages in 8s
  1130709 postcss         moderate    cleared

remaining: none
Same mechanism as #3272

Every fix already existed inside a range the manifests permitted. No manifest edits, no overrides, no allowlist entries, nothing semver-major.

The list stays hand-written (readable, intentional), but the script now
asserts that it covers every tracked lockfile:

  AUDITED_DIRS=()                        # appended by audit_project
  ...
  git ls-files '*package-lock.json'      # source of truth
    -> any lockfile whose directory was never passed to
       audit_project fails the run and is named by path

Negative test, run in place:

  $ (remove the orchestkit-demos line)
  $ bash tests/security/test-npm-audit.sh
    audit coverage gap: tracked lockfile(s) never audited
        orchestkit-demos/package-lock.json
    exit 1                               <-- assertion fires

Restored, full run:

  Total: 6  |  Passed: 6  |  Failed: 0
  exit 0
Fail-closed by design

A workspace added tomorrow with no audit_project line turns the build red and names itself, instead of joining the silent-pass set. The assertion also caught its own first bug: the root lockfile's dirname is ., which built $PROJECT_ROOT/. and failed a literal comparison against $PROJECT_ROOT, so root was falsely reported as uncovered.

Verification actually run

checkresult
full gate, all six workspacesTotal 6 | Passed 6 | Failed 0, exit 0
negative test (one entry removed)exit 1, names the missing lockfile
bash -n syntax checkclean
files changed2 lockfiles + the gate script
One caveat kept on the record

An earlier negative test appeared to show the assertion never firing. That harness was wrong, not the gate: it copied the script to /tmp, and PROJECT_ROOT is derived from SCRIPT_DIR, so the copy pointed at the wrong tree entirely. Re-run in place, it fires correctly. Recorded because a test that silently proves nothing is the same failure class this whole PR is about.