@rightkit/release 0.6.16 → 0.6.20

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 (64) hide show
  1. package/README.md +7 -3
  2. package/admission-host.mjs +9 -5
  3. package/app-closure.mjs +9 -3
  4. package/bin/darwin/rightkit-swap +0 -0
  5. package/bin/rightkit-gate.mjs +469 -102
  6. package/bin/rightkit-linux-setup.mjs +15 -0
  7. package/broker-admission.mjs +184 -0
  8. package/broker-wire.mjs +15 -3
  9. package/build-launch.mjs +4 -1
  10. package/build-ownership.mjs +125 -36
  11. package/build-release.mjs +39 -7
  12. package/cargo-contract.mjs +4 -1
  13. package/cargo-guard.mjs +25 -5
  14. package/cargo-target-shim.mjs +56 -7
  15. package/clippy-route.mjs +8 -7
  16. package/crate-graph.mjs +5 -1
  17. package/credential-provider.mjs +174 -0
  18. package/deterministic-scans.mjs +247 -40
  19. package/development.mjs +8 -0
  20. package/disk-policy.mjs +2 -2
  21. package/gate-executor.mjs +5 -3
  22. package/gate-stages.mjs +34 -3
  23. package/gradle-lock.mjs +206 -0
  24. package/heavy-command.mjs +33 -2
  25. package/linux-artifacts.mjs +2 -1
  26. package/linux-setup.mjs +106 -0
  27. package/local-publish.mjs +21 -10
  28. package/local-qualify.mjs +9 -3
  29. package/managed-cargo.mjs +54 -18
  30. package/ownership-lockfiles.mjs +14 -3
  31. package/ownership.mjs +5 -1
  32. package/package.json +17 -11
  33. package/protected-credentials.mjs +13 -5
  34. package/publication-gate.mjs +9 -4
  35. package/python-lock.mjs +209 -0
  36. package/qa-admission-policy.json +32 -0
  37. package/qa-layout.mjs +229 -0
  38. package/qa-runner.mjs +156 -0
  39. package/release-artifact-path.mjs +29 -3
  40. package/release-config.mjs +1 -1
  41. package/release-qa-command.mjs +58 -8
  42. package/release-qa-policy.mjs +5 -3
  43. package/release-source-identity.mjs +1 -0
  44. package/release.mjs +10 -2
  45. package/rightkit-ownership.json +1465 -79
  46. package/rightkit-registry-resolver.mjs +7 -3
  47. package/rightkit-versions.json +173 -36
  48. package/runtime-budget-classes.json +4 -0
  49. package/runtime-budgets.mjs +5 -1
  50. package/shell-contract.mjs +1 -1
  51. package/snapshot-broker.mjs +107 -9
  52. package/snapshot-check.mjs +2 -0
  53. package/snapshot-request.mjs +20 -3
  54. package/snapshot.mjs +11 -4
  55. package/stage-commands.mjs +2 -1
  56. package/stage-ledger.mjs +15 -0
  57. package/stage-sidecar.mjs +17 -3
  58. package/suite-signing.mjs +40 -4
  59. package/swift-lock.mjs +334 -0
  60. package/test-command.mjs +13 -3
  61. package/tool-build.mjs +156 -0
  62. package/unit-gate.mjs +9 -2
  63. package/windows-identity-plan.mjs +119 -0
  64. package/windows-identity.mjs +136 -0
@@ -61,8 +61,12 @@
61
61
  // rust.<crate>.testInputs lists the paths they read, for npm, node and docs scans, and whenever a reference cannot be resolved
62
62
  // statically) are all identical. Stored in <git common dir>/rightkit-gate-cache; a hit prints
63
63
  // `cached` and is listed in the result's cacheHits. Failures, deferrals and class A scans are never cached.
64
- // RK_GATE_JOBS (default 4) sets class A scan concurrency. RK_GATE_CLASS_B_JOBS (default 3, the Mac broker's build capacity; the
65
- // broker queues beyond its own) sets how many components' class B scans run at once; one component's scans stay in order (rightkit#1285).
64
+ // RK_GATE_JOBS (default 4) sets class A scan concurrency. Class B on a broker host (rightkit#1367 step 2): the gate does not count builds; every
65
+ // component's broker-carried scans (cargo, rustright, carried Swift, and npm suites that declare a broker call) start without a limit, each component in plan order,
66
+ // and the broker queues and orders them. A carried scan's run limit is a stopwatch that stops while the broker holds all its requests QUEUED (broker-admission.mjs),
67
+ // so queue time is neither charged to the scan nor bounded by a gate allowance. Only class B suites the broker never sees (npm or node tests run on this host)
68
+ // keep a local limit of 3, one slot per suite. On a host with no broker RK_GATE_CLASS_B_JOBS (default 3) sets how many components' class B scans run at once; one
69
+ // component's scans stay in order (rightkit#1285).
66
70
  // A crate whose ci/gate.config.json entry says `quietTests: true` has its test scans run last,
67
71
  // one at a time, under the RustRight quiet lease (`rustright quiet acquire`, RUSTRIGHT_QUIET_LEASE in their environment).
68
72
  // SIGINT/SIGTERM stop scheduling, kill every running scan's process tree and exit non-zero listing the scans not run.
@@ -74,13 +78,16 @@ import os from "node:os";
74
78
  import path from "node:path";
75
79
  import { FOREIGN_PRE_COMMIT, retireHooksIn } from "../hook-retirement.mjs";
76
80
  import { fileURLToPath } from "node:url";
77
- import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanToolPolicyFromPipeline, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
81
+ import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, LOCAL_CLASS_B_JOBS, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanToolPolicyFromPipeline, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
78
82
  import { changedLockNames, parseLock, reach } from "../lock-graph.mjs";
79
83
  import { resolveExecutor } from "../gate-executor.mjs";
84
+ // The gate's own executor: an outer gate's cargo wrapper on PATH is never this gate's cargo (gate-executor.mjs `gateWrappers`).
85
+ const gateResolveExecutor = (args) => resolveExecutor({ ...args, gateWrappers: true });
80
86
  import { ALL_UNITS_ERRORS, ALL_UNITS_EXCEPTIONS_FILE, BROKER_EXCEPTIONS_FILE, BROKER_EXCEPTION_ERRORS, CARGO_WORKSPACE_LIST_FILE, CARGO_WORKSPACE_LIST_KEY, RIGHTKIT_IDENTITY_FILE, authorizeAllUnits, authorizeNoBroker, checkCargoWorkspaceList, describeAllUnitsException, describeBrokerException, isRightKitIdentity, unitsOfSelector } from "../broker-exception.mjs";
81
87
  import { SIGNING_REFUSAL_CODES, suiteSigningRefusals } from "../suite-signing-policy.mjs";
82
88
  import { gitListRustFiles } from "../signing-files.mjs";
83
- import { LINUX_BROKER_ROUTE, NO_BROKER_ENV, directPlatform, linuxBrokerDeclared } from "../managed-cargo.mjs";
89
+ import { LINUX_BROKER_ROUTE, NO_BROKER_ENV, directPlatform, linuxBrokerDeclared, managedRuntimeClient, realCargoOf } from "../managed-cargo.mjs";
90
+ import { createBrokerLive } from "../broker-admission.mjs";
84
91
  import { bindingOf, compileOutputsRecord, compileReuseProblem, endRun, readReceipt, RECEIPT_SCHEMA, receiptKey, sameBinding, sameSource, sealedFor, stagesTo, startRun, STAGES, STAGE_SOURCES, writeReceipt } from "../gate-stages.mjs";
85
92
  import { spawnSync as spawnInherit } from "../background-process.mjs";
86
93
  import { readDiskPolicy, scratchRoots } from "../disk-policy.mjs";
@@ -103,7 +110,7 @@ export const PARTIAL_EXIT = 3;
103
110
  /** The platforms a door run can package: desktop (signed, sealed, installed QA, publisher upload) and mobile (signed, sealed, store test track). */
104
111
  export const PLATFORMS = Object.freeze(["mac", "win", "ios", "android"]);
105
112
  export const MOBILE_PLATFORMS = Object.freeze(new Set(["ios", "android"]));
106
- const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify --units-file <json {"units":[<selector>,...]} written for this run> | rightkit-gate --to check|compile|package|qualify --all-units-exception <id> (RightKit only: its unit is named on every run, or every unit by Adrian's entry in ci/all-units-exceptions.json of the commit) | rightkit-gate --to check|compile|package|qualify [--unit <selector>] --no-broker <exception-id> (Mac and Dell only; Adrian's recorded exception in .rightkit/broker-exceptions.json of the commit) | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform mac|win|ios|android] [--commit <rev>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
113
+ const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to tool --unit cargo:rightkit-qa|cargo:rightkit-credentials [--no-broker <exception-id>] [--receipt-out <file>] [--commit <rev>] (builds the unit's release binary from the commit and enrols it: cargo:rightkit-qa as the managed rightkit-qa runner, cargo:rightkit-credentials as the managed OS credential provider updater signing runs; Mac and Windows) | rightkit-gate --to check|compile|package|qualify --units-file <json {"units":[<selector>,...]} written for this run> | rightkit-gate --to check|compile|package|qualify --all-units-exception <id> (RightKit only: its unit is named on every run, or every unit by Adrian's entry in ci/all-units-exceptions.json of the commit) | rightkit-gate --to check|compile|package|qualify [--unit <selector>] --no-broker <exception-id> (Mac and Dell only; Adrian's recorded exception in .rightkit/broker-exceptions.json of the commit) | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform mac|win|ios|android] [--commit <rev>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
107
114
  /** The host platform (process.platform) that constructs each platform's artifact; a platform missing here (android) builds on any host. */
108
115
  const STAGE_NATIVE_HOST = Object.freeze({ mac: "darwin", ios: "darwin", win: "win32" });
109
116
 
@@ -235,7 +242,13 @@ function workspaceMetadata({ root, spawn, cargoCommand, dir, realpath }) {
235
242
  const members = (meta.packages ?? []).map((p) => {
236
243
  const pkgDir = rel(path.dirname(p.manifest_path));
237
244
  if (pkgDir === ".." || pkgDir.startsWith("../")) throw new Error(`workspace member ${p.name} lives outside the repository (${pkgDir})`);
238
- return { name: p.name, dir: pkgDir, manifest: pkgDir === "." ? "Cargo.toml" : `${pkgDir}/Cargo.toml`, publish: !(Array.isArray(p.publish) && p.publish.length === 0) };
245
+ // Bin targets (rustright#209 item 5) and whether `cargo test -p <name>` links them: Cargo builds a package's bins only for the integration tests
246
+ // it selects, so a package with none leaves `debug/<bin>` to whatever an earlier build wrote. A bin with required-features is not counted.
247
+ const kinds = (t) => (Array.isArray(t.kind) ? t.kind : []);
248
+ const bins = (p.targets ?? []).filter((t) => kinds(t).includes("bin") && !(t["required-features"] ?? t.required_features ?? []).length).map((t) => t.name).sort();
249
+ // Only an integration test `cargo test` selects (`test` not false, no required-features) makes it link the bins; a bench is test = false by default.
250
+ const testLinksBins = (p.targets ?? []).some((t) => kinds(t).includes("test") && t.test !== false && !(t["required-features"] ?? t.required_features ?? []).length);
251
+ return { name: p.name, dir: pkgDir, manifest: pkgDir === "." ? "Cargo.toml" : `${pkgDir}/Cargo.toml`, publish: !(Array.isArray(p.publish) && p.publish.length === 0), bins, testLinksBins };
239
252
  }).sort((a, b) => (a.name < b.name ? -1 : 1));
240
253
  return { cargoDir, members };
241
254
  }
@@ -272,6 +285,7 @@ export function gateTmpRoot({ env = process.env, platform = process.platform, ex
272
285
  */
273
286
  /** Longest the class A environment waits for `pnpm --version`. */
274
287
  export const PNPM_PROBE_TIMEOUT_MS = 60_000;
288
+ export const PNPM_PIN_REFUSAL = "RK_PNPM_VERSION_MISMATCH";
275
289
  export const TOOL_CACHE_DIRS = new Set([".vite", ".vite-temp", ".vitest-cache", ".cache"]);
276
290
  export const INSTALL_STATE_NOT_COVERED = new Set([".pnpm-task-run-state-v1"]);
277
291
  const nodeModulesDirs = (root, files) => [...new Set(files.filter((f) => /(^|\/)package\.json$/.test(f)).map((f) => path.join(path.dirname(path.join(root, f)), "node_modules")))].sort();
@@ -433,6 +447,37 @@ export function classAEnvironmentDigest({ root, files, spawn = defaultSpawn, inc
433
447
  return createHash("sha256").update([`node ${process.version}`, `pnpm ${pnpmVersion}`, `platform ${process.platform}-${process.arch}`, ...gateFiles, ...installState, `installed ${installedDependencyDigest({ root, files, includeCaches })}`].join("\n")).digest("hex");
434
448
  }
435
449
 
450
+ /** The pnpm a package.json pins (`packageManager: "pnpm@12.16.1+sha512.."` -> `12.16.1`), or null for no pnpm pin. */
451
+ export function pnpmPinOf(packageJsonText) {
452
+ let pm; try { pm = JSON.parse(packageJsonText)?.packageManager; } catch { return null; }
453
+ const m = typeof pm === "string" ? /^pnpm@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/.exec(pm.trim()) : null;
454
+ return m ? m[1] : null;
455
+ }
456
+
457
+ /**
458
+ * Class A runs the pnpm it bound, never a pnpm that relaunches another one: a pnpm older than a package's `packageManager` pin switches
459
+ * itself to the pinned version through a nested shell (on Windows `cmd /d /s /c` with ^-escaped text), which the class A launch guard
460
+ * refuses mid-row as an unreadable launch. This compares the pnpm on PATH, asked from a directory with no package.json so it cannot
461
+ * switch, with every pnpm pin of the checked tree BEFORE any class A row launches, and returns a typed problem naming both versions
462
+ * and the fix, or null. Nothing is admitted: the guard keeps refusing any relaunch.
463
+ */
464
+ export function pnpmPinProblem({ root, files, spawn = defaultSpawn, readFile = (f) => fs.readFileSync(f, "utf8"), neutralCwd = os.tmpdir() }) {
465
+ const pins = new Map();
466
+ for (const f of files.filter((x) => /(^|\/)package\.json$/.test(x))) {
467
+ let pin = null; try { pin = pnpmPinOf(readFile(path.join(root, f))); } catch { pin = null; }
468
+ if (pin) pins.set(f, pin);
469
+ }
470
+ if (!pins.size) return null;
471
+ const r = spawn("pnpm", ["--version"], { cwd: neutralCwd, boundedMs: PNPM_PROBE_TIMEOUT_MS });
472
+ const onPath = failed(r) ? "" : text(r).trim();
473
+ const fix = (pin) => `install the pinned pnpm as the pnpm on PATH (for example \`pnpm self-update ${pin}\`) and run again`;
474
+ if (!onPath) { const [pin] = new Set(pins.values()); return { code: PNPM_PIN_REFUSAL, message: `${PNPM_PIN_REFUSAL}: cannot read the version of the pnpm on PATH (${r?.error ? r.error.code ?? r.error.message : why(r)}); ${fix(pin)}` }; }
475
+ const bad = [...pins].filter(([, pin]) => pin !== onPath);
476
+ if (!bad.length) return null;
477
+ const [file, pin] = bad[0];
478
+ return { code: PNPM_PIN_REFUSAL, message: `${PNPM_PIN_REFUSAL}: the pnpm on PATH is ${onPath} but ${file} pins packageManager pnpm@${pin}${bad.length > 1 ? ` (${bad.length} package.json files differ)` : ""}; a different pnpm relaunches the pinned one through a shell, which class A refuses (RK_CLASS_A_COMPILE_LAUNCH); ${fix(pin)}` };
479
+ }
480
+
436
481
  /** Split pathspecs into batches whose joined length stays under `limit` characters (well below Windows' 32767 command line). */
437
482
  export function pathspecBatches(paths, limit = 8000) {
438
483
  const batches = []; let current = []; let size = 0;
@@ -635,7 +680,7 @@ export function discoverRustWorkspaces({ root, spawn, cargoCommand, tracked, unt
635
680
  for (const member of found.members) {
636
681
  covered.add(member.manifest);
637
682
  if (!trackedSet.has(member.manifest)) { skipped.push({ manifest: member.manifest, why: "a workspace member that is untracked or ignored, so it is not part of the gated commit" }); continue; }
638
- units.push({ name: rustUnitName(found.cargoDir, member.name), crate: member.name, dir: member.dir, publish: member.publish, cargoDir: found.cargoDir });
683
+ units.push({ name: rustUnitName(found.cargoDir, member.name), crate: member.name, dir: member.dir, publish: member.publish, cargoDir: found.cargoDir, bins: member.bins ?? [], testLinksBins: member.testLinksBins === true });
639
684
  }
640
685
  workspaces.push({ cargoDir: found.cargoDir, units });
641
686
  }
@@ -1262,6 +1307,17 @@ export function readUnitList(readFile, file) {
1262
1307
  * units, so a repository that is not RightKit refuses them. Returns `{ exit }` when the run ends here, or `{}` to go on as a whole door run (the
1263
1308
  * all-units flag and its id are removed from `args`). An unresolvable commit is runStages' own refusal, so it goes on.
1264
1309
  */
1310
+ /** The RightKit sample app the run's working directory is in, when the commit's ci/sample-apps.json lists it; else null. */
1311
+ export function sampleAppOf({ o, root, git, commit }) {
1312
+ const shown = git("show", `${commit}:ci/sample-apps.json`);
1313
+ if (failed(shown)) return null;
1314
+ let apps;
1315
+ try { apps = JSON.parse(text(shown)).apps; } catch { return null; }
1316
+ if (!Array.isArray(apps)) return null;
1317
+ const rel = path.relative(root, path.resolve(o.cwd)).split(path.sep).join("/");
1318
+ return apps.find((dir) => typeof dir === "string" && dir && !dir.includes("..") && (rel === dir || rel.startsWith(`${dir}/`))) ?? null;
1319
+ }
1320
+
1265
1321
  export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList }) {
1266
1322
  const tag = "rightkit-gate:";
1267
1323
  const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
@@ -1299,6 +1355,13 @@ export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag,
1299
1355
  o.out(`${tag} ALL UNITS EXCEPTION ${describeAllUnitsException(granted)}`);
1300
1356
  return {};
1301
1357
  }
1358
+ // RightKit's own sample apps (ci/sample-apps.json of the commit: the all-packages e2e app) are built as apps, from inside their directory:
1359
+ // the run is scoped to that app (appRootOf), never the whole repository, exactly as a product app's run is.
1360
+ const sample = sampleAppOf({ o, root, git, commit: pinned.commit });
1361
+ if (sample) {
1362
+ o.out(`${tag} RightKit sample app ${sample}: built as an app, scoped to ${sample} (ci/sample-apps.json)`);
1363
+ return {};
1364
+ }
1302
1365
  o.err(`${tag} BLOCKED: RK_UNIT_NOT_NAMED: RightKit names its unit on every run: --unit <cargo:rightkit-name|npm:@rightkit/name>, --units-file <json written for this run>, or --all-units-exception <id> (Adrian's entry in ${ALL_UNITS_EXCEPTIONS_FILE}); a whole-repository run is never the default; nothing ran`);
1303
1366
  return { exit: 1 };
1304
1367
  }
@@ -1311,7 +1374,7 @@ async function runStages(o, args, root, git, io) {
1311
1374
  // of the snapshot), then right-release dev|test with the snapshot's app root as its cwd. They have no stage list of their own.
1312
1375
  const devTest = to === "dev" || to === "test";
1313
1376
  let stages = devTest ? [] : stagesTo(to);
1314
- if (!stages) { o.err(`${tag} --to must be one of ${[...STAGES, "dev", "test"].join(", ")}`); return 2; }
1377
+ if (!stages) { o.err(`${tag} --to must be one of ${[...STAGES, "dev", "test"].join(", ")}${to === "tool" ? " (--to tool builds one named tool unit: --to tool --unit cargo:rightkit-qa or --to tool --unit cargo:rightkit-credentials)" : ""}`); return 2; }
1315
1378
  // Every stage the door runs has a row naming where it reads its source from (gate-stages.mjs STAGE_SOURCES, drift-tested in
1316
1379
  // door-stages.test.mjs); a stage added without one is refused rather than run with an unstated source.
1317
1380
  if (!STAGE_SOURCES[to]) { o.err(`${tag} BLOCKED: stage ${to} has no STAGE_SOURCES row naming its source; nothing ran`); return 2; }
@@ -1345,7 +1408,7 @@ async function runStages(o, args, root, git, io) {
1345
1408
  // `--route`: which route this stage takes here and whether it refuses, answered without running anything (rightkit#1297).
1346
1409
  if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir });
1347
1410
  // Commit-only build (GATE-CONTRACT §1): on every host (not hosted CI, whose runner checkout is the commit) the door selects a commit
1348
- // (--commit <rev>, default HEAD), takes the host's build ownership, syncs the commit's gate-owned snapshot once, and every stage
1411
+ // (--commit <rev>, default HEAD), takes the repository's build ownership, syncs the commit's gate-owned snapshot once, and every stage
1349
1412
  // below reads that snapshot; the checkout may hold anything and is never a build source. Every non-hosted host takes this route: Mac
1350
1413
  // and Dell compile it through the RustRight snapshot carrier (`deps.snapshotBroker`, wired by `productionEntryDeps`; item 7 rows 7.20
1351
1414
  // to 7.24, rightkit#1301), right-release's own children included. Once it is selected any failure refuses; there is no retry on the
@@ -1354,7 +1417,7 @@ async function runStages(o, args, root, git, io) {
1354
1417
  // also answers from (rightkit#1297 L1): the probe and the real run cannot drift, and the door-route parity test compares the two.
1355
1418
  if (!o.doorSnapshot) {
1356
1419
  const whichRoute = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1357
- const routeExecutor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which: whichRoute, readFile: o.readFile });
1420
+ const routeExecutor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which: whichRoute, readFile: o.readFile });
1358
1421
  let decided;
1359
1422
  try { decided = routePlan(o, { to, stages: devTest ? [] : stages, devTest, executor: routeExecutor, stagePlatform: platform, capabilities: await brokerCapabilityStateWithHandshake(o, { to, executor: routeExecutor }), appConfig: selected && to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
1360
1423
  catch (e) { o.err(`${tag} ${e.message}`); return 2; }
@@ -1373,7 +1436,7 @@ async function runStages(o, args, root, git, io) {
1373
1436
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
1374
1437
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
1375
1438
  let own;
1376
- try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:${to}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit: selected.commit, platform: o.platform, env: o.env }); }
1439
+ try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(root).key }), stage: `door:${to}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit: selected.commit, platform: o.platform, env: o.env }); }
1377
1440
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 1; }
1378
1441
  o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
1379
1442
  try {
@@ -1400,7 +1463,7 @@ async function runStages(o, args, root, git, io) {
1400
1463
  if (!ds && (failed(dirty) || text(dirty).trim())) { o.err(`${tag} BLOCKED: the checkout differs from HEAD; every stage works on the commit, so commit or stash first`); return 1; }
1401
1464
 
1402
1465
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1403
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
1466
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
1404
1467
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
1405
1468
  const version = (cmd, a) => { const r = o.spawn(cmd, a, { cwd: root }); return failed(r) ? null : text(r).trim().split(/\r?\n/)[0]; };
1406
1469
  const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
@@ -1474,13 +1537,16 @@ async function runStages(o, args, root, git, io) {
1474
1537
  // the ones they sealed, so they are not run again.
1475
1538
  // The release configs of the commit (under this app) that declare `sidecars`: the commit's config decides on the snapshot route, never the checkout's.
1476
1539
  const sidecarConfigsOf = () => committedSidecarConfigs({ o, git, commit, appDir, base: ds ? ds.path : root });
1477
- const sealedNow = receipt.stages.package?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1540
+ // right-release keeps its outputs (`.right-release`: sealed sets, QA receipts, state) at the checkout's git root, never a nested app's
1541
+ // directory (build-release.mjs gitRootOf): every lookup of a sealed set or a QA receipt reads there.
1542
+ const releaseRoot = root;
1543
+ const sealedNow = receipt.stages.package?.ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1478
1544
  // The protected-receipt verifier pulls in the ownership policy modules (and their dependencies): loaded only for a run that qualifies or releases,
1479
1545
  // so an installed copy that only checks or compiles never needs them (GB-14).
1480
1546
  const qualifyReuse = stages.includes("qualify") || stages.includes("release") ? await import("../qualify-reuse.mjs") : null;
1481
1547
  const installedQaReceiptDigest = (...a) => qualifyReuse.installedQaReceiptDigest(...a);
1482
1548
  const qualifyReuseProblem = (...a) => qualifyReuse.qualifyReuseProblem(...a);
1483
- const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot: appRoot, platform, ...(o.verifyQa !== undefined ? { verify: o.verifyQa } : (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment" ? { verify: null } : {})) });
1549
+ const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot: releaseRoot, platform, ...(o.verifyQa !== undefined ? { verify: o.verifyQa } : (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment" ? { verify: null } : {})) });
1484
1550
  const sealedIntact = Boolean(sealedNow && sealedNow.id === receipt.stages.package.sealed && sealedNow.digest === receipt.stages.package.manifestSha256);
1485
1551
  // Every deterministic scan (full class A) runs before any compile, for every door stage. The app rules run at the tier the stage
1486
1552
  // owes: dev, test and package take the compile-tier rules; qualify and release add the release-only rules (approvals, ratified
@@ -1544,12 +1610,18 @@ async function runStages(o, args, root, git, io) {
1544
1610
  // On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
1545
1611
  if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
1546
1612
  else for (const config of ok ? sidecarConfigs : []) if (!await rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
1547
- let compiledDirs = [];
1548
- const reportOutputs = (dirs) => { compiledDirs = [...dirs]; };
1549
- if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs } : {}) })))({ reportOutputs })) === 0;
1613
+ // rightkit#1331: the declared compile inputs that are the app's web build output are produced in the snapshot before the compile reads them.
1614
+ if (ok && ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
1615
+ // The Windows package-identity MSIX a Tauri app bundles (its release config's windows.identity), produced in the snapshot on Windows only.
1616
+ if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
1617
+ let compiledDirs = []; let compiledBins = null; let qualifyOwed = [];
1618
+ const reportOutputs = (dirs, bins = null) => { compiledDirs = [...dirs]; compiledBins = bins; };
1619
+ if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, classAEarlier: true, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs, onQualifyOwed: (list) => { qualifyOwed = list; } } : {}) })))({ reportOutputs })) === 0;
1550
1620
  if (directReceipt) extra = { ...extra, direct: directReceipt };
1621
+ // What the plan left to the qualify stage is recorded with the compile, so qualification can prove it is not lost (rightkit#1332, #1333).
1622
+ if (qualifyOwed.length) extra = { ...extra, qualifyOwed };
1551
1623
  // What this compile produced, recorded with its result so a later run can prove the outputs are still there (GB-13).
1552
- if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env }) };
1624
+ if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env, builtBins: compiledBins }) };
1553
1625
  // The cross-check with `.rightkit/compile-inputs.json`: a declared input that covers ledger entries must carry the digest of those
1554
1626
  // very bytes in the compile receipt (the broker digests declared inputs in place in the snapshot, BC:39).
1555
1627
  if (ok && ds && directReceipt?.declaredInputDigests) {
@@ -1579,13 +1651,24 @@ async function runStages(o, args, root, git, io) {
1579
1651
  // GB-17: several Cargo workspaces: each of right-release's and its children's Cargo calls is bound to ITS workspace's target through the per-call wrapper.
1580
1652
  let packageMulti = null;
1581
1653
  if (ok && packagePin.dirs?.length > 1) {
1582
- packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1654
+ // The package stage builds the workspace that holds the app's release config (its Tauri app), never a test-only workspace beside it.
1655
+ const builtWorkspaces = [...new Set(sidecarConfigsOf().map((c) => path.posix.dirname(String(c).replaceAll("\\", "/"))))];
1656
+ packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), onlyWorkspaces: builtWorkspaces, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1583
1657
  if (packageMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packageMulti.refusal}; nothing was built`); ok = false; }
1584
1658
  else if (packageMulti.note) o.out(`${tag} [package] ${packageMulti.note}`);
1585
1659
  }
1586
1660
  const packageEnv = packagePin.dir ? { CARGO_TARGET_DIR: packagePin.dir } : (packageMulti?.env ?? {});
1587
- ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], packageEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
1588
- const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1661
+ // The QA shell the compile built is source QA's binary (rightkit#1309): the compile's recorded output directories are handed to `right-release qa` only
1662
+ // while that record still proves the outputs intact; it stages the declared layout from them in a run-scoped directory, never in the snapshot (qa-layout.mjs).
1663
+ const compileDirs = receipt.stages.compile?.outputs?.dirs;
1664
+ const compiledProblem = (o.compileReuseProblem ?? compileReuseProblem)(receipt.stages.compile, { env: o.env });
1665
+ // A direct host's suites (qa.layout.build) have no carried receipts: what they build lands in the output directories pinned here (test-command.mjs).
1666
+ const pinnedDirs = packagePin.dirs ?? (packagePin.dir ? [packagePin.dir] : []);
1667
+ const buildEnv = pinnedDirs.length ? { ...packageEnv, RIGHTKIT_GATE_TARGET_DIRS: JSON.stringify(pinnedDirs) } : packageEnv;
1668
+ const qaEnv = !mobile && !compiledProblem && Array.isArray(compileDirs) && compileDirs.length ? { ...buildEnv, RIGHT_RELEASE_QA_COMPILED_DIRS: JSON.stringify(compileDirs) } : buildEnv;
1669
+ if (ok && !mobile && compiledProblem && Array.isArray(compileDirs)) o.out(`${tag} [package] the compile outputs are not handed to source QA: ${compiledProblem}`);
1670
+ ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], qaEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
1671
+ const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1589
1672
  if (ok && !sealed) { o.err(`${tag} [package] the build reported success but sealed nothing for ${commit.slice(0, 8)} on ${platform}`); ok = false; }
1590
1673
  if (sealed) extra = { platform, sealed: sealed.id, manifestSha256: sealed.digest };
1591
1674
  // The cross-check: the Cargo targets the sealed manifest records are the directory this door resolved itself. A different one voids the stage.
@@ -1630,7 +1713,7 @@ async function runStages(o, args, root, git, io) {
1630
1713
  }
1631
1714
  } else {
1632
1715
  const sealed = receipt.stages.package?.ok ? receipt.stages.package : null;
1633
- const current = sealed ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1716
+ const current = sealed ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1634
1717
  if (!sealed || !current || current.id !== sealed.sealed || current.digest !== sealed.manifestSha256) {
1635
1718
  o.err(`${tag} BLOCKED: [${stage}] needs the sealed set the package stage produced for this commit, unchanged (sealed manifest digest)`);
1636
1719
  ok = false;
@@ -1649,9 +1732,14 @@ async function runStages(o, args, root, git, io) {
1649
1732
  ? (o.out(`${tag} [qualify] the release tier already passed at check for this exact input; not run again`), 0)
1650
1733
  : await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1651
1734
  if (releaseTier !== 0) { o.err(`${tag} [qualify] the release tier (every rule and scan) is red for ${commit.slice(0, 12)}; nothing was qualified`); ok = false; }
1652
- else ok = await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
1735
+ else {
1736
+ // rightkit#1332, #1333: what compile left to qualification (end-to-end crates, declared Swift packages) must have a declared run route here.
1737
+ const owedProblems = qualifyOwedProblem({ o, git, commit, appDir, base: ds ? ds.path : root, owed: receipt.stages.compile?.qualifyOwed ?? [] });
1738
+ for (const p of owedProblems) o.err(`${tag} BLOCKED: [qualify] ${p}`);
1739
+ ok = owedProblems.length === 0 && await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
1740
+ }
1653
1741
  // The protected installed-QA receipt this run produced is journaled by digest (GB-14), so the record can be re-verified before any reuse.
1654
- extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot: appRoot, releaseId: sealed.sealed, platform }) } : {}) };
1742
+ extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot: releaseRoot, releaseId: sealed.sealed, platform }) } : {}) };
1655
1743
  } else {
1656
1744
  if (!receipt.stages.qualify?.ok || receipt.stages.qualify.manifestSha256 !== sealed.manifestSha256) { o.err(`${tag} BLOCKED: [release] the sealed set was not qualified (installed QA) in this door run's receipt`); ok = false; }
1657
1745
  else if (qualifyRecordProblem(receipt.stages.qualify)) { o.err(`${tag} BLOCKED: [release] the recorded qualification no longer stands: ${qualifyRecordProblem(receipt.stages.qualify)}; nothing was uploaded`); ok = false; }
@@ -1699,7 +1787,9 @@ function stageForPublisher(o, args, root, git) {
1699
1787
  const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
1700
1788
  const receipt = (o.readReceipt ?? readReceipt)({ commonDir, commit, app: appDir });
1701
1789
  const pack = receipt?.stages?.package; const qual = receipt?.stages?.qualify;
1702
- const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1790
+ // right-release's outputs live at the checkout's git root (see runStages).
1791
+ const releaseRoot = root;
1792
+ const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1703
1793
  if (!pack?.ok || !qual?.ok || pack.platform !== platform || qual.manifestSha256 !== pack.manifestSha256 || !sealed || sealed.id !== pack.sealed || sealed.digest !== pack.manifestSha256) {
1704
1794
  o.err(`${tag} BLOCKED: stage needs this host's receipt for ${commit.slice(0, 12)} with package and qualify passed on ${platform} and the sealed set unchanged; run rightkit-gate --to qualify --platform ${platform} first`);
1705
1795
  return 1;
@@ -1711,7 +1801,7 @@ function stageForPublisher(o, args, root, git) {
1711
1801
  const receiptFile = path.join(work, `receipt-${platform}.json`);
1712
1802
  fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
1713
1803
  const archive = path.join(work, `sealed-${platform}.tar.gz`);
1714
- const tar = o.spawn("tar", ["-czf", archive, "-C", appRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
1804
+ const tar = o.spawn("tar", ["-czf", archive, "-C", releaseRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
1715
1805
  if (failed(tar)) { o.err(`${tag} could not pack ${sealed.id}/${platform}`); return 1; }
1716
1806
  const gh = (...a) => o.spawn("gh", a, { cwd: root });
1717
1807
  if (failed(gh("release", "view", releaseTag))) {
@@ -1782,7 +1872,12 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1782
1872
  const prior = new Map((ds.ledger?.entries ?? []).map((e) => [e.path, e]));
1783
1873
  const now = new Map(walked.entries.map((e) => [e.path, e]));
1784
1874
  const problems = [...walked.problems];
1785
- for (const [p, e] of prior) if (e.kind === "staged" && (!now.has(p) || entryKey(now.get(p)) !== entryKey(e))) problems.push(`${p} is a staged sidecar output and ${now.has(p) ? "changed" : "was removed"} after it was staged (a staged entry is immutable for the door run)`);
1875
+ // The package stage builds for the package triple (universal-apple-darwin on a Mac recipe with `stageSidecars`), so right-release's own
1876
+ // stage-sidecar may stage again there. A staged entry it changed or removed is admitted only through the receipt it wrote: a valid receipt
1877
+ // of the same config, every file it names present with that digest; the files it names become this stage's staged entries.
1878
+ const restaged = stage === "package" ? await restagedSidecars({ o, ds, prior, now }) : { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
1879
+ problems.push(...restaged.problems);
1880
+ for (const [p, e] of prior) if (e.kind === "staged" && !restaged.paths.has(p) && !restaged.receipts.has(p) && !(restaged.producers.has(e.producer?.step) && !now.has(p)) && (!now.has(p) || entryKey(now.get(p)) !== entryKey(e))) problems.push(`${p} is a staged sidecar output and ${now.has(p) ? "changed" : "was removed"} after it was staged (a staged entry is immutable for the door run)`);
1786
1881
  if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${p}`); return { ok: false, entries: ds.ledger?.entries ?? [] }; }
1787
1882
  // The compile's own verify (snapshot-check.mjs, 5.09d) returned the entries it admitted: they must be the entries this walk found.
1788
1883
  if (Array.isArray(ds.compileAdmitted) && stage === "compile") {
@@ -1793,6 +1888,7 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1793
1888
  ds.compileAdmitted = null;
1794
1889
  const entries = walked.entries.map((e) => {
1795
1890
  const was = prior.get(e.path);
1891
+ if (restaged.paths.has(e.path) || restaged.receipts.has(e.path)) return { ...e, kind: "staged", producer: { stage, step: "stage-sidecar" } };
1796
1892
  if (was?.kind === "staged") return { ...e, kind: "staged", producer: was.producer };
1797
1893
  return { ...e, kind: "generated", producer: was && entryKey(was) === entryKey(e) ? was.producer : { stage, step } };
1798
1894
  });
@@ -1809,6 +1905,39 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1809
1905
  return { ok: true, entries };
1810
1906
  }
1811
1907
 
1908
+ /**
1909
+ * The sidecar re-staging a package stage did, proven by its receipts: for each staged receipt (`<config dir>/.../stage-sidecar.json`) that
1910
+ * changed, the new receipt must be a valid stage-sidecar receipt and every file it names must be on disk with its recorded digest. Returns the
1911
+ * snapshot paths it names (`paths`), the receipts themselves (`receipts`), the producer steps whose earlier staged files it replaces
1912
+ * (`producers`, whose removed files are admitted) and the problems found.
1913
+ */
1914
+ async function restagedSidecars({ o, ds, prior, now }) {
1915
+ const { entryKey } = await import("../stage-ledger.mjs");
1916
+ const { validateStageSidecarReceipt } = await import("../stage-sidecar.mjs");
1917
+ const out = { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
1918
+ for (const [p, e] of prior) {
1919
+ if (e.kind !== "staged" || !/(^|\/)stage-sidecar\.json$/.test(p) || !now.has(p) || entryKey(now.get(p)) === entryKey(e)) continue;
1920
+ const config = e.producer?.step;
1921
+ const configDir = typeof config === "string" ? path.posix.dirname(config.replaceAll("\\", "/")) : ".";
1922
+ let receipt;
1923
+ try { receipt = validateStageSidecarReceipt(JSON.parse(String(o.readFile(path.join(ds.path, ...p.split("/")))))); }
1924
+ catch (error) { out.problems.push(`${p} was rewritten in the package stage but is not a valid stage-sidecar receipt (${error.message})`); continue; }
1925
+ const named = [];
1926
+ for (const file of receipt.files) {
1927
+ const rel = (configDir === "." ? file.path : `${configDir}/${file.path}`).replaceAll("\\", "/");
1928
+ const entry = now.get(rel);
1929
+ if (!entry) { out.problems.push(`${p} names ${rel}, which is not in the snapshot`); continue; }
1930
+ if (entry.sha256 !== undefined && entry.sha256 !== file.sha256) { out.problems.push(`${rel} does not have the digest ${p} records for it`); continue; }
1931
+ named.push(rel);
1932
+ }
1933
+ if (named.length !== receipt.files.length) continue;
1934
+ out.receipts.add(p);
1935
+ for (const rel of named) out.paths.add(rel);
1936
+ if (typeof config === "string") out.producers.add(config);
1937
+ }
1938
+ return out;
1939
+ }
1940
+
1812
1941
  /** The release configs of `commit` (under `appDir`) that declare `sidecars`, read from `base` (the snapshot on the snapshot route): the commit's config decides, never the checkout's. */
1813
1942
  export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
1814
1943
  const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
@@ -1848,7 +1977,11 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
1848
1977
  try { result = JSON.parse(String(r.stdout ?? "").trim()); } catch { o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar --json for ${config} did not print its result; its writes cannot be attributed`); return false; }
1849
1978
  let walked;
1850
1979
  try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
1851
- const underApp = (rel) => (appDir === "." ? rel : `${appDir}/${rel}`).replaceAll("\\", "/");
1980
+ // stage-sidecar reports its files relative to ITS app root, the directory of the config it was given (stage-sidecar-command.mjs
1981
+ // appRoot = dirname(config)), not the gate's appDir: a gate run at the repository root with the config in tauri-app-next/ would
1982
+ // otherwise find every staged file "not reported".
1983
+ const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
1984
+ const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`).replaceAll("\\", "/");
1852
1985
  const receiptAbs = typeof result.receiptPath === "string" ? path.relative(ds.path, result.receiptPath).split(path.sep).join("/") : null;
1853
1986
  const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs })];
1854
1987
  if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar ${config}: ${p}`); return false; }
@@ -1859,6 +1992,137 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
1859
1992
  return true;
1860
1993
  }
1861
1994
 
1995
+ /**
1996
+ * rightkit#1332, #1333: what the compile stage left to qualification must not be lost. Each owed item (an end-to-end crate whose tests need
1997
+ * the built app; a Swift package declared `qualifyOnly`) needs the run route the pipeline has for it: the app's release config declares
1998
+ * `qa.journeys`, so `right-release qa --stage installed` runs `rightkit-qa journeys run` against the sealed app and validates its receipt
1999
+ * (release-qa-command.mjs). Without it each owed item is named and qualification stops. Returns the problems (empty when nothing is owed or covered).
2000
+ */
2001
+ export function qualifyOwedProblem({ o, git, commit, appDir = ".", base, owed }) {
2002
+ if (!Array.isArray(owed) || !owed.length) return [];
2003
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
2004
+ const configs = (failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)));
2005
+ const declared = configs.some((f) => { try { return /\bqa\b[\s\S]*(^|[\s,{])["']?journeys["']?\s*:/m.test(o.readFile(path.join(base, f))); } catch { return false; } });
2006
+ if (declared) return [];
2007
+ return owed.map((x) => `${x.id} (${x.kind}${x.dir ? ` ${x.dir}` : ""}) is owed to qualification (${x.reason}) but no release config under ${appDir} declares qa.journeys, so nothing would run it; declare qa.journeys with a journeys.toml that covers it`);
2008
+ }
2009
+
2010
+ /** The release configs of `commit` (under `appDir`) that declare a `web` block (`web.build` steps), read from `base`: the commit's config decides, never the checkout's. */
2011
+ export function committedWebBuildConfigs({ o, git, commit, appDir = ".", base }) {
2012
+ const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
2013
+ return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
2014
+ .filter((f) => { try { return /(^|[\s,{])["']?web["']?\s*:\s*\{/.test(o.readFile(path.join(base, f))); } catch { return false; } });
2015
+ }
2016
+
2017
+ /**
2018
+ * rightkit#1331: the declared compile inputs (`.rightkit/compile-inputs.json`) that are an app's web build output (a Tauri `frontendDist`)
2019
+ * exist in the snapshot before the compile reads them. The broker's capture copies a declared path in from the checkout; the snapshot route
2020
+ * has no copy ("digested in place", GATE-CONTRACT GC-I14), so the door produces them the one way the contract allows: the app's own
2021
+ * `right-release web-build` (its release config's `web.build` steps) runs INSIDE the snapshot, once per door run, before the compile.
2022
+ * What it wrote may remain only under a declared input (declaredOutputProblems), ignored by the commit's own rules and recorded by this
2023
+ * gate's own walk as `generated` ledger entries (never `staged`: the package stage's own web build rewrites them). The compile's declared
2024
+ * input digest (compile-inputs.mjs, the broker's algorithm) then covers those very bytes and declaredInputProblems cross-checks it.
2025
+ * Nothing runs when every declared input is already present or the app declares no `web.build`; an input still absent afterwards is left
2026
+ * to the compile, which refuses a missing input and names it. Returns true when nothing is owed or the build passed.
2027
+ */
2028
+ export async function stageWebBuildOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
2029
+ if (ds.webBuilt) return true;
2030
+ ds.webBuilt = true;
2031
+ const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
2032
+ let declared;
2033
+ try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
2034
+ catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; } // a malformed declaration refuses in the compile, which names it
2035
+ const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
2036
+ if (!absent().length) return true;
2037
+ const configs = git ? committedWebBuildConfigs({ o, git, commit, appDir, base: ds.path }) : [];
2038
+ if (!configs.length) { o.out(`${tag} [${stage}] declared compile input(s) ${absent().join(", ")} are absent from the snapshot and no release config under ${appDir} declares web.build; the compile will report them missing`); return true; }
2039
+ const { declaredOutputProblems } = await import("../stage-ledger.mjs");
2040
+ const source = sourceSplitOf({ root, ds, appDir });
2041
+ const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
2042
+ for (const config of configs) {
2043
+ if (!absent().length) break;
2044
+ const before = ds.ledger?.entries ?? [];
2045
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
2046
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the web build did not run`); return false; }
2047
+ const run = startRun({ commonDir, commit, tree, stage, source });
2048
+ let r;
2049
+ try {
2050
+ o.out(`${tag} [${stage}] right-release web-build --config ${path.join(ds.path, config)} (from the snapshot ${ds.path}; declared input(s) ${absent().join(", ")} are absent)`);
2051
+ r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, "web-build", "--config", path.join(ds.path, config)], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
2052
+ } finally { endRun(run); }
2053
+ const carriedOk = await carrier.finish({ stage });
2054
+ if (!r || r.status !== 0) { o.err(`${tag} [${stage}] web-build for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
2055
+ if (!carriedOk) return false;
2056
+ let walked;
2057
+ try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
2058
+ const problems = [...walked.problems, ...declaredOutputProblems({ before, after: walked.entries, declared })];
2059
+ if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] web-build ${config}: ${p}`); return false; }
2060
+ const priorByPath = new Map(before.map((e) => [e.path, e]));
2061
+ ds.ledger = { entries: walked.entries.map((e) => (priorByPath.get(e.path)?.sha256 === e.sha256 ? priorByPath.get(e.path) : { ...e, kind: "generated", producer: { stage: "web-build", step: config } })) };
2062
+ }
2063
+ return true;
2064
+ }
2065
+
2066
+ /**
2067
+ * The Windows package-identity (sparse MSIX) an app bundles, produced INSIDE the snapshot before the compile, like stageWebBuildOnce: the
2068
+ * app's committed release config declares `windows.identity` (windows-identity-plan.mjs) and windows-identity.mjs (RightKit's msix tooling, the
2069
+ * provisioned signer; never app code) builds and signs it. Windows hosts only: elsewhere nothing runs and a declared input that stays absent is
2070
+ * left to the compile, which names it (platform scoping of the declared input itself is a separate decision). The child reports what it wrote
2071
+ * (`--json`); this gate's own walk must then find additions only under the producer's `out`, which must lie under a declared compile input
2072
+ * (identityOutputProblems). They become `generated` ledger entries, and the compile's declared-input digest covers those very bytes.
2073
+ * Nothing runs when every declared input is present or no committed config declares windows.identity (the package itself already being
2074
+ * present is the child's skip). Returns true when nothing is owed or the build passed.
2075
+ */
2076
+ export async function stageWindowsIdentityOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
2077
+ if (ds.identityBuilt) return true;
2078
+ ds.identityBuilt = true;
2079
+ const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
2080
+ let declared;
2081
+ try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
2082
+ catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; }
2083
+ const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
2084
+ if (!absent().length || !git) return true;
2085
+ const { committedWindowsIdentityConfigs } = await import("../windows-identity-plan.mjs");
2086
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
2087
+ const all = failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`));
2088
+ const configs = committedWindowsIdentityConfigs({ readFile: o.readFile, listConfigs: all, base: ds.path });
2089
+ if (!configs.length) return true;
2090
+ if ((o.platform ?? process.platform) !== "win32") { o.out(`${tag} [${stage}] ${configs.join(", ")} declares windows.identity; not a Windows host, so the identity package is not built here`); return true; }
2091
+ const { identityOutputProblems } = await import("../windows-identity-plan.mjs");
2092
+ const source = sourceSplitOf({ root, ds, appDir });
2093
+ const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
2094
+ const producer = path.join(path.dirname(cli), "..", "windows-identity.mjs");
2095
+ for (const config of configs) {
2096
+ const before = ds.ledger?.entries ?? [];
2097
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
2098
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the Windows identity package did not build`); return false; }
2099
+ const run = startRun({ commonDir, commit, tree, stage, source });
2100
+ let r;
2101
+ try {
2102
+ o.out(`${tag} [${stage}] windows-identity --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
2103
+ r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [producer, "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
2104
+ } finally { endRun(run); }
2105
+ const carriedOk = await carrier.finish({ stage });
2106
+ if (r?.stderr) o.err(String(r.stderr).trimEnd());
2107
+ if (!r || r.status !== 0) { o.err(`${tag} [${stage}] windows-identity for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
2108
+ if (!carriedOk) return false;
2109
+ let result;
2110
+ try { result = JSON.parse(String(r.stdout ?? "").trim()); } catch { o.err(`${tag} RK_CHECK_ERROR: [${stage}] windows-identity --json for ${config} did not print its result; its writes cannot be attributed`); return false; }
2111
+ let walked;
2112
+ try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
2113
+ const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
2114
+ const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`);
2115
+ let problems = walked.problems;
2116
+ if (result.skipped) problems = [...problems, ...diffProblems(before, walked.entries)];
2117
+ else problems = [...problems, ...identityOutputProblems({ before, after: walked.entries, out: underApp(result.out), declared, packageFile: result.package })];
2118
+ if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] windows-identity ${config}: ${p}`); return false; }
2119
+ const priorByPath = new Map(before.map((e) => [e.path, e]));
2120
+ ds.ledger = { entries: walked.entries.map((e) => (priorByPath.get(e.path)?.sha256 === e.sha256 ? priorByPath.get(e.path) : { ...e, kind: "generated", producer: { stage: "windows-identity", step: config } })) };
2121
+ }
2122
+ return true;
2123
+ }
2124
+ const diffProblems = (before, after) => { const b = new Map(before.map((e) => [e.path, e.sha256])); return after.filter((e) => b.get(e.path) !== e.sha256).map((e) => `${e.path} was written although the identity build reported a skip`); };
2125
+
1862
2126
  /** Hosted CI scans its committed runner checkout. Raw-tree verification defeats filters or a stale index hiding edits. */
1863
2127
  async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
1864
2128
  const head = git("rev-parse", "--verify", "HEAD^{commit}");
@@ -1873,7 +2137,7 @@ async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
1873
2137
  }
1874
2138
 
1875
2139
  /** Select output-binding workspaces from committed source before any Cargo metadata process runs. */
1876
- async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
2140
+ async function committedCargoManifests({ o, git, root, commit, scopeDirs, extraWorkspaces = [] }) {
1877
2141
  const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
1878
2142
  if (failed(listed)) throw new Error(`cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}`);
1879
2143
  const files = text(listed).split("\0").filter(Boolean).map(posix);
@@ -1900,7 +2164,9 @@ async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
1900
2164
  const inventory = discoverRustWorkspaces({ root, tracked, fixtures, readFile, metadata: sourceMetadata });
1901
2165
  const npm = discoverNpmUnits({ root, tracked, readFile, exists });
1902
2166
  const closure = tools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
1903
- const workspaces = [...new Set(inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir))].sort();
2167
+ // Workspaces the unit's own compile builds beside its closure (a crate's consumer rows under ci/consumers/): bound when the commit tracks their manifest.
2168
+ const extra = extraWorkspaces.filter((dir) => fileSet.has(`${dir}/Cargo.toml`));
2169
+ const workspaces = [...new Set([...inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir), ...extra])].sort();
1904
2170
  o.out(`rightkit-gate: Cargo output scope ${scopeDirs.join(", ")}: ${workspaces.length} workspace(s) (${workspaces.join(", ") || "none"}); dependency directories ${closure.dependencyDirs.join(", ")}`);
1905
2171
  return { manifests: workspaces.map((dir) => path.posix.join(dir, "Cargo.toml")), hasCargo: allManifests.length > 0 };
1906
2172
  }
@@ -1911,9 +2177,9 @@ async function cargoOutputPinForScope(options) {
1911
2177
  }
1912
2178
 
1913
2179
  /** Bind only committed closure workspaces to checkout outputs; runtime Cargo outside that map refuses. */
1914
- async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
2180
+ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs, extraWorkspaces = [], onlyWorkspaces = null }) {
1915
2181
  let selection;
1916
- try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs }); }
2182
+ try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs, extraWorkspaces }); }
1917
2183
  catch (error) { return { refusal: error.message }; }
1918
2184
  const { manifests, hasCargo } = selection;
1919
2185
  if (!hasCargo) return { env: {}, note: null, cleanup() {} };
@@ -1959,8 +2225,12 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1959
2225
  const workspaces = Object.keys(targetDirs);
1960
2226
  if (o.platform === "win32" && executor.kind !== "broker") {
1961
2227
  if (workspaces.length === 0) return { refusal: "the selected closure has no Cargo workspace; a direct Windows host cannot wrap undeclared Cargo calls" };
2228
+ // A stage that builds exactly one of them (the package stage builds the app's own workspace, the one holding its release config) pins
2229
+ // that workspace's target; the others are not built by it (HeardRight: tauri-app-next built, tauri-app-next/qa-e2e not).
2230
+ const only = Array.isArray(onlyWorkspaces) ? workspaces.filter((w) => onlyWorkspaces.includes(w)) : [];
2231
+ if (workspaces.length > 1 && only.length === 1) return { targetDirs, env: { CARGO_TARGET_DIR: targetDirs[only[0]] }, note: `output directory for the Cargo workspace ${only[0]}: ${targetDirs[only[0]]} (the checkout's); this stage builds no other workspace (${workspaces.filter((w) => w !== only[0]).join(", ")})`, cleanup() {} };
1962
2232
  if (workspaces.length > 1) return { refusal: `the commit holds ${workspaces.length} Cargo workspaces (${workspaces.join(", ")}) and a direct Windows host has no per-call wrapper to bind each to its own target directory` };
1963
- return { env: { CARGO_TARGET_DIR: Object.values(targetDirs)[0] }, note: `output directory for the Cargo workspace: ${Object.values(targetDirs)[0]} (the checkout's)`, cleanup() {} };
2233
+ return { targetDirs, env: { CARGO_TARGET_DIR: Object.values(targetDirs)[0] }, note: `output directory for the Cargo workspace: ${Object.values(targetDirs)[0]} (the checkout's)`, cleanup() {} };
1964
2234
  }
1965
2235
  const shimModule = await import("../cargo-target-shim.mjs");
1966
2236
  let broker = null; let realCargo = null;
@@ -1972,7 +2242,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1972
2242
  const { brokerSection } = await import("../snapshot-broker.mjs");
1973
2243
  broker = { checkoutDirs: Object.fromEntries(workspaces.map((w) => [w, path.join(checkoutReal, ...(w === "." ? [] : w.split("/")))])), section: (o.brokerSection ?? brokerSection)(sb) };
1974
2244
  } else {
1975
- realCargo = which("cargo").find((c) => path.isAbsolute(c));
2245
+ realCargo = realCargoOf(which("cargo"));
1976
2246
  if (!realCargo) return { refusal: "no cargo on PATH to wrap for the unit stages" };
1977
2247
  }
1978
2248
  let installed; let dir = null;
@@ -1982,6 +2252,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1982
2252
  const delta = Object.fromEntries(Object.entries(scan).filter(([k, v]) => o.env[k] !== v));
1983
2253
  const { CARRIED_CARGO_MAP_ENV } = await import("../carried-cargo.mjs");
1984
2254
  return {
2255
+ targetDirs, // { [cargoDir]: the checkout's target directory } on a direct host (null per workspace on a broker host, whose carried receipt names the output directory)
1985
2256
  env: { ...delta, ...(installed.mode === "broker" ? { [CARRIED_CARGO_MAP_ENV]: installed.mapFile } : {}) }, note: `runtime cargo wrapper ${installed.shim} (${installed.mode}): each cargo call a unit stage reaches binds its workspace's checkout target directory or is refused`,
1986
2257
  // Every call the wrapper decided is in the gate-owned ledger: a refused or unverified call fails the stage even when the test ignored cargo's exit (7.10, decision C).
1987
2258
  check: async () => { const { readLedger, UNSCOPED } = await import("../cargo-ledger.mjs"); return readLedger(installed.ledgerDir, UNSCOPED).problems ?? []; },
@@ -1991,7 +2262,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1991
2262
 
1992
2263
  /**
1993
2264
  * `--to <stage> --unit <selector> [--commit <rev>]` on the snapshot route (row 7.06a): select a commit (HEAD by default), take the
1994
- * host's build ownership, sync its snapshot, run class A of the snapshot (`check --commit <sha> --full-a --snapshot <dir>`), and build
2265
+ * repository's build ownership, sync its snapshot, run class A of the snapshot (`check --commit <sha> --full-a --snapshot <dir>`), and build
1995
2266
  * the unit from the snapshot (`--workspace <snapshot>`). Commit and tree come from the selection; no HEAD or status is read. The unit
1996
2267
  * manifest and the local-build grant are read from the selected commit, so one present only in the checkout is refused. Each stage is
1997
2268
  * bracketed by the ledger verify (7.08); Cargo's output is pinned to the checkout's target directory (7.09); `--to release` keeps its
@@ -2004,6 +2275,12 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2004
2275
  const match = /^(?:npm:@rightkit\/([a-z0-9-]+)|cargo:(rightkit-[a-z0-9-]+))$/.exec(selector);
2005
2276
  const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
2006
2277
  if (!manifest) { o.err(`${tag} BLOCKED: unit needs npm:@rightkit/<name> or cargo:rightkit-<name>`); return 1; }
2278
+ // `--to tool` (tool-build.mjs) builds and enrols one named tool binary; its unit and host are decided here, before ownership, a snapshot or any process.
2279
+ const toolRun = option("--to") === "tool";
2280
+ if (toolRun) {
2281
+ const { toolPlan } = await import("../tool-build.mjs");
2282
+ try { toolPlan({ selector, platform: o.platform }); } catch (e) { o.err(`${tag} BLOCKED: ${e.message}; nothing ran`); return 1; }
2283
+ }
2007
2284
  const snap = await import("../snapshot.mjs");
2008
2285
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
2009
2286
  let selected;
@@ -2013,7 +2290,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2013
2290
  // The canonical manifest and the local-build grant must be tracked in the SELECTED COMMIT (read with the object database, never the checkout).
2014
2291
  for (const file of [".rightkit-local-build.json", manifest]) if (failed(git("cat-file", "-e", `${commit}:${file}`))) { o.err(`${tag} BLOCKED: unit and local grant must be tracked canonical files of ${commit.slice(0, 12)} (${file} is not)`); return 1; }
2015
2292
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2016
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2293
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2017
2294
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2018
2295
  { const decided = routePlan(o, { to: option("--to"), stages: [], devTest: true, executor, capabilities: await brokerCapabilityStateWithHandshake(o, { to: option("--to"), executor }) });
2019
2296
  if (decided.refusal?.code === "RK_BROKER_CAPABILITY_MISSING") { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran`); return 1; } }
@@ -2023,7 +2300,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2023
2300
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2024
2301
  { const mismatch = await declaredSnapshotRootRefusal(o, executor, snapshotsAt); if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; } }
2025
2302
  let own;
2026
- try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:unit:${option("--to")}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit, platform: o.platform, env: o.env }); }
2303
+ try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(root).key }), stage: `door:unit:${option("--to")}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit, platform: o.platform, env: o.env }); }
2027
2304
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 1; }
2028
2305
  o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}`);
2029
2306
  try {
@@ -2035,7 +2312,14 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2035
2312
  // A unit's test or pack that reaches Cargo must never write `target/` into the snapshot (npm units included): see bindUnitCargo.
2036
2313
  // A check starts no unit command: its own Class A path binds read-only Cargo calls. Resolving every unit's compile output
2037
2314
  // here would run unrelated metadata before the scoped check even begins.
2038
- const bound = option("--to") === "check" ? { env: {}, cleanup() {} } : await bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs: [path.posix.dirname(manifest)] });
2315
+ // A crate's consumer rows (ci/gate.config.json rust.<crate>.consumerRows, local-publish.mjs) build their own Cargo workspaces under ci/consumers/: they
2316
+ // are part of this unit's compile, so they are bound beside the crate's workspace (read from the snapshot, which is the commit's tree).
2317
+ let consumerScope = [];
2318
+ if (match[2] && !toolRun) {
2319
+ try { consumerScope = [...new Set((JSON.parse(fs.readFileSync(path.join(ds.path, "ci", "gate.config.json"), "utf8")).rust?.[match[2]]?.consumerRows ?? []).map((row) => path.posix.dirname(String(row.manifestPath))))]; }
2320
+ catch { consumerScope = []; }
2321
+ }
2322
+ const bound = option("--to") === "check" ? { env: {}, cleanup() {} } : await bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs: [path.posix.dirname(manifest)], extraWorkspaces: consumerScope, ...(toolRun ? { onlyWorkspaces: [path.posix.dirname(manifest)] } : {}) });
2039
2323
  if (bound.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${bound.refusal}; nothing ran`); return 1; }
2040
2324
  if (bound.note) o.out(`${tag} ${bound.note}`);
2041
2325
  const envExtra = bound.env;
@@ -2045,17 +2329,27 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2045
2329
  const reportOut = option("--receipt-out");
2046
2330
  const { runUnitGate, resolveUnit } = await import("../unit-gate.mjs");
2047
2331
  const { localBuildGrantedAt } = await import("../local-publish.mjs");
2048
- return await runUnitGate({ selector, to: option("--to"), root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra,
2049
- resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
2050
- ...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
2051
- ...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
2052
- stageGuard: { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
2332
+ const stageGuard = { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
2053
2333
  if (!(await closeSnapshotStage({ o, ds, root, stage: `unit:${stage}`, step: `unit:${stage}`, tag })).ok) return false;
2054
2334
  const calls = bound.check ? await bound.check() : [];
2055
2335
  for (const p of calls.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [unit:${stage}] a cargo call the unit stage made did not complete: ${typeof p === "string" ? p : JSON.stringify(p)}`);
2056
2336
  return calls.length === 0;
2057
- } },
2058
- check: (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root, buildOwnership: own })))(tier), out: o.out, err: o.err,
2337
+ } };
2338
+ const runCheck = (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root, buildOwnership: own })))(tier);
2339
+ if (toolRun) {
2340
+ const { runUnitTool } = await import("../tool-build.mjs");
2341
+ return await runUnitTool({ selector, root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra, targetDirs: bound.targetDirs ?? {},
2342
+ resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
2343
+ ...(o.toolBuildDeps ?? {}), // test seam: run, enroll, dataRoot, readKnownRed
2344
+ stageGuard, check: runCheck, out: o.out, err: o.err,
2345
+ ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
2346
+ });
2347
+ }
2348
+ return await runUnitGate({ selector, to: option("--to"), root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra,
2349
+ resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
2350
+ ...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
2351
+ ...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
2352
+ stageGuard, check: runCheck, out: o.out, err: o.err,
2059
2353
  ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
2060
2354
  });
2061
2355
  } finally { bound.cleanup(); }
@@ -2095,7 +2389,7 @@ function sourceSplitOf({ root, ds, appDir }) {
2095
2389
  */
2096
2390
  export function snapshotRouteRefusal(o) {
2097
2391
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2098
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2392
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2099
2393
  if (executor.kind === "refuse") return { code: "RK_SNAPSHOT_BROKER_UNAVAILABLE", message: executor.problem };
2100
2394
  if (executor.kind === "broker" && !o.snapshotBroker) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `a ${o.platform} host compiles a commit snapshot only through the RustRight snapshot carrier (item 7 rows 7.20 to 7.24), and this gate was started without it` };
2101
2395
  return null;
@@ -2270,7 +2564,7 @@ async function runRouteProbe(o, args, root, git, { to, platform, stages, devTest
2270
2564
  try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
2271
2565
  catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2272
2566
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2273
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2567
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2274
2568
  let plan;
2275
2569
  const capabilities = await brokerCapabilityStateWithHandshake(o, { to, executor });
2276
2570
  try { plan = routePlan(o, { to, stages, devTest, executor, stagePlatform: platform, capabilities, appConfig: to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
@@ -2544,7 +2838,7 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
2544
2838
  if (failed(dirty) || text(dirty).trim()) { o.err(`${tag} BLOCKED: the checkout differs from HEAD; every stage works on the commit, so commit or stash first`); return 1; }
2545
2839
  }
2546
2840
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2547
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2841
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2548
2842
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2549
2843
  o.out(`${tag} --to ${to} for ${commit.slice(0, 12)}${appDir === "." ? "" : ` (app ${appDir})`}: check (class A) -> right-release ${to}; executor ${executor.kind}${ds ? `; snapshot ${ds.path}` : ""}`);
2550
2844
  // Full class A of the commit before right-release compiles anything: of the snapshot, the checks that execute repository code included.
@@ -2686,7 +2980,7 @@ export async function main(argv, io = {}) {
2686
2980
  // commit, never a diff. Nothing compiles. The RustRight broker runs exactly this before it admits any compile. `--list`,
2687
2981
  // `--print-baseline` and `--only` stay app-check-only views.
2688
2982
  // `check --commit <sha>` without `--snapshot` reads the gate's one warm snapshot of that commit, selected the way
2689
- // every `--to` stage selects it: host-wide build ownership first, then one sync of the commit into the gate-owned source directory, then the
2983
+ // every `--to` stage selects it: per-repository build ownership first, then one sync of the commit into the gate-owned source directory, then the
2690
2984
  // check of that snapshot below. There is no export and no copy of the source (docs/GATE-CONTRACT.md, 2026-10-29); the checkout's own files are
2691
2985
  // never read, so an uncommitted edit in it is ignored. Hosted CI has no warm snapshot: its runner checkout is the commit, scanned in place.
2692
2986
  // `check --commit <sha> --full-a` takes the same route (GB-48, GATE 34/36/44, E17): the selected commit's snapshot, never a conversion to
@@ -2706,7 +3000,7 @@ export async function main(argv, io = {}) {
2706
3000
  const selected = sc.commitOf(checkout, option("--commit") ?? "HEAD", o.spawn);
2707
3001
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2708
3002
  if (!own) {
2709
- try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: "check", snapshot: path.join(snapshotsAt, snap.repositoryOf(checkout).key), commit: selected.commit, platform: o.platform, env: o.env }); ownHere = true; }
3003
+ try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(checkout).key }), stage: "check", snapshot: path.join(snapshotsAt, snap.repositoryOf(checkout).key), commit: selected.commit, platform: o.platform, env: o.env }); ownHere = true; }
2710
3004
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`rightkit-gate: RK_CHECK_ERROR: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 2; }
2711
3005
  o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
2712
3006
  }
@@ -2722,12 +3016,19 @@ export async function main(argv, io = {}) {
2722
3016
  finally { if (ownHere) { const r = own.release(); if (r?.released === false && !own.joined) o.err(`rightkit-gate: build ownership ${own.handle} kept: ${r.why}`); } }
2723
3017
  }
2724
3018
  if ((sub === "check" || (!sub && args.includes("--all"))) && args.includes("--snapshot") && !o.buildOwnership) {
2725
- // Host-wide build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
3019
+ // Per-repository build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
2726
3020
  // end of the check, so no other request can sync the snapshot while class A runs in it. A nested gate whose
2727
3021
  // RIGHTKIT_BUILD_OWNERSHIP names the current owner joins it. Busy (or liveness unknown) runs nothing.
2728
3022
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
3023
+ const snap = await import("../snapshot.mjs");
3024
+ let key;
3025
+ try { key = snap.repositoryOf(option("--repo") ? path.resolve(o.cwd, option("--repo")) : o.cwd).key; }
3026
+ catch (e) { o.err(`rightkit-gate: RK_CHECK_ERROR: the repository whose build ownership this check takes cannot be identified: ${e.message}; nothing ran`); return 2; }
3027
+ // The record is keyed by the repository --repo/cwd names while --snapshot names a separate directory: they must be one repository.
3028
+ const snapshotRootHere = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
3029
+ if (path.basename(option("--snapshot") ?? "") !== key || path.resolve(o.cwd, path.dirname(option("--snapshot"))) !== path.resolve(o.cwd, snapshotRootHere)) { o.err(`rightkit-gate: RK_CHECK_ERROR: --snapshot ${option("--snapshot")} is not the snapshot of repository ${key} (its directory name must be the repository key, inside the snapshot root ${snapshotRootHere}); nothing ran`); return 2; }
2729
3030
  let own;
2730
- try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: sub ?? "compile", snapshot: option("--snapshot") ?? null, commit: option("--commit") ?? null, platform: o.platform, env: o.env }); }
3031
+ try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: key }), stage: sub ?? "compile", snapshot: option("--snapshot") ?? null, commit: option("--commit") ?? null, platform: o.platform, env: o.env }); }
2731
3032
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`rightkit-gate: RK_CHECK_ERROR: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 2; }
2732
3033
  o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
2733
3034
  try { return await main(argv, { ...io, buildOwnership: own }); }
@@ -2900,7 +3201,11 @@ export async function main(argv, io = {}) {
2900
3201
  const scopeDirs = args.flatMap((a, i) => a === "--scope" ? [args[i + 1]] : []);
2901
3202
  if (scopeDirs.length) {
2902
3203
  const bad = scopeDirs.find((d) => typeof d !== "string" || !d || d.startsWith("--") || path.isAbsolute(d) || /^[A-Za-z]:/.test(d) || /(^|[\\/])\.\.([\\/]|$)/.test(d));
2903
- if (!fullA || (!snapshotA && !hostedCommitA) || bad !== undefined || scopeDirs.some((d) => d === undefined)) { o.err("rightkit-gate: --scope needs full Class A of a snapshot or committed hosted CI checkout & repository-relative directories"); return 2; }
3204
+ // Full Class A needs a snapshot or the committed hosted checkout. The content pass (`check --commit <sha> --scope <dir>`: publication
3205
+ // admission, GC-A03) reads the commit the same way it always did (the warm snapshot, or the runner checkout proven to be that commit
3206
+ // in place) and only narrows which executing workspace scans run, so it is the one other form --scope accepts; never the live tree.
3207
+ const scopeForm = fullA ? Boolean(snapshotA || hostedCommitA) : classAOnly && !args.includes("--worktree");
3208
+ if (!scopeForm || bad !== undefined || scopeDirs.some((d) => d === undefined)) { o.err("rightkit-gate: --scope needs full Class A of a snapshot or committed hosted CI checkout & repository-relative directories"); return 2; }
2904
3209
  }
2905
3210
  if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
2906
3211
 
@@ -2981,7 +3286,7 @@ export async function main(argv, io = {}) {
2981
3286
  if (!e.code?.startsWith("RK_BROKER_EXCEPTION_")) { o.err(`rightkit-gate: cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2982
3287
  o.err(`rightkit-gate: BLOCKED: ${e.message}; nothing ran`); return e.code === BROKER_EXCEPTION_ERRORS.usage ? 2 : 1;
2983
3288
  }
2984
- const brokerResolver = o.resolveExecutor ?? resolveExecutor;
3289
+ const brokerResolver = o.resolveExecutor ?? gateResolveExecutor;
2985
3290
  o.resolveExecutor = (a) => brokerResolver({ ...a, forceDirect: true });
2986
3291
  o.brokerException = granted;
2987
3292
  io = { ...io, resolveExecutor: o.resolveExecutor, brokerException: granted };
@@ -3022,7 +3327,7 @@ export async function main(argv, io = {}) {
3022
3327
  const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
3023
3328
  if (!manifest || failed(git("ls-files", "--error-unmatch", "--", ".rightkit-local-build.json", manifest))) { o.err("rightkit-gate: BLOCKED: unit and local grant must be tracked canonical files"); return 1; }
3024
3329
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
3025
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3330
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3026
3331
  if (executor.kind === "refuse") { o.err(`rightkit-gate: BLOCKED: ${executor.problem}`); return 1; }
3027
3332
  const reportOut = option("--receipt-out");
3028
3333
  // Loaded here, not at the top: the unit path reads Cargo manifests (smol-toml), which an installed copy of this package without
@@ -3082,7 +3387,7 @@ export async function main(argv, io = {}) {
3082
3387
  // The executor is chosen here, never by the caller (Adrian, 2026-10-25): the RustRight broker where this machine has it, a refusal
3083
3388
  // on a broker-managed machine whose broker cannot be reached, direct cargo on any other host (a cloud container, a CI runner).
3084
3389
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
3085
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3390
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3086
3391
  // A broker-managed host whose broker is unreachable refuses the moment a Cargo manifest is in the snapshot (below): a repository
3087
3392
  // with no Rust owes nothing to the broker (a git hook's minimal PATH, a JS-only app), and the class A scans never compile.
3088
3393
  const cargoCommand = executor.cargo ?? ["cargo"];
@@ -3245,6 +3550,59 @@ export async function main(argv, io = {}) {
3245
3550
  // digested in the snapshot, and the stage's own timings.
3246
3551
  let outputResolution = {}; let directDeclared = []; let directDeclaredNotes = [];
3247
3552
  const directTiming = { bindStart: 0, bindEnd: 0, cargoStart: 0, cargoEnd: 0, afterStart: 0, afterEnd: 0, diagnostics: 0 };
3553
+ // The wrapper and each workspace's output directory, resolved before ANY scan runs (rightkit#1328): Class A runs once, before the compile,
3554
+ // under this wrapper and its per-scan sentinel, so a cargo that bypasses it fails the Class A row, not a later pass. Nothing compiles here.
3555
+ // `wrapRoot` is the verified snapshot, `wrapBroker` the broker of a broker host (the compile's, or the check's discovery broker).
3556
+ const bindCargoWrapper = async (wrapRoot, wrapBroker) => {
3557
+ targetDirs = {};
3558
+ // A scoped run (--unit or a nested app) wraps its closure's workspaces (GC-A03) and, always, every workspace inside a scope
3559
+ // directory: a unit's own workspace is wrapped even when closure membership names its crates differently.
3560
+ const inScope = (ws) => scopeDirs.some((d) => { const s = d.replace(/\/+$/, ""); return ws.cargoDir === s || ws.cargoDir.startsWith(`${s}/`) || s.startsWith(`${ws.cargoDir}/`); });
3561
+ const wrapped = closure ? rust.workspaces.filter((ws) => inScope(ws) || ws.units.some((u) => closure.rust.has(u.name))) : rust.workspaces;
3562
+ for (const ws of wrapBroker ? [] : wrapped) {
3563
+ const manifest = path.join(root, ws.cargoDir, "Cargo.toml");
3564
+ if (!o.exists(manifest)) { o.err(`${tag} BLOCKED: the checkout has no ${posix(path.relative(root, manifest))}, so the output directory of workspace ${ws.cargoDir} cannot be resolved; nothing compiled`); return 1; }
3565
+ // Resolved in the PRIMARY CHECKOUT with the stage's environment, before the source location changes (row 7.09: CARGO_TARGET_DIR,
3566
+ // a relative or absolute build.target-dir, or the default target/ all resolve as for a developer's own build); the cwd and
3567
+ // environment used are recorded. A directory inside the snapshot is refused before anything compiles (a second output).
3568
+ let resolved;
3569
+ try { resolved = resolveTargetDirectory({ spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, manifest, env: o.env, checkoutRoot: root, snapshotRoot: wrapRoot, workspace: ws.cargoDir }); }
3570
+ catch (error) { o.err(`${tag} BLOCKED: ${error.message}; nothing compiled`); return 1; }
3571
+ targetDirs[ws.cargoDir] = resolved.dir; outputResolution[ws.cargoDir] = resolved.inputs;
3572
+ o.out(`${tag} output directory for workspace ${ws.cargoDir}: ${targetDirs[ws.cargoDir]} (the checkout's)`);
3573
+ }
3574
+ // CARGO_HOME, sccache & temp are inherited as absolute values, never snapshot-relative (GATE-CONTRACT "Output directory & cache"):
3575
+ // a relative one would be resolved inside the snapshot, a second cache or output location. Refused before anything compiles.
3576
+ const relativeEnv = SNAPSHOT_ABSOLUTE_ENV.filter((name) => typeof o.env[name] === "string" && o.env[name] !== "" && !path.isAbsolute(o.env[name]));
3577
+ if (relativeEnv.length) { o.err(`${tag} BLOCKED: ${relativeEnv.map((n) => `${n}=${o.env[n]}`).join(", ")} ${relativeEnv.length > 1 ? "are" : "is"} relative; a snapshot compile inherits CARGO_HOME, sccache and temp only as absolute paths, never resolved inside the snapshot; nothing compiled`); return 1; }
3578
+ // The runtime cargo wrapper (item 7): first on PATH and named in CARGO for every scan, it binds each real cargo invocation to its
3579
+ // own workspace's checkout target directory or refuses it (RK_CARGO_*), whatever path reaches cargo. Direct POSIX hosts only (a
3580
+ // broker host never reaches here; a direct Windows host keeps the fail-closed static walk). Removed when the gate ends (cleanup).
3581
+ if (o.platform !== "win32" && rust.workspaces.length) {
3582
+ const realCargo = realCargoOf(which("cargo"));
3583
+ if (!realCargo && !wrapBroker) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return 1; }
3584
+ try {
3585
+ const shimModule = await import("../cargo-target-shim.mjs");
3586
+ let broker = null;
3587
+ if (wrapBroker) {
3588
+ // Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
3589
+ // map is the only source of a request's primaryCwd; each carried request records its sub-handle in the ownership just before it is sent.
3590
+ const checkoutRoot = fs.realpathSync.native(root);
3591
+ const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
3592
+ const { brokerSection } = await import("../snapshot-broker.mjs");
3593
+ broker = { checkoutDirs, section: brokerSection(wrapBroker) };
3594
+ }
3595
+ // Its own base directory: the wrapper's directory is named by its identity, and a unit stage's outer wrapper over the same
3596
+ // workspaces has the same identity, so a shared base would let this run's cleanup delete the wrapper the next stage runs under.
3597
+ const shimBase = fs.mkdtempSync(path.join(os.tmpdir(), "rk-gate-shim-"));
3598
+ cargoShimDir = shimBase;
3599
+ cargoShim = shimModule.installCargoShim({ base: shimBase, snapshotRoot: wrapRoot, targetDirs, broker, realCargo: realCargo ?? null, platform: o.platform });
3600
+ cargoShim.scanEnv = shimModule.shimScanEnv(o.env, cargoShim);
3601
+ } catch (error) { o.err(`${tag} BLOCKED: the runtime cargo wrapper cannot be installed (${error.message}); nothing compiled`); return 1; }
3602
+ o.out(`${tag} runtime cargo wrapper ${cargoShim.shim} (identity ${cargoShim.identity.slice(0, 16)}, ${cargoShim.mode}) -> ${cargoShim.mode === "broker" ? "the RustRight broker, one carried request per call" : realCargo}: each cargo invocation ${cargoShim.mode === "broker" ? "is carried with its own receipt, verified by the gate," : "binds its workspace's checkout target directory"} or is refused`);
3603
+ }
3604
+ return 0;
3605
+ };
3248
3606
  if (snapshotB) {
3249
3607
  const sc = await import("../snapshot-check.mjs");
3250
3608
  try {
@@ -3281,45 +3639,14 @@ export async function main(argv, io = {}) {
3281
3639
  } catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
3282
3640
  o.out(`${tag} broker snapshot compile: pins ${snapshotBroker.pins.keyId.slice(0, 12)} from ${snapshotBroker.pinsSource}, ${snapshotBroker.declaredInputs.length} declared input(s) read from the snapshot, ownership ${snapshotBroker.handle}`);
3283
3641
  }
3284
- targetDirs = {};
3285
- for (const ws of snapshotBroker ? [] : rust.workspaces) {
3286
- const manifest = path.join(root, ws.cargoDir, "Cargo.toml");
3287
- if (!o.exists(manifest)) { o.err(`${tag} BLOCKED: the checkout has no ${posix(path.relative(root, manifest))}, so the output directory of workspace ${ws.cargoDir} cannot be resolved; nothing compiled`); return 1; }
3288
- // Resolved in the PRIMARY CHECKOUT with the stage's environment, before the source location changes (row 7.09: CARGO_TARGET_DIR,
3289
- // a relative or absolute build.target-dir, or the default target/ all resolve as for a developer's own build); the cwd and
3290
- // environment used are recorded. A directory inside the snapshot is refused before anything compiles (a second output).
3291
- let resolved;
3292
- try { resolved = resolveTargetDirectory({ spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, manifest, env: o.env, checkoutRoot: root, snapshotRoot: snapshotB, workspace: ws.cargoDir }); }
3293
- catch (error) { o.err(`${tag} BLOCKED: ${error.message}; nothing compiled`); return 1; }
3294
- targetDirs[ws.cargoDir] = resolved.dir; outputResolution[ws.cargoDir] = resolved.inputs;
3295
- o.out(`${tag} output directory for workspace ${ws.cargoDir}: ${targetDirs[ws.cargoDir]} (the checkout's)`);
3296
- }
3297
- // CARGO_HOME, sccache & temp are inherited as absolute values, never snapshot-relative (GATE-CONTRACT "Output directory & cache"):
3298
- // a relative one would be resolved inside the snapshot, a second cache or output location. Refused before anything compiles.
3299
- const relativeEnv = SNAPSHOT_ABSOLUTE_ENV.filter((name) => typeof o.env[name] === "string" && o.env[name] !== "" && !path.isAbsolute(o.env[name]));
3300
- if (relativeEnv.length) { o.err(`${tag} BLOCKED: ${relativeEnv.map((n) => `${n}=${o.env[n]}`).join(", ")} ${relativeEnv.length > 1 ? "are" : "is"} relative; a snapshot compile inherits CARGO_HOME, sccache and temp only as absolute paths, never resolved inside the snapshot; nothing compiled`); return 1; }
3301
- // The runtime cargo wrapper (item 7): first on PATH and named in CARGO for every scan, it binds each real cargo invocation to its
3302
- // own workspace's checkout target directory or refuses it (RK_CARGO_*), whatever path reaches cargo. Direct POSIX hosts only (a
3303
- // broker host never reaches here; a direct Windows host keeps the fail-closed static walk). Removed when the gate ends (cleanup).
3304
- if (o.platform !== "win32" && rust.workspaces.length) {
3305
- const realCargo = which("cargo").find((c) => path.isAbsolute(c));
3306
- if (!realCargo && !snapshotBroker) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return 1; }
3307
- try {
3308
- const shimModule = await import("../cargo-target-shim.mjs");
3309
- let broker = null;
3310
- if (snapshotBroker) {
3311
- // Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
3312
- // map is the only source of a request's primaryCwd; each carried request records its sub-handle in the ownership just before it is sent.
3313
- const checkoutRoot = fs.realpathSync.native(root);
3314
- const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
3315
- const { brokerSection } = await import("../snapshot-broker.mjs");
3316
- broker = { checkoutDirs, section: brokerSection(snapshotBroker) };
3317
- }
3318
- cargoShim = shimModule.installCargoShim({ base: os.tmpdir(), snapshotRoot: snapshotB, targetDirs, broker, realCargo: realCargo ?? null, platform: o.platform, onDir: (dir) => { cargoShimDir = dir; } });
3319
- cargoShim.scanEnv = shimModule.shimScanEnv(o.env, cargoShim);
3320
- } catch (error) { o.err(`${tag} BLOCKED: the runtime cargo wrapper cannot be installed (${error.message}); nothing compiled`); return 1; }
3321
- o.out(`${tag} runtime cargo wrapper ${cargoShim.shim} (identity ${cargoShim.identity.slice(0, 16)}, ${cargoShim.mode}) -> ${cargoShim.mode === "broker" ? "the RustRight broker, one carried request per call" : realCargo}: each cargo invocation ${cargoShim.mode === "broker" ? "is carried with its own receipt, verified by the gate," : "binds its workspace's checkout target directory"} or is refused`);
3322
- }
3642
+ { const rc = await bindCargoWrapper(snapshotB, snapshotBroker); if (rc) return rc; }
3643
+ }
3644
+
3645
+ // Full class A of a snapshot (the door's check stage): the same wrapper, sentinel and ledger as the compile, installed BEFORE the scans so
3646
+ // the one Class A pass is the one that sees a cargo bypass (rightkit#1328). The compile pass runs class B only.
3647
+ if (!planOnly && fullA && snapshotA && rust.workspaces.length) {
3648
+ const rc = await bindCargoWrapper(snapshotA, executor.kind === "broker" ? discoveryBroker : null);
3649
+ if (rc) return rc;
3323
3650
  }
3324
3651
 
3325
3652
  // Class B scans (compile, lint, tests) run in the live checkout, so it must equal HEAD where they read it: an uncommitted fix could
@@ -3347,8 +3674,15 @@ export async function main(argv, io = {}) {
3347
3674
  const changed = allFilesMode ? files : [...new Set(text(diff).split(/\r?\n/).map((f) => f.trim()).filter(Boolean))].filter((f) => o.exists(path.join(repo, f)));
3348
3675
  if (allFilesMode) base = snapshot ?? "the working tree";
3349
3676
 
3350
- let crateFeatures = {}; let packageFeatures = {};
3351
- try { const gateConfig = JSON.parse(o.readFile(path.join(repo, "ci/gate.config.json"))); crateFeatures = gateConfig.rust ?? {}; packageFeatures = gateConfig.npm ?? {}; } catch { /* no config: default feature sets */ }
3677
+ let crateFeatures = {}; let packageFeatures = {}; let swiftPackages = {};
3678
+ try { const gateConfig = JSON.parse(o.readFile(path.join(repo, "ci/gate.config.json"))); crateFeatures = gateConfig.rust ?? {}; packageFeatures = gateConfig.npm ?? {}; swiftPackages = gateConfig.swift && typeof gateConfig.swift === "object" ? gateConfig.swift : {}; } catch { /* no config: default feature sets */ }
3679
+ // rightkit#1333 (P7): the end-to-end crates of the tree (a non-optional rightkit-qa dependency and a driver module), found from the files alone by the
3680
+ // same reader the G22 check rule uses; their test scans compile with --no-run and their tests are owed to the qualify stage (planScans e2eCrateDirs).
3681
+ let e2eCrateDirs = null;
3682
+ if (rust.units.length) {
3683
+ try { const { e2eCrates } = await import("../app-check.mjs"); e2eCrateDirs = new Set(e2eCrates(repo, files).crates.map((c) => c.dir)); }
3684
+ catch (e) { o.err(`${tag} BLOCKED: cannot tell which crates are end-to-end crates (${e.message}); their tests would run as class B unit tests, so nothing is planned`); return 1; }
3685
+ }
3352
3686
  // One lock and one lock scope per Cargo workspace. A workspace whose lock-only change reaches a subset puts only that subset's
3353
3687
  // units (by unit name) in scope; every other workspace keeps all of its units in scope (planScans reads the scope only for the
3354
3688
  // workspaces whose own root files changed).
@@ -3402,6 +3736,7 @@ export async function main(argv, io = {}) {
3402
3736
  // The door's snapshot compile already executes the repository's own code (its tests) in the verified snapshot, guarded like
3403
3737
  // full class A, so the docs generator runs there too instead of being deferred (step 4c review).
3404
3738
  quietTests: fullA || o.env.RK_GATE_QUIET_TESTS === "local" ? "local" : "ci", packageFeatures, liveExecution: (fullA && (args.includes("--worktree") || Boolean(snapshotA))) || Boolean(snapshotB),
3739
+ e2eCrateDirs, swiftPackages,
3405
3740
  rustUnits: rust.units, npmUnits: npm, npmRoots, cargoDir: ".", rustLocks, crateFeatures, crateDependents, externalInputs,
3406
3741
  // The broker (or, on a direct host, the gate itself) admits compiles; no separate grant file decides it here.
3407
3742
  localCompile: true,
@@ -3416,7 +3751,7 @@ export async function main(argv, io = {}) {
3416
3751
  if (closure) {
3417
3752
  let kept;
3418
3753
  try {
3419
- kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units });
3754
+ kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units, npmUnits: npm });
3420
3755
  plan = closureTools.scopedPlan(fullPlan, kept.scans, closure);
3421
3756
  } catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
3422
3757
  plan.notes = plan.notes.filter((n) => !n.includes("every Rust unit is in scope"));
@@ -3428,9 +3763,21 @@ export async function main(argv, io = {}) {
3428
3763
  const cargoExcluded = kept.dropped.filter((s) => closureTools.isCargoScan(s)).length;
3429
3764
  o.out(`${tag} scope ${closure.dirs.join(", ")}: dependency directories ${closure.dependencyDirs.join(", ")}; ${closure.npm.size} npm & ${closure.rust.size} Rust unit(s); Class A ${executing} executing scan(s) (${cargoRun} Cargo), ${classA.length - executing} repository file-only scan(s); ${kept.dropped.length} unrelated executing scan(s) excluded (${cargoExcluded} Cargo); ${plan.scans.length - classA.length} closure Class B scan(s) do not run in this check; install roots ${installRoots.join(", ") || "none"}; scope digest ${plan.scopeDigest}`);
3430
3765
  }
3766
+ // What the plan leaves to the qualify stage (end-to-end crates, declared Swift packages): handed to the door, which records it in the compile
3767
+ // receipt and refuses to qualify without a declared run route (rightkit#1332, #1333). Never dropped silently.
3768
+ o.onQualifyOwed?.((plan.qualifyOwed ?? []).filter((x) => x.kind !== "e2e-crate" || plan.scans.some((s) => s.id === x.id)));
3431
3769
  const jobs = Math.max(1, Number(o.env.RK_GATE_JOBS ?? 4) || 4);
3432
3770
  const testJobs = Math.max(1, Number(o.env.RK_GATE_TEST_JOBS ?? jobs) || jobs);
3433
- const classBJobs = Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
3771
+ // A broker host has no gate-side build count: the broker decides it. What remains is the local limit on class B processes the broker never sees.
3772
+ const brokerHost = executor.kind === "broker";
3773
+ const classBJobs = brokerHost ? LOCAL_CLASS_B_JOBS : Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
3774
+ // The broker's live state of a request (queued or running), asked through the executor's own client with an asynchronous spawn: a carried scan's run limit does not tick
3775
+ // while the broker holds it queued. No client to ask (none resolves): no reader, and a carried scan's limit simply runs, counting its queue time.
3776
+ // The client is the declared Linux route's, else the managed runtime client, else the executor's own: a broker host whose executor is the `rustright` found on PATH
3777
+ // (`["rustright", ...]`) asks through that one. No client to ask (a shim-only executor with none resolved): no reader, and the scan's limit simply runs.
3778
+ const executorClient = path.basename(String(cargoCommand[0] ?? "")).replace(/\.(exe|cmd|bat)$/i, "") === "rustright" ? (brokerClientOf(o, executor) ?? cargoCommand[0]) : null;
3779
+ const brokerClient = !brokerHost ? null : declaredClient ? "rustright" : managedRuntimeClient(o.env, o.platform, o.exists) ?? executorClient;
3780
+ const brokerLive = brokerClient ? createBrokerLive({ client: brokerClient, env: { ...o.env, ...(executor.env ?? {}) }, spawn: withDeclaredClient(defaultSpawnAsync), platform: o.platform }) : null;
3434
3781
 
3435
3782
  if (planOnly) {
3436
3783
  o.out(`${tag} plan: ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)} (reads the working tree; nothing runs)`);
@@ -3446,7 +3793,7 @@ export async function main(argv, io = {}) {
3446
3793
  for (const n of plan.notes) o.out(`${tag} note: ${n}`);
3447
3794
  o.out(`${tag} executor: ${executor.kind}${executor.kind === "broker" ? ` (${cargoCommand.join(" ")})` : " (no broker on this host: cargo runs directly, through the same gates)"}`);
3448
3795
  const quietCount = plan.scans.filter((s) => s.quiet === true).length;
3449
- o.out(`${tag} ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)}; class A scans ${jobs} at a time, class B scans ${classBJobs} component(s) at a time${quietCount ? `, ${quietCount} quiet test scan(s) last under the broker's quiet lease` : ""}`);
3796
+ o.out(`${tag} ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)}; class A scans ${jobs} at a time, ${brokerHost ? `class B: the broker queues and orders the carried scans of every component, no gate count (local test suites the broker does not see: ${classBJobs} at a time)` : `class B scans ${classBJobs} component(s) at a time`}${quietCount ? `, ${quietCount} quiet test scan(s) last under the broker's quiet lease` : ""}`);
3450
3797
  let signalCode = 0;
3451
3798
  for (const sig of ["SIGINT", "SIGTERM"]) {
3452
3799
  const handler = () => {
@@ -3520,6 +3867,8 @@ export async function main(argv, io = {}) {
3520
3867
  if (boundFullA) {
3521
3868
  const orphans = orphanedPnpmStores({ root: liveBase, files: live.files });
3522
3869
  if (orphans.length) { o.err(`${tag} BLOCKED: orphaned nested pnpm store(s) ${orphans.join(", ")}: left by a standalone install in a workspace member; they can hold stale dependencies and make concurrent vitest runs clear their shared module cache under each other. Move them aside (or reinstall) and run again.`); return 1; }
3870
+ const pinProblem = pnpmPinProblem({ root: liveBase, files: live.files, spawn: o.spawn, readFile: o.readFile });
3871
+ if (pinProblem) { o.err(`${tag} BLOCKED: ${pinProblem.message}; no class A row ran`); return 1; }
3523
3872
  try { envAtStart = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files: live.files, spawn: o.spawn, includeCaches: false }); envStartWithCaches = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files: live.files, spawn: o.spawn }); cachesAtStart = toolCacheManifest({ root: liveBase, files: live.files }); }
3524
3873
  catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
3525
3874
  }
@@ -3581,11 +3930,15 @@ export async function main(argv, io = {}) {
3581
3930
  if (enrolledTools.length) o.out(`${tag} scan-tool policy: ${enrolledTools.map((name) => `${name} (${scanTools.sources?.[name] ?? "enrolled"})`).join(", ")}`);
3582
3931
  // The direct compile's freshness census (row 7.19): the fingerprint units before the scans, and the fingerprint log of each cargo
3583
3932
  // process that compiles (CARGO_LOG on that process only, read from its own standard error). A broker host's census is the broker's.
3933
+ // The door's compile stage runs only after its check stage passed full class A of this same snapshot, once, before the compile and under the
3934
+ // cargo wrapper (rightkit#1328): this pass runs class B alone. Set by the door (io option), never by a command-line flag.
3935
+ const classAEarlier = Boolean(snapshotB && o.classAEarlier === true);
3936
+ if (classAEarlier) o.out(`${tag} class A is not repeated: it ran once before the compile, under the cargo wrapper, on this snapshot`);
3584
3937
  const census = snapshotB && !snapshotBroker ? createFreshnessCensus({ targetDirs }) : null;
3585
3938
  directTiming.cargoStart = Date.now();
3586
3939
  census?.start();
3587
3940
  const scanResult = await (o.runScans ?? runScans)(plan, {
3588
- classes: classAOnly ? ["A"] : ["A", "B"], jobs, testJobs, classBJobs, signal: ac.signal, ...(cache ? { cache } : {}),
3941
+ classes: classAOnly ? ["A"] : classAEarlier ? ["B"] : ["A", "B"], ...(classAEarlier ? { classAEarlier: true } : {}), jobs, testJobs, classBJobs, ...(brokerHost ? { brokerHost: true } : {}), ...(brokerLive ? { brokerLive } : {}), signal: ac.signal, ...(cache ? { cache } : {}),
3589
3942
  ...(census ? { spawn: census.wrapSpawn(defaultSpawnAsync) } : {}),
3590
3943
  // The declared Linux broker route: the scans are planned with the logical `rustright`, which this spawn (and scanSpawn's pass-through below) starts as the
3591
3944
  // executor's validated client, never a PATH lookup (withDeclaredClient).
@@ -3685,7 +4038,21 @@ export async function main(argv, io = {}) {
3685
4038
  // A broker host's evidence is the broker's own signed receipts (each carried scan's row.snapshotEvidence, verified above); the
3686
4039
  // gate writes no receipt that says `executor: direct` for it.
3687
4040
  // The output directories this compile's evidence names (GB-13): the broker's signed receipts' `outputDir`, or the direct host's resolved targets.
3688
- o.onCompileOutputs?.(snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir).filter(Boolean) : Object.values(targetDirs ?? {}).filter(Boolean));
4041
+ // With them, per output directory, the binaries this compile linked there (rustright#209 item 5). The plan has no `cargo build`: a bin is
4042
+ // linked only by a PASSED, not cached `cargo test -p <crate>` scan (test:rust:<unit>...) of a package with a selected integration test, which Cargo
4043
+ // rebuilds or proves fresh for this commit. A unit only checked, linted, documented, deferred to CI or without such a target counts for nothing.
4044
+ const unitByName = new Map(rust.units.map((u) => [u.name, u]));
4045
+ const known = new Set(unitByName.keys());
4046
+ const builtBins = {};
4047
+ const addBins = (dir, unit) => { if (dir && unit?.testLinksBins && unit.bins?.length) builtBins[dir] = [...new Set([...(builtBins[dir] ?? []), ...unit.bins])].sort(); };
4048
+ for (const r of scanResult.scans) {
4049
+ // A row answered from the scan cache compiled nothing in this run: the bytes in the target are not vouched for by it.
4050
+ if (r.status !== "pass" || r.cached === true || !/^test:rust(?:-serial)?:/.test(String(r.id))) continue;
4051
+ const unit = unitByName.get(rustUnitOfScanId(r.id, known));
4052
+ addBins(snapshotBroker ? r.snapshotEvidence?.outputDir : targetDirs?.[unit?.cargoDir ?? "."], unit);
4053
+ }
4054
+ const outputWorkspaces = (snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir) : Object.values(targetDirs ?? {})).filter(Boolean).map((dir) => [dir]);
4055
+ o.onCompileOutputs?.(outputWorkspaces.map(([dir]) => dir), builtBins);
3689
4056
  if (snapshotBroker) o.out(`${tag} broker compile: ${scanResult.scans.filter((r) => r.snapshotEvidence).length} carried request(s) with verified signed receipts; no direct receipt on a broker host`);
3690
4057
  else {
3691
4058
  // The direct host's own receipt (GATE-CONTRACT "Broker interface", row 7.18): the contract's returned-evidence names minus the