Files
cliffhallandClaude Opus 5.5 727cf5ade0 feat: local:gate, the gate lease, boot smoke and the pre-push-gate skill
Adds `npm run local:gate`, the one command that runs every check CI runs
for both languages, under a machine-wide lease so gates started in
separate worktrees queue instead of contending.

- scripts/gate-lease.mjs: the lease, ported from the MCP Inspector.
- verify:install-fresh: the gate's first stage; node_modules must match
  package-lock.json.
- scripts/smoke-servers.mjs (`npm run smoke`): boots all seven servers
  from the artifact a user launches, over each transport they implement
  (stdio for all; SSE and Streamable HTTP for everything), and calls one
  tool. Also a CI job.
- verify:no-test-retries: no Vitest retry, no pytest rerun plugin.
- scripts/lib/workflow-gate.mjs: no workflow may invoke a local:* script,
  every CI job declares timeout-minutes, and local:gate stays exactly the
  lease wrapper around its stages.
- timeout-minutes on every job in all four workflows.
- Removes the last temporary wiring allowance from verify:skills.
- docs/quality-gate.md, the pre-push-gate skill with eval cases, and the
  AGENTS.md pre-push rule.

Per-file coverage is not part of this change: #4871 was rescoped so the
gate does not wait on the spec refactor's test suites. #4854 and #4855
add their coverage stages to the gate built here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-30 20:45:18 -04:00

167 lines
7.6 KiB
JavaScript

// Shared npm-script reachability helpers.
//
// Ported from the MCP Inspector (#4863); `inspector#N` references are that
// repo's issues and PRs, a bare `#N` is this repo's. `verify-skills.mjs` uses
// the vouch helpers (`scriptChainRuns`, `rootReachesScript`) today; the coverage
// guards #4864 ports (`verify-format-coverage.mjs`,
// `verify-typecheck-coverage.mjs`) reuse the harvesting half, and are why it is
// shared: the wiring logic they all depend on — "is this gate actually run?" —
// must not drift between them.
/**
* Split a shell-ish command string into args, honoring single and double quotes
* (a quoted arg becomes one token with the quotes stripped). Used by both
* coverage guards so they parse a script's args — quoting is the house style
* here (a quoted prettier glob, `tsc -p "tsconfig.json"`) — the same way.
*/
export function tokenize(command) {
const tokens = [];
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
let m;
while ((m = re.exec(command)) !== null) {
tokens.push(m[1] ?? m[2] ?? m[3]);
}
return tokens;
}
/**
* Names of scripts transitively reachable from `entry` by following `npm run
* <name>` references within a single manifest's `scripts`, plus npm's implicit
* `pre<name>`/`post<name>` lifecycle hooks (npm runs those around `<name>`
* without an explicit `npm run`, so a gate moved into e.g. `prevalidate` is
* still reached). Every spelling npm accepts for `run` is followed —
* `run-script` is the canonical command and `run`, `rum` and `urn` are its
* aliases — so a chain written as `npm run-script validate:web` reaches the
* same scripts as `npm run validate:web` (Copilot, inspector#2341). A gate harvested from a script that nothing reachable from
* `entry` invokes gates nothing, so callers restrict to this set to assert "CI
* actually runs this", not merely "the script exists".
*/
export function reachableScripts(scripts, entry = "validate") {
const reached = new Set();
const queue = [entry];
const runRef = /npm (?:run-script|run|rum|urn) ([\w:-]+)/g;
while (queue.length > 0) {
const name = queue.shift();
if (reached.has(name)) continue;
reached.add(name);
// npm runs pre<name>/post<name> implicitly around <name>.
for (const hook of [`pre${name}`, `post${name}`])
if (typeof scripts?.[hook] === "string") queue.push(hook);
const cmd = scripts?.[name];
if (typeof cmd !== "string") continue;
for (const m of cmd.matchAll(runRef)) queue.push(m[1]);
}
return reached;
}
/**
* The lease wrapper `local:gate` runs under: `scripts/gate-lease.mjs` (#4871,
* from inspector#2339). The vouch helpers look through it, so a question about
* what `local:gate` runs is answered from the stages behind the wrapper.
*/
export const GATE_LEASE_WRAPPER = "node scripts/gate-lease.mjs ";
/** `node scripts/gate-lease.mjs npm run X` → `npm run X`; anything else unchanged. */
function unwrapGateLease(segment) {
return segment.startsWith(GATE_LEASE_WRAPPER)
? segment.slice(GATE_LEASE_WRAPPER.length).trim()
: segment;
}
/** The command strings of every script reachable from the root `validate`. */
function rootReachedCommands(rootScripts) {
return [...reachableScripts(rootScripts)]
.map((n) => rootScripts?.[n])
.filter((c) => typeof c === "string");
}
/**
* Whether the root `validate` chain runs a client's `validate` — either
* `cd <clientDir> && npm run validate` or `npm --prefix <clientDir> run
* validate`. Without this a per-client gate would still be harvested from that
* client's own `validate` and count as coverage even after the root chain
* stopped running it — the "gate silently stops gating" failure, one level up.
*/
export function rootRunsClientValidate(rootScripts, clientDir) {
// Anchored on both ends: an optional leading `./` (`cd ./clients/x` genuinely
// runs the client) and a boundary after the dir, so a prefix-sibling
// (`clients/x` vs `clients/x-next`) can't satisfy the check for the shorter
// name; and a boundary after `validate` so `run validate:fast` (a *different*
// script that may skip typecheck) doesn't count as running `validate`. A bare
// `includes`/`\b` on either would let a sibling silently vouch for a dropped
// client. Quotes are stripped first so `cd "clients/x"` still matches. Both
// `cd <dir> && npm run validate` and `npm --prefix <dir> run validate` count.
const escaped = clientDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const dirRe = new RegExp(`(?:cd|--prefix) \\.?/?${escaped}(?=$|[\\s&;])`);
return rootReachedCommands(rootScripts).some((c) => {
const stripped = c.replace(/["']/g, ""); // both halves see quotes stripped
return dirRe.test(stripped) && /\brun validate(?=$|[\s&;])/.test(stripped);
});
}
/**
* Whether `scriptName` is reachable from the root `validate`. A guard can't
* assert it is *itself* run (an unrun guard runs no check), but the two coverage
* guards can each assert the *other* is still wired — so dropping either from
* `validate` is caught by its sibling. Only deleting both slips through.
*/
export function rootReachesScript(rootScripts, scriptName) {
return scriptChainRuns(rootScripts, "validate", scriptName);
}
/**
* Whether `npm run <entry>` reaches a real invocation of `npm run <target>`.
*
* `reachableScripts` matches every `npm run …` **substring**, which is right for
* harvesting (over-inclusion is safe there) and wrong for a vouch: `echo npm run
* verify:skills` would satisfy it while the guard never executes (Copilot).
*
* Splitting is on `\n`, `;` and `&&` only — deliberately NOT on `|` or a bare
* `&`, because `||` is the failure-masking form and both of its shapes must be
* rejected: `npm run X || true` swallows the guard's own failure, and `true ||
* npm run X` never runs it. Leaving them inside a segment means the segment does
* not equal the invocation, so both are refused without special-casing either.
*
* A segment naming some OTHER script is followed, with or without trailing
* flags, as are npm's implicit `pre`/`post` hooks. Only the target is matched
* exactly, which suits the argument-less scripts a vouch asks about.
*
* One wrapper is transparent: `node scripts/gate-lease.mjs npm run X` runs
* `npm run X` under the local:gate lease (inspector#2339) and nothing else, so a vouch
* about what `local:gate` runs looks through it. It is matched by its exact
* leading tokens — a wrapper with flags, or any other wrapper, still hides the
* invocation, because this helper cannot know what an arbitrary wrapper does.
*
* @param {Record<string, string>} scripts
* @param {string} entry
* @param {string} target
*/
export function scriptChainRuns(scripts, entry, target) {
const seen = new Set();
const queue = [entry];
while (queue.length > 0) {
const name = queue.shift();
if (seen.has(name)) continue;
seen.add(name);
// npm runs pre<name>/post<name> implicitly around <name>.
for (const hook of [`pre${name}`, `post${name}`])
if (typeof scripts?.[hook] === "string") queue.push(hook);
const body = scripts?.[name];
if (typeof body !== "string") continue;
for (const segment of body
.split(/\n|;|&&/)
.map((part) => unwrapGateLease(part.trim()))) {
if (segment === `npm run ${target}`) return true;
const tokens = segment.split(/\s+/);
// An intermediate hop must be as unmasked as the target itself: in
// `npm run stage || true` (or `| tee`) the stage's failure is swallowed,
// so a target reached through it gates nothing (Copilot, #4895).
if (segment.includes("|")) continue;
if (tokens[0] === "npm" && tokens[1] === "run" && tokens[2]) {
queue.push(tokens[2]);
}
}
}
return false;
}