Files
openrig/scripts/gate-lane-run.mjs
v-openrig-build a5a1198f04 gate: the web-UI leg is ruled out — the gate runs typecheck + vitest only
Founder ruling 2026-08-21: the web UI (packages/ui) went best-effort experimental
at the 0.5.0 TUI pivot; releases must not spend gate time or block on it. The
vitest:ui leg is removed, its exclusion-ledger resident (table-crash STRESS)
dies with it, and the absence is pinned by a test so it reads as ruled, not
forgotten. npm run test:ui remains for manual runs; it gates nothing.

(cherry picked from commit f12db236a3a9d696956b07ac08cbf0f439425cb9)
2026-08-22 10:11:58 -07:00

135 lines
7.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// F1 gate-lane runner LOGIC (arch d6a6c1db, mechanism (B), 5 pins). Pure/injected so the teaching text,
// the honesty-gap leg set, and the C2 verdict are unit-tested; the CLI entrypoint (gate-lane.mjs) wires
// the real mutex + subprocess exec + artifact write around these.
import { rmSync } from "node:fs";
import { join } from "node:path";
import { resolveGateWithLedger, renderLedgerState } from "./gate-lane-ledger.mjs";
/**
* SOURCE-TRUTH hygiene: remove the vendored daemon bundle at packages/cli/daemon before the legs run.
* That path is a GITIGNORED build artifact assembled by `npm run build:package`; a stale desk leftover
* there poisons test:repo's freshness guard (the guard correctly flags any assembled-but-stale bundle,
* but the gate is not a package-time context — it tests SOURCE truth). Removing it at gate start means a
* leftover can never poison a run; real package-time assembly is still guarded, and a fresh clone with no
* bundle is a `force:true` no-op. `rm` is injected for testability. Returns the removed path.
*/
export function cleanStaleVendoredBundle(repoRoot, rm = rmSync) {
const vendored = join(repoRoot, "packages", "cli", "daemon");
rm(vendored, { recursive: true, force: true });
return vendored;
}
/**
* P5 — the refusal teaching text. ALWAYS hard-refuses; ALWAYS teaches the port constant. A gate holder is
* NAMED (pid/started-at); a foreign squatter is HONEST-UNKNOWN (never a fabricated holder).
*/
export function renderRefusal(result, port) {
const head = `⛔ gate lane BUSY — lock = localhost port ${port} (OPENRIG_GATE_LANE_PORT). REFUSING (non-blocking, exit nonzero).`;
if (result.reason === "gate-holder" && result.holder) {
return (
`${head}\n` +
` held by another GATE: pid ${result.holder.pid}, started ${result.holder.startedAt}.\n` +
` Another full gate lane is running on this machine — wait for it or inspect that pid.`
);
}
return (
`${head}\n` +
` held by a FOREIGN process — holder UNKNOWN (no gate holder-info file).\n` +
` Fail-closed (load-115): a gate lane must not run beside unknown load. Free port ${port} or inspect what holds it.`
);
}
/**
* The gate runs BOTH legs — typecheck (lint) AND vitest (test: repo scripts + the supported
* workspaces: daemon, cli, tui). All legs run (not fail-fast) so the verdict records every result;
* green = every leg ok. `exec(cmd)` is injected → `{ok, code}`.
*
* The web UI (packages/ui) is deliberately NOT a gate leg. Founder ruling 2026-08-21: the web UI
* went best-effort experimental at the 0.5.0 TUI pivot and releases must not spend gate time or
* block on it. `npm run test:ui` still exists for manual runs; it gates nothing.
*/
export async function runLegs(exec) {
const specs = [
{ name: "typecheck", cmd: "npm run lint" }, // tsc ×4 (incl. P9 typecheck:prep)
{ name: "vitest", cmd: "npm run test" }, // test:repo + supported workspaces (no packages/ui)
];
const legs = [];
for (const s of specs) {
// Per-leg wall-clock (ms) — the discriminator a whole-run total hides: a MIXED-mode run (some legs
// real, some smoked/skipped) still sums into a plausible band, but the skipped legs show ~0ms here.
const t0 = Date.now();
const r = await exec(s.cmd);
const durationMs = Date.now() - t0;
legs.push({ name: s.name, cmd: s.cmd, ok: !!r.ok, code: r.code ?? null, durationMs });
}
return legs;
}
/**
* Advisory foreign-load context (arch: foreign NON-gate load cannot be locked, only OBSERVED → advisory
* warn, exit code UNCHANGED, RECORDED in the verdict). Counts foreign toolchain processes (node/vitest/
* tsc) — never the gate's own pid — plus loadavg. Injected reader (loadavg, processes) for testability.
*/
export function observeForeignLoad({ loadavg, processes }) {
const foreign = processes.filter((p) => p.pid !== process.pid && /\b(node|vitest|tsc)\b/.test(p.command));
const advisory = [];
if (foreign.length > 0) advisory.push(`${foreign.length} foreign node/vitest/tsc process(es) running during the gate`);
advisory.push(`loadavg ${loadavg.map((n) => n.toFixed(2)).join(" ")}`);
return { advisory, foreignProcessCount: foreign.length, loadavg };
}
/**
* C2-style verdict: a green carries the load context it ran under (RECORDED, not just printed) — a green
* is only as good as what could have been red. The gate is resolved AGAINST THE EXCLUSION LEDGER (F1
* 4 rails): pass iff every failed leg is COVERED by an active, valid, unexpired resident AND no resident
* is expired/invalid. With the shipped EMPTY seed the ledger holds no one, so this collapses to strict
* "pass iff every leg ok" — the ledger state (0 exclusions) is still recorded in-band, loud.
*/
export function buildVerdict({ legs, foreignLoad, startedAt, endedAt, ledger = [], now, cutCeiling, smoke = false }) {
const failures = legs.filter((l) => !l.ok).map((l) => l.name);
const ledgerResult = resolveGateWithLedger({
failures,
ledger,
now: now ?? endedAt,
...(cutCeiling ? { cutCeiling } : {}),
});
return {
gate: ledgerResult.gate,
// SELF-DESCRIBING: the mode this verdict ran under, taken from the SINGLE choice point — the caller
// (gate-lane.mjs) computes SMOKE ONCE to pick the executor (:56) and passes THAT same value here. It
// is deliberately NOT a second, independent `process.env` read: buildVerdict never sees the injected
// executor, so reading the env here would record INTENT that can disagree with what actually ran (a
// smoking exec under an unset env → smoke:false over legs that never executed). Two reads of the same
// flag can diverge; one cannot. The per-leg durationMs below is the INDEPENDENT observed cross-check:
// declared mode vs observed effect — if they ever disagree, that disagreement is itself detectable.
// Without this a smoke run seals a normal PASS indistinguishable from a real one; hash-verifying the
// JSON proves the file authentic, never that the gate RAN.
smoke: smoke === true,
legs,
foreignLoad,
ledger: ledgerResult,
ledgerState: renderLedgerState(ledgerResult),
startedAt,
endedAt,
};
}
/**
* The gate WIRING, extracted so production and its test call ONE origin — not a mirror. The SINGLE
* `smoke` value BOTH picks the executor (the skip branch vs the injected real one) AND flows into the
* verdict, so smoke can never be set at one site and forgotten at the other (the two-origins failure).
* The real executor is INJECTED — a unit test can't spawn real npm, and this is the same exec-injection
* runLegs already uses — but the smoke→executor→verdict wiring lives HERE, once. gate-lane.mjs calls
* this to produce the sealed verdict; the test calls the SAME function, so its negative control guards
* the shipped path rather than a lookalike.
*/
export async function runGate({ smoke, realExec, foreignLoad, startedAt, ledger = [], cutCeiling }) {
const exec = smoke
? async (cmd) => { console.log(`[smoke] skip: ${cmd}`); return { ok: true, code: 0 }; }
: realExec;
const legs = await runLegs(exec);
const endedAt = new Date().toISOString();
return buildVerdict({ legs, foreignLoad, startedAt, endedAt, ledger, now: endedAt, cutCeiling, smoke });
}