@rightkit/release 0.6.8 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +30 -12
  2. package/admission-files.mjs +4 -1
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +105 -27
  7. package/assemble-app-command.mjs +151 -0
  8. package/assembled-app.mjs +445 -0
  9. package/bin/darwin/rightkit-swap +0 -0
  10. package/bin/darwin/rightkit-swap.sign.json +6 -0
  11. package/bin/rightkit-gate.mjs +1608 -345
  12. package/broker-pins.mjs +152 -0
  13. package/build-launch.mjs +88 -0
  14. package/build-ownership.mjs +1231 -0
  15. package/build-release.mjs +82 -28
  16. package/cargo-contract.mjs +3 -1
  17. package/cargo-ledger.mjs +62 -0
  18. package/cargo-target-shim.mjs +238 -0
  19. package/class-a-launch-guard.mjs +126 -0
  20. package/cli/right-release.mjs +31 -5
  21. package/clippy-equivalence.mjs +202 -0
  22. package/clippy-route.mjs +543 -0
  23. package/codesign-shim.mjs +11 -1
  24. package/committed-source.mjs +39 -5
  25. package/crate-workspaces.mjs +87 -0
  26. package/create-mac-updater.mjs +14 -2
  27. package/deterministic-scans.mjs +997 -71
  28. package/development-signing.mjs +4 -1
  29. package/development.mjs +120 -21
  30. package/direct-receipt.mjs +135 -0
  31. package/door-policy.mjs +71 -6
  32. package/door.mjs +6 -6
  33. package/freshness-census.mjs +257 -0
  34. package/gate-executor.mjs +14 -2
  35. package/gate-stages.mjs +91 -15
  36. package/heavy-command.mjs +3 -2
  37. package/hook-retirement.mjs +65 -0
  38. package/init-command.mjs +1 -1
  39. package/install-app.mjs +11 -2
  40. package/install-assembled-app.mjs +423 -0
  41. package/inventory-check.mjs +11 -8
  42. package/ios-dev.mjs +70 -3
  43. package/ios-upload.mjs +20 -15
  44. package/linux-artifacts.mjs +93 -0
  45. package/local-publish.mjs +224 -53
  46. package/local-qualify.mjs +5 -3
  47. package/local-runtime.mjs +5 -4
  48. package/mac-package-recipe.mjs +4 -1
  49. package/managed-cargo.mjs +47 -5
  50. package/managed-root-guard.mjs +34 -0
  51. package/mirror-root-artifact.mjs +18 -5
  52. package/native-helpers.mjs +355 -0
  53. package/ownership-class-rules.mjs +1 -1
  54. package/ownership-evidence.mjs +37 -13
  55. package/ownership-hooks.mjs +20 -59
  56. package/ownership-lockfiles.mjs +14 -1
  57. package/ownership-policy-bundle.mjs +3 -2
  58. package/ownership-staged.mjs +2 -1
  59. package/ownership.mjs +24 -0
  60. package/package-recipe-targets.mjs +84 -10
  61. package/package.json +4 -3
  62. package/pipeline-admission.mjs +4 -4
  63. package/pipeline-receipts.mjs +302 -302
  64. package/preflight.mjs +27 -4
  65. package/project-type.mjs +2 -2
  66. package/provenance-verify.mjs +115 -0
  67. package/publish-cargo.mjs +12 -9
  68. package/qa-admission-policy.json +10 -1
  69. package/recipe-build.mjs +106 -14
  70. package/release-artifact-path.mjs +341 -0
  71. package/release-config.mjs +1 -1
  72. package/release-invocation.mjs +8 -1
  73. package/release-qa-command.mjs +25 -9
  74. package/release-source-brand.mjs +17 -0
  75. package/release-source-identity.mjs +171 -0
  76. package/release.mjs +59 -22
  77. package/rightkit-versions.json +73 -47
  78. package/runtime-budgets.mjs +6 -2
  79. package/scan-digest.mjs +71 -5
  80. package/sign-updater.mjs +6 -3
  81. package/snapshot-broker.mjs +172 -0
  82. package/snapshot-check.mjs +312 -0
  83. package/snapshot-git.mjs +166 -0
  84. package/snapshot-request.mjs +363 -0
  85. package/snapshot-root-config.mjs +76 -0
  86. package/snapshot.mjs +969 -0
  87. package/source-split.mjs +73 -0
  88. package/stage-commands.mjs +60 -0
  89. package/stage-ledger.mjs +204 -0
  90. package/stage-sidecar-command.mjs +5 -1
  91. package/stage-sidecar.mjs +157 -27
  92. package/swap-helper-bundle.mjs +5 -2
  93. package/test-command.mjs +17 -4
  94. package/unit-gate.mjs +301 -0
  95. package/web-build.mjs +40 -6
  96. package/win-package-recipe.mjs +20 -7
@@ -0,0 +1,172 @@
1
+ // The gate's production seam for a broker snapshot compile (item 7 rows 7.20 to 7.24; GATE-CONTRACT "Broker interface").
2
+ //
3
+ // createSnapshotBroker builds `deps.snapshotBroker` for runScans on a broker host (Mac, Dell): the commit, the snapshot, the
4
+ // build-ownership handle, the declared inputs READ FROM THE SNAPSHOT, the host's enrolled pins (broker-pins.mjs,
5
+ // never the receipt, an environment variable or a flag), the receipt reader and the managed output roots.
6
+ // runBrokerCargo one cargo call reached by a test on the Mac, through the runtime wrapper's broker mode: its own fresh
7
+ // nonce and carrier, `primaryCwd` from the gate's snapshot-to-checkout map, `callingGate` inherited and never
8
+ // forged, the carrier removed after the call, the signed receipt verified, and a ledger record either way.
9
+ // verifyBrokerLedger the gate's own re-verification of every ledgered call from the broker's signed receipt, with the gate's
10
+ // own commit, snapshot, ownership handle and pins (never the wrapper's say-so), and one use per carrier.
11
+ import crypto from "node:crypto";
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { spawnSync } from "./background-process.mjs";
16
+ import { loadBrokerPins } from "./broker-pins.mjs";
17
+ import { resolveManagedRoots } from "./managed-roots.mjs";
18
+ import {
19
+ SnapshotRequestError, argvSha256, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
20
+ } from "./snapshot-request.mjs";
21
+
22
+ /** The status a refused or unverified call returns to its caller: cargo's own error status. */
23
+ export const BROKER_REFUSAL_STATUS = 101;
24
+ export const CALLING_GATE_ENV = "RIGHTKIT_CALLING_GATE";
25
+
26
+ /** The managed `ws` roots a broker pins its generation targets under (BROKER-CONTRACT output pinning). */
27
+ export const brokerOutputRoots = ({ dataRoot }) => [path.join(dataRoot, "rightkit-build-control", "ws")];
28
+
29
+ /** The broker's receipt of one request, read through the RustRight client (`rustright status <id>`), synchronously. */
30
+ export function receiptSync({ client, requestId, env, spawn = spawnSync }) {
31
+ const r = spawn(client, ["status", requestId], { encoding: "utf8", env, timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
32
+ if (r.error || r.status !== 0) throw new SnapshotRequestError("receipt", `rustright status ${requestId} failed (${r.error?.message ?? `exit ${r.status}`})`);
33
+ let doc;
34
+ try { doc = JSON.parse(String(r.stdout)); } catch { throw new SnapshotRequestError("receipt", `rustright status ${requestId} printed no JSON`); }
35
+ // The status envelope is `{type:"STATUS", id, state, ..., receipt}`: `receipt` is null while the job is live and full once it is
36
+ // terminal. Polling is not allowed, so a null receipt after the cargo process exited is a clear error, never a retry.
37
+ if (!doc || typeof doc !== "object" || !Object.hasOwn(doc, "receipt")) throw new SnapshotRequestError("receipt", `rustright status ${requestId} returned no receipt field (not a STATUS envelope)`);
38
+ if (doc.receipt === null) throw new SnapshotRequestError("receipt-not-ready", `rustright status ${requestId} reports the job ${doc.state ?? "state unknown"} with no receipt yet, although its cargo process exited; the gate does not poll, so this call is unverified`);
39
+ if (typeof doc.receipt !== "object" || Array.isArray(doc.receipt)) throw new SnapshotRequestError("receipt", `rustright status ${requestId} returned a receipt that is not an object`);
40
+ return doc.receipt;
41
+ }
42
+
43
+ /**
44
+ * `deps.snapshotBroker` for a production run on a broker host. Throws when anything the compile owes cannot be established:
45
+ * no build ownership, no RustRight client, no pins (an app repository without an enrollment), a malformed declared-inputs manifest.
46
+ * There is no way to hand it pins: tests build their own object and inject it as `deps.snapshotBroker`.
47
+ */
48
+ export function createSnapshotBroker({ env = process.env, platform = process.platform, checkout, commit, snapshot, ownership, client, gateFile, dataRoot, spawn = spawnSync, loadPins = loadBrokerPins }) {
49
+ const need = (ok, why) => { if (!ok) throw new SnapshotRequestError("broker", why); };
50
+ need(checkout && path.isAbsolute(checkout), "the primary checkout must be an absolute path");
51
+ need(commit?.sha && commit?.tree, "the commit and its tree are required");
52
+ need(snapshot?.path && path.isAbsolute(snapshot.path) && snapshot.expectedTree === commit.tree, "the snapshot needs an absolute path and the commit's tree as expectedTree");
53
+ need(ownership && typeof ownership.handle === "string" && ownership.handle && typeof ownership.markBrokerSubmit === "function", "a broker snapshot compile needs the gate's build ownership (handle and submit hook)");
54
+ need(typeof client === "string" && path.isAbsolute(client), "a broker snapshot compile needs the RustRight client (`rustright`) by absolute path");
55
+ const roots = resolveManagedRoots({ env, platform });
56
+ const managedData = dataRoot ?? roots.dataRoot;
57
+ const pins = loadPins({ dataRoot: managedData, platform, gateFile, checkout, spawn });
58
+ return {
59
+ controlRoot: roots.controlRoot, commit: { sha: commit.sha, tree: commit.tree }, snapshot: { path: snapshot.path, expectedTree: snapshot.expectedTree },
60
+ declaredInputs: readDeclaredInputs(snapshot.path), handle: ownership.handle, markBrokerSubmit: () => ownership.markBrokerSubmit(),
61
+ pins: { host: pins.host, keyId: pins.keyId, publicKey: pins.publicKey, brokerDigest: pins.brokerDigest }, pinsSource: pins.source,
62
+ checkout: fs.realpathSync.native(checkout), outputRoots: brokerOutputRoots({ dataRoot: managedData }), platform, client,
63
+ receiptOf: async (requestId) => receiptSync({ client, requestId, env, spawn }),
64
+ };
65
+ }
66
+
67
+ /**
68
+ * What the production gate hands the door as `o.snapshotBroker`: the statement that this gate carries the RustRight snapshot carrier.
69
+ * The door only asks whether a broker host may take the snapshot route; the broker itself (pins, declared inputs, ownership, receipt
70
+ * reader) is built by `createSnapshotBroker` where the snapshot compile runs.
71
+ */
72
+ export const PRODUCTION_SNAPSHOT_CARRIER = Object.freeze({ kind: "production", create: createSnapshotBroker });
73
+
74
+ /** The part of a snapshot broker the wrapper process needs, as the map file carries it (public values only). */
75
+ export const brokerSection = (sb, { sentinel = null } = {}) => ({
76
+ client: sb.client, controlRoot: sb.controlRoot, commit: sb.commit, snapshot: sb.snapshot, handle: sb.handle, checkout: sb.checkout,
77
+ pins: sb.pins, outputRoots: sb.outputRoots, platform: sb.platform, sentinel,
78
+ });
79
+
80
+ const escapes = (rel) => rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
81
+ const inside = (child, parent) => { const rel = path.relative(parent, child); return rel === "" || !escapes(rel); };
82
+
83
+ /**
84
+ * One cargo call of a test, carried through the broker. `workspace` is the real snapshot path of the cargo workspace the call
85
+ * resolved; `checkoutWorkspace` its counterpart in the primary checkout, from the gate's own map. Returns cargo's exit status.
86
+ * Every outcome is ledgered through `record` first or after, never skipped.
87
+ */
88
+ export function runBrokerCargo({ words, cwd, env, section, workspace, checkoutWorkspace, record, spawn = spawnSync, err, tmp = os.tmpdir() }) {
89
+ const id = crypto.randomUUID();
90
+ const base = { id, mode: "broker", cwd, argv: [...words] };
91
+ const refuse = (code, message) => { record({ ...base, status: "refused", code, message }); err(`rightkit-gate: ${code}: ${message}`); return BROKER_REFUSAL_STATUS; };
92
+ const gate = env[CALLING_GATE_ENV];
93
+ if (typeof gate !== "string" || !gate.trim()) return refuse("RK_CARGO_BROKER_NO_GATE", `${CALLING_GATE_ENV} is not set: the call names no gate, and the wrapper never forges one`);
94
+ let realCwd; let checkoutCwd;
95
+ try {
96
+ realCwd = fs.realpathSync.native(cwd);
97
+ const rel = path.relative(workspace, realCwd);
98
+ if (escapes(rel)) throw new Error(`${realCwd} is not inside the workspace ${workspace}`);
99
+ checkoutCwd = fs.realpathSync.native(path.join(checkoutWorkspace, rel));
100
+ if (!fs.statSync(checkoutCwd).isDirectory()) throw new Error(`${checkoutCwd} is not a directory`);
101
+ if (!inside(checkoutCwd, section.checkout)) throw new Error(`${checkoutCwd} is outside the primary checkout ${section.checkout}`);
102
+ } catch (e) { return refuse("RK_CARGO_BROKER_CWD", `the call's working directory has no counterpart in the primary checkout (${e.message}); the broker runs it where the checkout has it`); }
103
+ let inputs;
104
+ try { inputs = readDeclaredInputs(section.snapshot.path); } catch (e) { return refuse("RK_CARGO_BROKER_DECLARED_INPUTS", e.message); }
105
+ // The environment the client sees: the caller's, without the gate's unbound-sentinel output directory (the broker pins the output).
106
+ const clientEnv = { ...env };
107
+ for (const key of Object.keys(clientEnv)) {
108
+ if (/^(CARGO_TARGET_DIR|CARGO_BUILD_TARGET_DIR)$/i.test(key) && section.sentinel && String(clientEnv[key]).startsWith(section.sentinel)) delete clientEnv[key];
109
+ }
110
+ const refDir = fs.mkdtempSync(path.join(tmp, "rk-snapreq-"));
111
+ let made = null;
112
+ try {
113
+ try {
114
+ made = writeSnapshotRequest({ controlRoot: section.controlRoot, commit: section.commit, snapshot: section.snapshot, declaredInputs: inputs, handle: section.handle, primaryCwd: checkoutCwd, argv: words, requestIdFile: path.join(refDir, "request.json") });
115
+ } catch (e) { return refuse("RK_CARGO_BROKER_CARRIER", e.message); }
116
+ try { record({ ...base, status: "pending", nonce: made.carrier.nonce, checkoutCwd }); } catch (e) { fs.rmSync(made.file, { force: true }); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
117
+ const childEnv = { ...clientEnv, ...made.env };
118
+ const r = spawn(section.client, ["cargo", ...words], { cwd: checkoutCwd, env: childEnv, stdio: "inherit" });
119
+ fs.rmSync(made.file, { force: true });
120
+ let outcome;
121
+ let ref = null;
122
+ try {
123
+ if (r.error) throw new SnapshotRequestError("spawn", `the RustRight client did not start (${r.error.message})`);
124
+ ref = readRequestRef(path.join(refDir, "request.json"));
125
+ const receipt = receiptSync({ client: section.client, requestId: ref.requestId, env: clientEnv, spawn });
126
+ const observedEnv = observedEnvOf(clientEnv, section.platform);
127
+ const { evidence, attribution, toolchain } = verifySnapshotReceipt(receipt, { pins: section.pins, jobId: ref.requestId, carrier: made.carrier, argv: words, workspace: ref.workspace, observedEnv, checkout: section.checkout, outputRoots: section.outputRoots, platform: section.platform });
128
+ // attribution and toolchain are recorded as the broker signed them (a consumer that needs per-package attribution treats available:false as FAILED).
129
+ outcome = { status: "verified", requestId: ref.requestId, workspace: ref.workspace, observedEnv, outputDir: evidence.outputDir, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
130
+ } catch (e) {
131
+ outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
132
+ }
133
+ record({ ...base, ...outcome, nonce: made.carrier.nonce, checkoutCwd, clientExit: r.status ?? null });
134
+ if (outcome.status !== "verified") {
135
+ err(`rightkit-gate: RK_CARGO_BROKER_UNVERIFIED: \`cargo ${words.join(" ")}\` has no verified signed receipt (${outcome.code}: ${outcome.message}); the scan fails`);
136
+ return r.status ? r.status : BROKER_REFUSAL_STATUS;
137
+ }
138
+ return r.status ?? (r.signal ? 128 : BROKER_REFUSAL_STATUS);
139
+ } finally {
140
+ if (made) fs.rmSync(made.file, { force: true });
141
+ fs.rmSync(refDir, { recursive: true, force: true });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * The gate's own check of every ledgered broker call of one scan. A call counts only when the broker's signed receipt, fetched
147
+ * again now, verifies against THIS gate's commit, snapshot, ownership handle and pins, and its carrier was used once: a nonce or a
148
+ * request id that appears twice in the run (`seen`) is a replay. Returns the problems; none means every call verified.
149
+ */
150
+ export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set() } } = {}) {
151
+ const problems = [];
152
+ for (const e of entries) {
153
+ if (e.mode !== "broker" || e.status !== "verified") continue;
154
+ const call = `cargo ${(e.argv ?? []).join(" ")}`.trim();
155
+ try {
156
+ if (typeof e.nonce !== "string" || typeof e.requestId !== "string" || !Array.isArray(e.argv) || !e.argv.every((a) => typeof a === "string") || typeof e.workspace !== "string" || typeof e.checkoutCwd !== "string") throw new SnapshotRequestError("ledger", "the record is incomplete");
157
+ if (seen.nonces.has(e.nonce) || seen.ids.has(e.requestId)) throw new SnapshotRequestError("replay", "a carrier or request was used twice; each call carries its own fresh nonce and is used once");
158
+ seen.nonces.add(e.nonce); seen.ids.add(e.requestId);
159
+ const workspace = fs.realpathSync.native(e.workspace);
160
+ if (!inside(workspace, sb.checkout)) throw new SnapshotRequestError("ledger", `the request ran in ${workspace}, outside the primary checkout`);
161
+ if (fs.realpathSync.native(e.checkoutCwd) !== workspace) throw new SnapshotRequestError("ledger", "the request's working directory is not the one the call recorded");
162
+ // The gate's own truth: its commit, snapshot, handle and declared inputs; only the call's own argv, nonce and cwd come from the ledger.
163
+ const carrier = {
164
+ schema: "rightkit.snapshot-request/1", commit: sb.commit, snapshot: sb.snapshot, declaredInputs: { inputs: readDeclaredInputs(sb.snapshot.path) },
165
+ ownership: { handle: sb.handle }, primaryCwd: workspace, argvSha256: argvSha256(e.argv), nonce: e.nonce,
166
+ };
167
+ const receipt = await sb.receiptOf(e.requestId);
168
+ verifySnapshotReceipt(receipt, { pins: sb.pins, jobId: e.requestId, carrier, argv: e.argv, workspace: e.workspace, observedEnv: e.observedEnv ?? {}, checkout: sb.checkout, outputRoots: sb.outputRoots, platform: sb.platform });
169
+ } catch (error) { problems.push(`${call} (from ${e.cwd}): the gate's own verification of the broker's receipt failed (${error.code ?? "error"}: ${error.message})`); }
170
+ }
171
+ return problems;
172
+ }
@@ -0,0 +1,312 @@
1
+ // Snapshot-aware Class A (joint plan step 2; docs/GATE-CONTRACT.md §1 "Broker interface", E17, E18; rightkit#1257).
2
+ //
3
+ // `rightkit-gate check --commit <sha> --full-a --snapshot <dir>` judges the gate-owned snapshot of a selected commit: the checkout
4
+ // is only where the commit and its objects come from (cwd), never what is checked. Its HEAD and status are not read. The snapshot is
5
+ // verified to be exactly the commit's tree (seeded identity, snapshot.mjs) before and after the check; a mismatch or an error is
6
+ // RK_CHECK_ERROR, exit 2.
7
+ //
8
+ // Dependencies for the checks that execute repository code (tests, typecheck, lint) are installed INTO the snapshot from the commit's
9
+ // lockfile (coordinator decision A, recorded in decisions.md with rightkit#1257): the pinned pnpm, `--offline --frozen-lockfile
10
+ // --ignore-scripts --ignore-pnpmfile`, and the store the primary checkout's own install recorded (its node_modules/.modules.yaml
11
+ // storeDir), resolved before any cwd change. No new store, no network, no code of the live checkout. A store miss is RK_CHECK_ERROR
12
+ // naming the package and root, never a skipped check. The next exact sync prunes this install.
13
+ //
14
+ // `rightkit-gate live-scans --repo <root>` re-runs only the live remote scans (pnpm-audit, per pnpm root) and answers the broker's
15
+ // parser (rustright verdict_reuse.rs parse_live_scans): one JSON line, exit 0 when every scan passed, 1 otherwise.
16
+ //
17
+ // A red full Class A whose every failure is deterministic prints `rightkit-gate: verdict deterministic failed=<ids> env=<digest>`;
18
+ // a timeout, a missing tool, a fetch or lease problem, a cancellation, a deferral, or any live scan among the failures prints none.
19
+
20
+ import crypto from "node:crypto";
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { LIVE_SCAN_IDS, defaultSpawn, isLiveScan, isRegistryScan, launchGuardEnv, planScans, runScans } from "./deterministic-scans.mjs";
24
+ import { compileLaunch } from "./class-a-launch-guard.mjs";
25
+ import { SnapshotError, isInside, snapshotProblem, snapshotTree, treeEntries } from "./snapshot.mjs";
26
+ import { discoveryRun, sealedRun } from "./snapshot-git.mjs";
27
+
28
+ export const CHECK_ERROR = "RK_CHECK_ERROR";
29
+ export const LIVE_SCANS_SCHEMA = "rightkit.live-scans/1";
30
+ export const DETERMINISTIC_MARKER = "rightkit-gate: verdict deterministic";
31
+
32
+ /** Failure codes a re-run of the same inputs reproduces: an exit of the check itself, its own findings, a test summary problem. */
33
+ const DETERMINISTIC_CODES = new Set(["EXIT", "RUN_SCRIPT_TOO_LARGE", "NO_TESTS", "ALL_SKIPPED", "SUMMARY_FAILED", "NO_TEST_SUMMARY"]);
34
+
35
+ export class CheckError extends Error {
36
+ constructor(message) { super(message); this.code = CHECK_ERROR; }
37
+ }
38
+
39
+ const text = (r) => String(r?.stdout ?? "");
40
+ const failed = (r) => !r || Boolean(r.error) || r.status !== 0;
41
+ const tail = (r) => `${r?.stderr ?? ""}\n${r?.stdout ?? ""}`.trim().split(/\r?\n/).slice(-6).join(" | ").slice(-600);
42
+ // A path is outside only as `..`, `..<sep>…` or absolute: a sibling named `..x` is inside (snapshot.mjs isInside; review item5-r2 #4).
43
+ const inside = (child, parent) => isInside(child, parent, path);
44
+
45
+ /** The `--snapshot <dir>` of a check: absolute, an existing real directory, not inside the checkout and not holding it. */
46
+ export function snapshotOption(args, { checkout }) {
47
+ const i = args.indexOf("--snapshot");
48
+ if (i < 0) return null;
49
+ const dir = args[i + 1];
50
+ if (!dir || dir.startsWith("--")) throw new CheckError("check --snapshot needs a directory");
51
+ if (!path.isAbsolute(dir)) throw new CheckError(`check --snapshot needs an absolute directory, not ${JSON.stringify(dir)}`);
52
+ if (!args.includes("--full-a") || !args.includes("--commit")) throw new CheckError("check --snapshot is full class A of a selected commit: it needs --commit <sha> and --full-a");
53
+ if (args.includes("--worktree")) throw new CheckError("check --snapshot never checks the working tree: drop --worktree");
54
+ let st;
55
+ try { st = fs.lstatSync(dir); } catch { throw new CheckError(`snapshot ${dir} does not exist`); }
56
+ if (!st.isDirectory()) throw new CheckError(`snapshot ${dir} is not a directory (a link is never followed)`);
57
+ const real = fs.realpathSync.native(dir);
58
+ if (inside(real, checkout) || inside(checkout, real)) throw new CheckError(`snapshot ${real} and checkout ${checkout} overlap`);
59
+ return real;
60
+ }
61
+
62
+ /**
63
+ * `{ commit, tree }` of a revision in the checkout's object database. The sealed invocation (snapshot-git.mjs, item 5 D3): the
64
+ * revision is resolved under the sealed environment with replace refs off, so a replace ref cannot change what <sha>^{tree} means.
65
+ * `spawn` is accepted for the callers that still pass one and is not used: the gate's identity git never goes through an injected runner.
66
+ */
67
+ export function commitOf(checkout, rev, spawn = defaultSpawn) { // eslint-disable-line no-unused-vars
68
+ const name = String(rev ?? "");
69
+ if (!name || name.startsWith("-") || name.includes("\0")) throw new CheckError(`commit ${name} does not resolve in ${checkout}`);
70
+ const c = discoveryRun(checkout, ["rev-parse", "--verify", "--quiet", `${name}^{commit}`]);
71
+ if (failed(c)) throw new CheckError(`commit ${name} does not resolve in ${checkout}`);
72
+ const commit = text(c).trim();
73
+ const t = discoveryRun(checkout, ["rev-parse", "--verify", "--quiet", `${commit}^{tree}`]);
74
+ if (failed(t)) throw new CheckError(`commit ${commit} has no tree`);
75
+ return { commit, tree: text(t).trim() };
76
+ }
77
+
78
+ /**
79
+ * The snapshot holds exactly the commit (E6, GATE-CONTRACT §1): no path under it the commit's tree lacks (ignored ones included; the
80
+ * only allowances are snapshot.mjs's named pnpm dependency directories and the gate's config-only .git/config), no link resolving out
81
+ * of it or into the checkout, and the seeded tree id (the broker's method) equal to the commit's tree. Otherwise CheckError. Returns the tree id.
82
+ *
83
+ * Stage-output ledger (row 5.09d), the same walk as snapshot.mjs verifySnapshot (one implementation, snapshotProblem): given `ledger`
84
+ * (the entries an earlier verify returned) and/or `admitNew` (the END of a stage only), the stage's own ignored, untracked outputs
85
+ * are admitted and the ledger must still match exactly. Then the result is `{ tree, ledger: [{ path, type, size, sha256, execBit }] }`.
86
+ */
87
+ export function verifySnapshotDir({ checkout, dir, commit, tree, when, ledger, admitNew }) {
88
+ const ledgerMode = ledger !== undefined || admitNew === true;
89
+ let inspected;
90
+ try { inspected = snapshotProblem({ dir, entries: treeEntries(checkout, commit), checkout, ledger, admitNew }); }
91
+ catch (e) { throw new CheckError(`cannot list snapshot ${dir} ${when}: ${e.message}`); }
92
+ const extra = ledgerMode ? inspected.problem : inspected;
93
+ if (extra) throw new CheckError(`snapshot ${dir} is not exactly commit ${commit}${ledgerMode ? " plus its stage-output ledger" : ""} ${when}: ${extra}`);
94
+ let actual;
95
+ try { actual = snapshotTree({ checkout, dir, baseTree: tree }); }
96
+ catch (e) { throw new CheckError(`cannot compute the tree of snapshot ${dir} ${when}: ${e.message}`); }
97
+ if (actual !== tree) throw new CheckError(`snapshot ${dir} is tree ${actual} ${when}, not tree ${tree} of commit ${commit}`);
98
+ return ledgerMode ? { tree: actual, ledger: inspected.admitted } : actual;
99
+ }
100
+
101
+ /** Every file of the commit with its blob id, from the checkout's object database: what the snapshot holds. */
102
+ export function commitFiles(checkout, commit, spawn = defaultSpawn) { // eslint-disable-line no-unused-vars
103
+ const r = sealedRun(checkout, ["ls-tree", "-r", "-z", "--full-tree", commit]);
104
+ if (failed(r)) throw new CheckError(`cannot list commit ${commit}: ${tail(r)}`);
105
+ const ids = new Map();
106
+ for (const record of text(r).split("\0").filter(Boolean)) {
107
+ const tab = record.indexOf("\t");
108
+ ids.set(record.slice(tab + 1), record.slice(0, tab).split(" ")[2]);
109
+ }
110
+ const files = [...ids.keys()].sort();
111
+ const digestOf = (list) => crypto.createHash("sha256").update(list.map((f) => `${f}\t${ids.get(f)}\n`).join("")).digest("hex");
112
+ return { files, ids, digestOf, digest: digestOf(files) };
113
+ }
114
+
115
+ /** `storeDir` from a node_modules/.modules.yaml: pnpm writes `"storeDir": "<JSON string>"` (Windows paths escaped, `D:\\x`) or `storeDir: <abs>`. */
116
+ export function modulesStoreDir(yamlText) {
117
+ const src = String(yamlText ?? "");
118
+ const quoted = /^\s*"?storeDir"?\s*:\s*("(?:[^"\\\n]|\\.)*")\s*,?\s*$/m.exec(src);
119
+ if (quoted) { try { return JSON.parse(quoted[1]); } catch { return null; } }
120
+ const bare = /^\s*storeDir\s*:\s*([^"'\s][^\n]*?)\s*$/m.exec(src);
121
+ return bare ? bare[1].trim() : null;
122
+ }
123
+
124
+ /**
125
+ * The existing warm store a pnpm root of the PRIMARY checkout installs from: the storeDir its own install recorded (the root's
126
+ * node_modules/.modules.yaml, else the repository root's), else what `pnpm store path` answers in that root with the primary
127
+ * environment. It must be an absolute directory that exists; nothing is created.
128
+ */
129
+ export function resolvePrimaryStore({ checkout, root = ".", env = process.env, spawn = defaultSpawn, readFile = (f) => fs.readFileSync(f, "utf8"), exists = fs.existsSync }) {
130
+ const candidates = [...new Set([path.join(checkout, root, "node_modules", ".modules.yaml"), path.join(checkout, "node_modules", ".modules.yaml")])];
131
+ for (const file of candidates) {
132
+ let dir = null;
133
+ try { dir = modulesStoreDir(readFile(file)); } catch { dir = null; }
134
+ if (dir && path.isAbsolute(dir) && exists(dir)) return { storeDir: dir, source: path.relative(checkout, file).split(path.sep).join("/") };
135
+ }
136
+ const r = spawn("pnpm", ["store", "path"], { cwd: path.join(checkout, root), env: { ...env, COREPACK_ENABLE_AUTO_PIN: "0" } });
137
+ const dir = failed(r) ? null : text(r).trim().split(/\r?\n/).pop();
138
+ if (dir && path.isAbsolute(dir) && exists(dir)) return { storeDir: dir, source: "pnpm store path (primary checkout)" };
139
+ throw new CheckError(`no existing pnpm store for root ${root} of ${checkout}: its install recorded none and \`pnpm store path\` answered ${dir ? JSON.stringify(dir) : tail(r) || "nothing"}; install the checkout's dependencies once, then run the check again`);
140
+ }
141
+
142
+ /** The pnpm version a root pins (`packageManager: pnpm@x.y.z`, its own package.json or the repository root's), or null. */
143
+ export function pinnedPnpm(dir, { readFile = (f) => fs.readFileSync(f, "utf8") } = {}) {
144
+ try { const pm = JSON.parse(readFile(path.join(dir, "package.json"))).packageManager; const m = /^pnpm@(\d+\.\d+\.\d+)/.exec(String(pm ?? "")); return m ? m[1] : null; } catch { return null; }
145
+ }
146
+
147
+ /** Dependencies whose install scripts the repository allows (pnpm-workspace.yaml `allowBuilds: { name: true }`, `onlyBuiltDependencies`). */
148
+ export function allowedBuilds(dir, { readFile = (f) => fs.readFileSync(f, "utf8") } = {}) {
149
+ const names = new Set();
150
+ let ws = "";
151
+ try { ws = readFile(path.join(dir, "pnpm-workspace.yaml")); } catch { ws = ""; }
152
+ const block = (key) => { const m = new RegExp(`^${key}:\\s*\\n((?:[ \\t]+.*\\n?|\\s*#.*\\n?|\\s*\\n)*)`, "m").exec(ws); return m ? m[1] : ""; };
153
+ for (const line of block("allowBuilds").split("\n")) { const m = /^\s+['"]?([^'":#\s]+)['"]?\s*:\s*true\b/.exec(line); if (m) names.add(m[1]); }
154
+ for (const line of block("onlyBuiltDependencies").split("\n")) { const m = /^\s+-\s+['"]?([^'"#\s]+)['"]?/.exec(line); if (m) names.add(m[1]); }
155
+ try { for (const n of JSON.parse(readFile(path.join(dir, "package.json"))).pnpm?.onlyBuiltDependencies ?? []) names.add(n); } catch { /* none */ }
156
+ return [...names].sort();
157
+ }
158
+
159
+ /**
160
+ * A dependency whose install script the repository allows but `--ignore-scripts` skipped must be shown usable without it, never
161
+ * assumed: its own bin runs with `--version`, or, with no bin, the package loads in a fresh node. The probe runs before class A is
162
+ * green, so it runs under the class A launch guard (launchGuardEnv): it can never start a compile; a refused launch fails the probe.
163
+ * A package that is itself a compile tool the guard lists (esbuild) is never launched here: class A cannot launch it either, so its
164
+ * usability is proven where it is launched, after class A, and it is recorded as such. A package the root does not have installed is
165
+ * not required. Returns [{ name, version, probe, result }]; throws CheckError naming the package and root when one is installed but
166
+ * not usable.
167
+ */
168
+ export function probeSkippedBuilds({ dir, root, names, spawn = defaultSpawn, env = process.env, node = process.execPath }) {
169
+ const results = [];
170
+ const store = path.join(dir, "node_modules", ".pnpm");
171
+ let entries = [];
172
+ try { entries = fs.readdirSync(store); } catch { entries = []; }
173
+ for (const name of names) {
174
+ const flat = name.replace("/", "+");
175
+ for (const entry of entries.filter((e) => e.startsWith(`${flat}@`)).sort()) {
176
+ const pkgDir = path.join(store, entry, "node_modules", ...name.split("/"));
177
+ let manifest;
178
+ try { manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8")); } catch { continue; }
179
+ const bins = typeof manifest.bin === "string" ? { [name.split("/").pop()]: manifest.bin } : (manifest.bin ?? {});
180
+ const binName = Object.keys(bins).find((b) => b === name.split("/").pop()) ?? Object.keys(bins)[0];
181
+ const compiler = compileLaunch(name.split("/").pop()) ?? (binName ? compileLaunch(binName) : null);
182
+ if (compiler) { results.push({ name, version: manifest.version, probe: "none", result: `compile tool (${compiler}): class A never launches it; its usability is proven where it is launched, after class A` }); continue; }
183
+ const probe = binName ? [path.join(pkgDir, bins[binName]), "--version"] : ["-e", `require(${JSON.stringify(pkgDir)})`];
184
+ const r = spawn(node, probe, { cwd: dir, env: { ...env, ...launchGuardEnv(env) }, boundedMs: 60_000 });
185
+ const out = text(r).trim().split(/\r?\n/).pop() ?? "";
186
+ if (failed(r)) {
187
+ const refused = `${r?.stderr ?? ""}\n${r?.stdout ?? ""}`.split(/\r?\n/).find((l) => l.includes("class A refuses a compile/emit launch"));
188
+ const why = refused ? `it launched a compile, which the class A launch guard refused: ${refused.replace(/^rightkit-gate: /, "").trim()}` : (tail(r) || r?.error?.message || `exit ${r?.status}`);
189
+ throw new CheckError(`root ${root}: ${name}@${manifest.version} needs its install script (allowed by the repository, not run under --ignore-scripts) and is not usable without it: ${binName ? `${binName} --version` : "require"} failed: ${why}`);
190
+ }
191
+ results.push({ name, version: manifest.version, probe: binName ? `${binName} --version` : "require", result: out || "loaded" });
192
+ }
193
+ }
194
+ return results;
195
+ }
196
+
197
+ /**
198
+ * Installs each pnpm root's dependencies into the snapshot from the commit's lockfile and the primary checkout's existing store.
199
+ * Returns one record per root ({ root, storeDir, storeSource, pnpm, lockfileSha256, ms, allowedBuildsNotRun }); throws CheckError on a
200
+ * store miss, a pnpm that is not the pinned one, or any install failure.
201
+ */
202
+ export function installSnapshotDependencies({ checkout, snapshot, roots, env = process.env, spawn = defaultSpawn, now = () => Date.now(), readFile, exists, probe = probeSkippedBuilds }) {
203
+ const records = [];
204
+ const versionOf = (cwd) => { const r = spawn("pnpm", ["--version"], { cwd, env: { ...env, COREPACK_ENABLE_AUTO_PIN: "0" } }); return failed(r) ? null : text(r).trim().split(/\r?\n/).pop(); };
205
+ for (const root of roots) {
206
+ const dir = path.join(snapshot, root);
207
+ // No lockfile: nothing to install from. Not an error here: the pnpm-lockfile scan judges it, and checks that need dependencies fail.
208
+ if (!fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) { records.push({ root, skipped: "no pnpm-lock.yaml in the commit: nothing to install from (the pnpm-lockfile scan judges it)" }); continue; }
209
+ const { storeDir, source } = resolvePrimaryStore({ checkout, root, env, spawn, ...(readFile ? { readFile } : {}), ...(exists ? { exists } : {}) });
210
+ const pin = pinnedPnpm(dir) ?? pinnedPnpm(snapshot);
211
+ // Asked where the install runs: pnpm switches itself to the root's packageManager version there (Dell 2026-10-29: an installed
212
+ // 11.24.0 answers 11.24.0 in a neutral directory and runs 11.25.0 in the root), so that is the pnpm that will install.
213
+ const version = versionOf(dir);
214
+ if (!version) throw new CheckError("pnpm is not runnable on this host; the snapshot's dependencies cannot be installed");
215
+ if (pin && version !== pin) throw new CheckError(`pnpm ${version} is on PATH, but root ${root} of the commit pins pnpm@${pin} (packageManager); install the pinned pnpm`);
216
+ const started = now();
217
+ // The virtual store is pinned inside the snapshot and the global virtual store off, whatever the host's pnpm config says: the
218
+ // install writes nowhere but the snapshot (links to the existing store's content are the only reads outside it).
219
+ const r = spawn("pnpm", ["install", "--offline", "--frozen-lockfile", "--ignore-scripts", "--ignore-pnpmfile", "--store-dir", storeDir, "--virtual-store-dir", "node_modules/.pnpm", "--config.enable-global-virtual-store=false", "--reporter", "append-only"], {
220
+ cwd: dir, env: { ...env, COREPACK_ENABLE_AUTO_PIN: "0", CI: "true" }, maxBuffer: 1 << 28,
221
+ });
222
+ const ms = now() - started;
223
+ if (failed(r)) {
224
+ const out = `${r?.stdout ?? ""}\n${r?.stderr ?? ""}`;
225
+ const miss = /ERR_PNPM_NO_OFFLINE_TARBALL[^\n]*?(https?:\/\/\S+?\/-\/(\S+?)\.tgz)/.exec(out);
226
+ if (miss) throw new CheckError(`root ${root}: ${miss[2]} is missing from the store ${storeDir} (offline install, no network fallback); install the checkout's dependencies so the store holds it, then run the check again`);
227
+ throw new CheckError(`root ${root}: offline dependency install failed (store ${storeDir}): ${tail(r) || r?.error?.message || `exit ${r?.status}`}`);
228
+ }
229
+ const lockfileSha256 = crypto.createHash("sha256").update(fs.readFileSync(path.join(dir, "pnpm-lock.yaml"))).digest("hex");
230
+ const skipped = probe ? probe({ dir, root, names: allowedBuilds(dir), spawn, env }) : [];
231
+ records.push({ root, storeDir, storeSource: source, pnpm: version, pinned: pin, lockfileSha256, ms, allowedBuildsNotRun: skipped });
232
+ }
233
+ return records;
234
+ }
235
+
236
+ /** One line per installed root, for the check's output and evidence. */
237
+ export function describeInstall(record) {
238
+ if (record.skipped) return `snapshot dependencies root=${record.root} not installed: ${record.skipped}`;
239
+ return `snapshot dependencies root=${record.root} pnpm=${record.pnpm}${record.pinned ? ` (pinned ${record.pinned})` : ""} store=${record.storeDir} (from ${record.storeSource}) lockfile=sha256:${record.lockfileSha256} ms=${record.ms}${record.allowedBuildsNotRun.length ? ` build-scripts-not-run=${record.allowedBuildsNotRun.map((b) => (b.probe === "none" ? `${b.name}@${b.version}(deferred, not proven here: ${b.result})` : `${b.name}@${b.version}(usable: ${b.probe} -> ${b.result})`)).join(",")}` : ""}`;
240
+ }
241
+
242
+ /** The deterministic-red marker for a failed run, or null when any failure is not one a re-run of the same inputs reproduces. */
243
+ export function deterministicMarker(scanResult, env) {
244
+ const failing = scanResult.scans.filter((r) => r.status !== "pass");
245
+ if (!failing.length || !env) return null;
246
+ // A fail row with no code is a scan's own findings (an internal check's list); every other code names why it could not judge.
247
+ const eligible = failing.every((r) => r.status === "fail" && !isLiveScan(r.id) && !isRegistryScan(r.id) && (r.code === undefined || r.code === null || DETERMINISTIC_CODES.has(r.code)));
248
+ return eligible ? `${DETERMINISTIC_MARKER} failed=${failing.map((r) => r.id).join(",")} env=${env}` : null;
249
+ }
250
+
251
+ /** Repository-relative files under `root`: git's view when `root` is a work tree's top level, else a walk (no node_modules, no .git). */
252
+ export function rootFiles(root, spawn = defaultSpawn) {
253
+ const top = spawn("git", ["rev-parse", "--show-toplevel"], { cwd: root });
254
+ if (!failed(top) && path.resolve(text(top).trim()) === path.resolve(root)) {
255
+ const r = spawn("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 1 << 30 });
256
+ if (!failed(r)) return text(r).split("\0").filter(Boolean).map((f) => f.split(path.sep).join("/"));
257
+ }
258
+ // No git (a gate-owned snapshot): list what git would, a link as one entry (never followed into), and only real directories
259
+ // entered. A link must land, physically, inside the root on a file; one that leaves it (or dangles) stops the listing rather than
260
+ // silently dropping what it names (a committed apps/web/package.json -> ../../data/web.json is a manifest the audit must see).
261
+ const realRoot = fs.realpathSync.native(root);
262
+ const out = [];
263
+ const walk = (d, rel) => {
264
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
265
+ if (e.name === "node_modules" || e.name === ".git") continue;
266
+ const r = rel ? `${rel}/${e.name}` : e.name;
267
+ const p = path.join(d, e.name);
268
+ if (e.isSymbolicLink()) {
269
+ let real = null;
270
+ try { real = fs.realpathSync.native(p); } catch { real = null; }
271
+ if (!real || !inside(real, realRoot)) throw new CheckError(`${r} is a link that does not land inside ${root}; the live scans cannot list the tree safely`);
272
+ if (fs.statSync(real).isFile()) out.push(r);
273
+ } else if (e.isDirectory()) walk(p, r);
274
+ else if (e.isFile()) out.push(r);
275
+ }
276
+ };
277
+ walk(root, "");
278
+ return out.sort();
279
+ }
280
+
281
+ /**
282
+ * `rightkit-gate live-scans --repo <root>`: the live remote scans of the tree at <root>, planned exactly as the full check plans them
283
+ * (planScans, all files changed) and run through the same runner. `deps` supplies the gate's unit discovery.
284
+ */
285
+ export async function liveScansCommand(o, args, { discoverPnpmRoots, discoverNpmUnits, plan = planScans, run = runScans }) {
286
+ const i = args.indexOf("--repo");
287
+ if (i < 0 || !args[i + 1] || args[i + 1].startsWith("--") || args.length !== 2) { o.err("rightkit-gate: live-scans takes exactly --repo <checked tree root>"); return 2; }
288
+ const root = path.resolve(o.cwd, args[i + 1]);
289
+ let scans;
290
+ try {
291
+ const files = rootFiles(root, o.spawn);
292
+ const tracked = (name) => files.filter((f) => f === name || f.endsWith(`/${name}`));
293
+ const readFile = (f) => o.readFile(f);
294
+ const npmRoots = discoverPnpmRoots({ root, tracked, exists: o.exists });
295
+ const npmUnits = discoverNpmUnits({ root, tracked, readFile, exists: o.exists });
296
+ const full = plan({ changed: files, contentRoot: root, liveRoot: root, trustedRoot: root, includeTests: false, rustUnits: [], npmUnits, npmRoots, tier: "release", exists: o.exists, readFile: o.readFile, platform: o.platform });
297
+ const live = { ...full, scans: full.scans.filter((s) => isLiveScan(s.id)), deferred: [] };
298
+ // A tree with a pnpm lockfile always owes a live audit: planning none there is a listing or planning failure, never an empty pass.
299
+ const lockfiles = files.filter((f) => /(^|\/)pnpm-lock\.yaml$/.test(f));
300
+ if (lockfiles.length && !live.scans.length) throw new Error(`the tree holds ${lockfiles.join(", ")} but no live scan was planned; refusing to answer an empty refresh`);
301
+ const result = await run(live, { classes: ["A"], jobs: 2 });
302
+ scans = result.scans.map((r) => ({ id: r.id, status: r.status === "pass" ? "passed" : "failed", ...(r.status === "pass" ? {} : { detail: String(r.detail ?? r.status).slice(0, 300) }) }));
303
+ } catch (e) {
304
+ o.err(`rightkit-gate: live-scans could not run: ${e.message}`);
305
+ return 2;
306
+ }
307
+ o.out(JSON.stringify({ schema: LIVE_SCANS_SCHEMA, scans: scans.map(({ id, status }) => ({ id, status })) }));
308
+ for (const s of scans) if (s.status === "failed") o.err(`rightkit-gate: live scan ${s.id} failed: ${s.detail}`);
309
+ return scans.every((s) => s.status === "passed") ? 0 : 1;
310
+ }
311
+
312
+ export { LIVE_SCAN_IDS, SnapshotError };