mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
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)
135 lines
7.2 KiB
JavaScript
135 lines
7.2 KiB
JavaScript
// 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 });
|
||
}
|