@rightkit/release 0.6.16 → 0.6.21

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 (65) 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 +481 -103
  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 +253 -42
  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 +114 -9
  52. package/snapshot-check.mjs +6 -1
  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-command.mjs +7 -1
  58. package/stage-sidecar.mjs +17 -3
  59. package/suite-signing.mjs +40 -4
  60. package/swift-lock.mjs +334 -0
  61. package/test-command.mjs +13 -3
  62. package/tool-build.mjs +156 -0
  63. package/unit-gate.mjs +9 -2
  64. package/windows-identity-plan.mjs +119 -0
  65. 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
@@ -1542,14 +1608,22 @@ async function runStages(o, args, root, git, io) {
1542
1608
  // `main --all` compiles. A broker host's stage-sidecar child reaches Cargo through the stage's carrier (carried-cargo.mjs).
1543
1609
  // right-release runs in appRoot (nested apps), so the repository-relative config is handed over absolute (Astra, second pass).
1544
1610
  // On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
1611
+ // rightkit#1331: the declared compile inputs that are the app's web build output are produced in the snapshot before ANY compile reads them,
1612
+ // the sidecar builds included: the broker admits a compile only when every declared input is in the snapshot (HeardRight Mac 2026-11-02:
1613
+ // stage-sidecar's cargo build REJECTED "snapshot admission: tauri-app-next/dist: No such file or directory").
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, for the same reason.
1616
+ if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
1545
1617
  if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
1546
1618
  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;
1619
+ let compiledDirs = []; let compiledBins = null; let qualifyOwed = [];
1620
+ const reportOutputs = (dirs, bins = null) => { compiledDirs = [...dirs]; compiledBins = bins; };
1621
+ 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
1622
  if (directReceipt) extra = { ...extra, direct: directReceipt };
1623
+ // What the plan left to the qualify stage is recorded with the compile, so qualification can prove it is not lost (rightkit#1332, #1333).
1624
+ if (qualifyOwed.length) extra = { ...extra, qualifyOwed };
1551
1625
  // 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 }) };
1626
+ if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env, builtBins: compiledBins }) };
1553
1627
  // The cross-check with `.rightkit/compile-inputs.json`: a declared input that covers ledger entries must carry the digest of those
1554
1628
  // very bytes in the compile receipt (the broker digests declared inputs in place in the snapshot, BC:39).
1555
1629
  if (ok && ds && directReceipt?.declaredInputDigests) {
@@ -1564,7 +1638,11 @@ async function runStages(o, args, root, git, io) {
1564
1638
  // compile was already passed (a separately invoked `--to package` re-stages after the sync, so it never meets a missing externalBin).
1565
1639
  // right-release then takes its identity from the selected commit and the verified snapshot (release-source-identity.mjs, row 7.04),
1566
1640
  // never the checkout's HEAD, status or branch; its outputs, state and the sealed directory stay in the checkout's `.right-release`.
1567
- ok = ds ? await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor }) : true;
1641
+ // The declared web build and Windows identity inputs first, as in the compile stage: the sidecar builds are broker-admitted only when every declared input is in the snapshot.
1642
+ ok = true;
1643
+ if (ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
1644
+ if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
1645
+ if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor });
1568
1646
  // Source QA (right-release qa --stage source) exists for mac and win only; a mobile package is the signed build alone.
1569
1647
  // Row 7.04c (R-T1, 7.09): the snapshot holds no Cargo target. On a direct host the checkout's output directory is resolved here, in the primary
1570
1648
  // checkout, and handed to right-release absolute: it binds every manifest's target for this stage, and the sealed manifest must record it back
@@ -1579,13 +1657,24 @@ async function runStages(o, args, root, git, io) {
1579
1657
  // 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
1658
  let packageMulti = null;
1581
1659
  if (ok && packagePin.dirs?.length > 1) {
1582
- packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1660
+ // The package stage builds the workspace that holds the app's release config (its Tauri app), never a test-only workspace beside it.
1661
+ const builtWorkspaces = [...new Set(sidecarConfigsOf().map((c) => path.posix.dirname(String(c).replaceAll("\\", "/"))))];
1662
+ packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), onlyWorkspaces: builtWorkspaces, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1583
1663
  if (packageMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packageMulti.refusal}; nothing was built`); ok = false; }
1584
1664
  else if (packageMulti.note) o.out(`${tag} [package] ${packageMulti.note}`);
1585
1665
  }
1586
1666
  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;
1667
+ // 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
1668
+ // 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).
1669
+ const compileDirs = receipt.stages.compile?.outputs?.dirs;
1670
+ const compiledProblem = (o.compileReuseProblem ?? compileReuseProblem)(receipt.stages.compile, { env: o.env });
1671
+ // 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).
1672
+ const pinnedDirs = packagePin.dirs ?? (packagePin.dir ? [packagePin.dir] : []);
1673
+ const buildEnv = pinnedDirs.length ? { ...packageEnv, RIGHTKIT_GATE_TARGET_DIRS: JSON.stringify(pinnedDirs) } : packageEnv;
1674
+ const qaEnv = !mobile && !compiledProblem && Array.isArray(compileDirs) && compileDirs.length ? { ...buildEnv, RIGHT_RELEASE_QA_COMPILED_DIRS: JSON.stringify(compileDirs) } : buildEnv;
1675
+ if (ok && !mobile && compiledProblem && Array.isArray(compileDirs)) o.out(`${tag} [package] the compile outputs are not handed to source QA: ${compiledProblem}`);
1676
+ ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], qaEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
1677
+ const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1589
1678
  if (ok && !sealed) { o.err(`${tag} [package] the build reported success but sealed nothing for ${commit.slice(0, 8)} on ${platform}`); ok = false; }
1590
1679
  if (sealed) extra = { platform, sealed: sealed.id, manifestSha256: sealed.digest };
1591
1680
  // 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 +1719,7 @@ async function runStages(o, args, root, git, io) {
1630
1719
  }
1631
1720
  } else {
1632
1721
  const sealed = receipt.stages.package?.ok ? receipt.stages.package : null;
1633
- const current = sealed ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1722
+ const current = sealed ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1634
1723
  if (!sealed || !current || current.id !== sealed.sealed || current.digest !== sealed.manifestSha256) {
1635
1724
  o.err(`${tag} BLOCKED: [${stage}] needs the sealed set the package stage produced for this commit, unchanged (sealed manifest digest)`);
1636
1725
  ok = false;
@@ -1649,9 +1738,14 @@ async function runStages(o, args, root, git, io) {
1649
1738
  ? (o.out(`${tag} [qualify] the release tier already passed at check for this exact input; not run again`), 0)
1650
1739
  : 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
1740
  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]);
1741
+ else {
1742
+ // rightkit#1332, #1333: what compile left to qualification (end-to-end crates, declared Swift packages) must have a declared run route here.
1743
+ const owedProblems = qualifyOwedProblem({ o, git, commit, appDir, base: ds ? ds.path : root, owed: receipt.stages.compile?.qualifyOwed ?? [] });
1744
+ for (const p of owedProblems) o.err(`${tag} BLOCKED: [qualify] ${p}`);
1745
+ ok = owedProblems.length === 0 && await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
1746
+ }
1653
1747
  // 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 }) } : {}) };
1748
+ extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot: releaseRoot, releaseId: sealed.sealed, platform }) } : {}) };
1655
1749
  } else {
1656
1750
  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
1751
  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 +1793,9 @@ function stageForPublisher(o, args, root, git) {
1699
1793
  const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
1700
1794
  const receipt = (o.readReceipt ?? readReceipt)({ commonDir, commit, app: appDir });
1701
1795
  const pack = receipt?.stages?.package; const qual = receipt?.stages?.qualify;
1702
- const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1796
+ // right-release's outputs live at the checkout's git root (see runStages).
1797
+ const releaseRoot = root;
1798
+ const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
1703
1799
  if (!pack?.ok || !qual?.ok || pack.platform !== platform || qual.manifestSha256 !== pack.manifestSha256 || !sealed || sealed.id !== pack.sealed || sealed.digest !== pack.manifestSha256) {
1704
1800
  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
1801
  return 1;
@@ -1711,7 +1807,7 @@ function stageForPublisher(o, args, root, git) {
1711
1807
  const receiptFile = path.join(work, `receipt-${platform}.json`);
1712
1808
  fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
1713
1809
  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))]);
1810
+ const tar = o.spawn("tar", ["-czf", archive, "-C", releaseRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
1715
1811
  if (failed(tar)) { o.err(`${tag} could not pack ${sealed.id}/${platform}`); return 1; }
1716
1812
  const gh = (...a) => o.spawn("gh", a, { cwd: root });
1717
1813
  if (failed(gh("release", "view", releaseTag))) {
@@ -1782,7 +1878,12 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1782
1878
  const prior = new Map((ds.ledger?.entries ?? []).map((e) => [e.path, e]));
1783
1879
  const now = new Map(walked.entries.map((e) => [e.path, e]));
1784
1880
  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)`);
1881
+ // The package stage builds for the package triple (universal-apple-darwin on a Mac recipe with `stageSidecars`), so right-release's own
1882
+ // stage-sidecar may stage again there. A staged entry it changed or removed is admitted only through the receipt it wrote: a valid receipt
1883
+ // of the same config, every file it names present with that digest; the files it names become this stage's staged entries.
1884
+ const restaged = stage === "package" ? await restagedSidecars({ o, ds, prior, now }) : { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
1885
+ problems.push(...restaged.problems);
1886
+ 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
1887
  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
1888
  // The compile's own verify (snapshot-check.mjs, 5.09d) returned the entries it admitted: they must be the entries this walk found.
1788
1889
  if (Array.isArray(ds.compileAdmitted) && stage === "compile") {
@@ -1793,6 +1894,7 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1793
1894
  ds.compileAdmitted = null;
1794
1895
  const entries = walked.entries.map((e) => {
1795
1896
  const was = prior.get(e.path);
1897
+ if (restaged.paths.has(e.path) || restaged.receipts.has(e.path)) return { ...e, kind: "staged", producer: { stage, step: "stage-sidecar" } };
1796
1898
  if (was?.kind === "staged") return { ...e, kind: "staged", producer: was.producer };
1797
1899
  return { ...e, kind: "generated", producer: was && entryKey(was) === entryKey(e) ? was.producer : { stage, step } };
1798
1900
  });
@@ -1809,6 +1911,39 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1809
1911
  return { ok: true, entries };
1810
1912
  }
1811
1913
 
1914
+ /**
1915
+ * The sidecar re-staging a package stage did, proven by its receipts: for each staged receipt (`<config dir>/.../stage-sidecar.json`) that
1916
+ * 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
1917
+ * snapshot paths it names (`paths`), the receipts themselves (`receipts`), the producer steps whose earlier staged files it replaces
1918
+ * (`producers`, whose removed files are admitted) and the problems found.
1919
+ */
1920
+ async function restagedSidecars({ o, ds, prior, now }) {
1921
+ const { entryKey } = await import("../stage-ledger.mjs");
1922
+ const { validateStageSidecarReceipt } = await import("../stage-sidecar.mjs");
1923
+ const out = { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
1924
+ for (const [p, e] of prior) {
1925
+ if (e.kind !== "staged" || !/(^|\/)stage-sidecar\.json$/.test(p) || !now.has(p) || entryKey(now.get(p)) === entryKey(e)) continue;
1926
+ const config = e.producer?.step;
1927
+ const configDir = typeof config === "string" ? path.posix.dirname(config.replaceAll("\\", "/")) : ".";
1928
+ let receipt;
1929
+ try { receipt = validateStageSidecarReceipt(JSON.parse(String(o.readFile(path.join(ds.path, ...p.split("/")))))); }
1930
+ catch (error) { out.problems.push(`${p} was rewritten in the package stage but is not a valid stage-sidecar receipt (${error.message})`); continue; }
1931
+ const named = [];
1932
+ for (const file of receipt.files) {
1933
+ const rel = (configDir === "." ? file.path : `${configDir}/${file.path}`).replaceAll("\\", "/");
1934
+ const entry = now.get(rel);
1935
+ if (!entry) { out.problems.push(`${p} names ${rel}, which is not in the snapshot`); continue; }
1936
+ if (entry.sha256 !== undefined && entry.sha256 !== file.sha256) { out.problems.push(`${rel} does not have the digest ${p} records for it`); continue; }
1937
+ named.push(rel);
1938
+ }
1939
+ if (named.length !== receipt.files.length) continue;
1940
+ out.receipts.add(p);
1941
+ for (const rel of named) out.paths.add(rel);
1942
+ if (typeof config === "string") out.producers.add(config);
1943
+ }
1944
+ return out;
1945
+ }
1946
+
1812
1947
  /** 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
1948
  export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
1814
1949
  const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
@@ -1848,7 +1983,11 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
1848
1983
  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
1984
  let walked;
1850
1985
  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("\\", "/");
1986
+ // stage-sidecar reports its files relative to ITS app root, the directory of the config it was given (stage-sidecar-command.mjs
1987
+ // appRoot = dirname(config)), not the gate's appDir: a gate run at the repository root with the config in tauri-app-next/ would
1988
+ // otherwise find every staged file "not reported".
1989
+ const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
1990
+ const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`).replaceAll("\\", "/");
1852
1991
  const receiptAbs = typeof result.receiptPath === "string" ? path.relative(ds.path, result.receiptPath).split(path.sep).join("/") : null;
1853
1992
  const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs })];
1854
1993
  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 +1998,137 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
1859
1998
  return true;
1860
1999
  }
1861
2000
 
2001
+ /**
2002
+ * 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
2003
+ * the built app; a Swift package declared `qualifyOnly`) needs the run route the pipeline has for it: the app's release config declares
2004
+ * `qa.journeys`, so `right-release qa --stage installed` runs `rightkit-qa journeys run` against the sealed app and validates its receipt
2005
+ * (release-qa-command.mjs). Without it each owed item is named and qualification stops. Returns the problems (empty when nothing is owed or covered).
2006
+ */
2007
+ export function qualifyOwedProblem({ o, git, commit, appDir = ".", base, owed }) {
2008
+ if (!Array.isArray(owed) || !owed.length) return [];
2009
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
2010
+ const configs = (failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)));
2011
+ 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; } });
2012
+ if (declared) return [];
2013
+ 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`);
2014
+ }
2015
+
2016
+ /** 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. */
2017
+ export function committedWebBuildConfigs({ o, git, commit, appDir = ".", base }) {
2018
+ const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
2019
+ return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
2020
+ .filter((f) => { try { return /(^|[\s,{])["']?web["']?\s*:\s*\{/.test(o.readFile(path.join(base, f))); } catch { return false; } });
2021
+ }
2022
+
2023
+ /**
2024
+ * rightkit#1331: the declared compile inputs (`.rightkit/compile-inputs.json`) that are an app's web build output (a Tauri `frontendDist`)
2025
+ * exist in the snapshot before the compile reads them. The broker's capture copies a declared path in from the checkout; the snapshot route
2026
+ * 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
2027
+ * `right-release web-build` (its release config's `web.build` steps) runs INSIDE the snapshot, once per door run, before the compile.
2028
+ * What it wrote may remain only under a declared input (declaredOutputProblems), ignored by the commit's own rules and recorded by this
2029
+ * gate's own walk as `generated` ledger entries (never `staged`: the package stage's own web build rewrites them). The compile's declared
2030
+ * input digest (compile-inputs.mjs, the broker's algorithm) then covers those very bytes and declaredInputProblems cross-checks it.
2031
+ * Nothing runs when every declared input is already present or the app declares no `web.build`; an input still absent afterwards is left
2032
+ * to the compile, which refuses a missing input and names it. Returns true when nothing is owed or the build passed.
2033
+ */
2034
+ export async function stageWebBuildOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
2035
+ if (ds.webBuilt) return true;
2036
+ ds.webBuilt = true;
2037
+ const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
2038
+ let declared;
2039
+ try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
2040
+ catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; } // a malformed declaration refuses in the compile, which names it
2041
+ const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
2042
+ if (!absent().length) return true;
2043
+ const configs = git ? committedWebBuildConfigs({ o, git, commit, appDir, base: ds.path }) : [];
2044
+ 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; }
2045
+ const { declaredOutputProblems } = await import("../stage-ledger.mjs");
2046
+ const source = sourceSplitOf({ root, ds, appDir });
2047
+ const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
2048
+ for (const config of configs) {
2049
+ if (!absent().length) break;
2050
+ const before = ds.ledger?.entries ?? [];
2051
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
2052
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the web build did not run`); return false; }
2053
+ const run = startRun({ commonDir, commit, tree, stage, source });
2054
+ let r;
2055
+ try {
2056
+ 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)`);
2057
+ 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 }) });
2058
+ } finally { endRun(run); }
2059
+ const carriedOk = await carrier.finish({ stage });
2060
+ if (!r || r.status !== 0) { o.err(`${tag} [${stage}] web-build for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
2061
+ if (!carriedOk) return false;
2062
+ let walked;
2063
+ try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
2064
+ const problems = [...walked.problems, ...declaredOutputProblems({ before, after: walked.entries, declared })];
2065
+ if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] web-build ${config}: ${p}`); return false; }
2066
+ const priorByPath = new Map(before.map((e) => [e.path, e]));
2067
+ 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 } })) };
2068
+ }
2069
+ return true;
2070
+ }
2071
+
2072
+ /**
2073
+ * The Windows package-identity (sparse MSIX) an app bundles, produced INSIDE the snapshot before the compile, like stageWebBuildOnce: the
2074
+ * app's committed release config declares `windows.identity` (windows-identity-plan.mjs) and windows-identity.mjs (RightKit's msix tooling, the
2075
+ * provisioned signer; never app code) builds and signs it. Windows hosts only: elsewhere nothing runs and a declared input that stays absent is
2076
+ * left to the compile, which names it (platform scoping of the declared input itself is a separate decision). The child reports what it wrote
2077
+ * (`--json`); this gate's own walk must then find additions only under the producer's `out`, which must lie under a declared compile input
2078
+ * (identityOutputProblems). They become `generated` ledger entries, and the compile's declared-input digest covers those very bytes.
2079
+ * Nothing runs when every declared input is present or no committed config declares windows.identity (the package itself already being
2080
+ * present is the child's skip). Returns true when nothing is owed or the build passed.
2081
+ */
2082
+ export async function stageWindowsIdentityOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
2083
+ if (ds.identityBuilt) return true;
2084
+ ds.identityBuilt = true;
2085
+ const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
2086
+ let declared;
2087
+ try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
2088
+ catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; }
2089
+ const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
2090
+ if (!absent().length || !git) return true;
2091
+ const { committedWindowsIdentityConfigs } = await import("../windows-identity-plan.mjs");
2092
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
2093
+ const all = failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`));
2094
+ const configs = committedWindowsIdentityConfigs({ readFile: o.readFile, listConfigs: all, base: ds.path });
2095
+ if (!configs.length) return true;
2096
+ 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; }
2097
+ const { identityOutputProblems } = await import("../windows-identity-plan.mjs");
2098
+ const source = sourceSplitOf({ root, ds, appDir });
2099
+ const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
2100
+ const producer = path.join(path.dirname(cli), "..", "windows-identity.mjs");
2101
+ for (const config of configs) {
2102
+ const before = ds.ledger?.entries ?? [];
2103
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
2104
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the Windows identity package did not build`); return false; }
2105
+ const run = startRun({ commonDir, commit, tree, stage, source });
2106
+ let r;
2107
+ try {
2108
+ o.out(`${tag} [${stage}] windows-identity --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
2109
+ 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 }) });
2110
+ } finally { endRun(run); }
2111
+ const carriedOk = await carrier.finish({ stage });
2112
+ if (r?.stderr) o.err(String(r.stderr).trimEnd());
2113
+ if (!r || r.status !== 0) { o.err(`${tag} [${stage}] windows-identity for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
2114
+ if (!carriedOk) return false;
2115
+ let result;
2116
+ 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; }
2117
+ let walked;
2118
+ try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
2119
+ const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
2120
+ const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`);
2121
+ let problems = walked.problems;
2122
+ if (result.skipped) problems = [...problems, ...diffProblems(before, walked.entries)];
2123
+ else problems = [...problems, ...identityOutputProblems({ before, after: walked.entries, out: underApp(result.out), declared, packageFile: result.package })];
2124
+ if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] windows-identity ${config}: ${p}`); return false; }
2125
+ const priorByPath = new Map(before.map((e) => [e.path, e]));
2126
+ 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 } })) };
2127
+ }
2128
+ return true;
2129
+ }
2130
+ 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`); };
2131
+
1862
2132
  /** Hosted CI scans its committed runner checkout. Raw-tree verification defeats filters or a stale index hiding edits. */
1863
2133
  async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
1864
2134
  const head = git("rev-parse", "--verify", "HEAD^{commit}");
@@ -1873,7 +2143,7 @@ async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
1873
2143
  }
1874
2144
 
1875
2145
  /** Select output-binding workspaces from committed source before any Cargo metadata process runs. */
1876
- async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
2146
+ async function committedCargoManifests({ o, git, root, commit, scopeDirs, extraWorkspaces = [] }) {
1877
2147
  const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
1878
2148
  if (failed(listed)) throw new Error(`cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}`);
1879
2149
  const files = text(listed).split("\0").filter(Boolean).map(posix);
@@ -1900,7 +2170,9 @@ async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
1900
2170
  const inventory = discoverRustWorkspaces({ root, tracked, fixtures, readFile, metadata: sourceMetadata });
1901
2171
  const npm = discoverNpmUnits({ root, tracked, readFile, exists });
1902
2172
  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();
2173
+ // 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.
2174
+ const extra = extraWorkspaces.filter((dir) => fileSet.has(`${dir}/Cargo.toml`));
2175
+ const workspaces = [...new Set([...inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir), ...extra])].sort();
1904
2176
  o.out(`rightkit-gate: Cargo output scope ${scopeDirs.join(", ")}: ${workspaces.length} workspace(s) (${workspaces.join(", ") || "none"}); dependency directories ${closure.dependencyDirs.join(", ")}`);
1905
2177
  return { manifests: workspaces.map((dir) => path.posix.join(dir, "Cargo.toml")), hasCargo: allManifests.length > 0 };
1906
2178
  }
@@ -1911,9 +2183,9 @@ async function cargoOutputPinForScope(options) {
1911
2183
  }
1912
2184
 
1913
2185
  /** 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 }) {
2186
+ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs, extraWorkspaces = [], onlyWorkspaces = null }) {
1915
2187
  let selection;
1916
- try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs }); }
2188
+ try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs, extraWorkspaces }); }
1917
2189
  catch (error) { return { refusal: error.message }; }
1918
2190
  const { manifests, hasCargo } = selection;
1919
2191
  if (!hasCargo) return { env: {}, note: null, cleanup() {} };
@@ -1959,8 +2231,12 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1959
2231
  const workspaces = Object.keys(targetDirs);
1960
2232
  if (o.platform === "win32" && executor.kind !== "broker") {
1961
2233
  if (workspaces.length === 0) return { refusal: "the selected closure has no Cargo workspace; a direct Windows host cannot wrap undeclared Cargo calls" };
2234
+ // A stage that builds exactly one of them (the package stage builds the app's own workspace, the one holding its release config) pins
2235
+ // that workspace's target; the others are not built by it (HeardRight: tauri-app-next built, tauri-app-next/qa-e2e not).
2236
+ const only = Array.isArray(onlyWorkspaces) ? workspaces.filter((w) => onlyWorkspaces.includes(w)) : [];
2237
+ 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
2238
  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() {} };
2239
+ 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
2240
  }
1965
2241
  const shimModule = await import("../cargo-target-shim.mjs");
1966
2242
  let broker = null; let realCargo = null;
@@ -1972,7 +2248,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1972
2248
  const { brokerSection } = await import("../snapshot-broker.mjs");
1973
2249
  broker = { checkoutDirs: Object.fromEntries(workspaces.map((w) => [w, path.join(checkoutReal, ...(w === "." ? [] : w.split("/")))])), section: (o.brokerSection ?? brokerSection)(sb) };
1974
2250
  } else {
1975
- realCargo = which("cargo").find((c) => path.isAbsolute(c));
2251
+ realCargo = realCargoOf(which("cargo"));
1976
2252
  if (!realCargo) return { refusal: "no cargo on PATH to wrap for the unit stages" };
1977
2253
  }
1978
2254
  let installed; let dir = null;
@@ -1982,6 +2258,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1982
2258
  const delta = Object.fromEntries(Object.entries(scan).filter(([k, v]) => o.env[k] !== v));
1983
2259
  const { CARRIED_CARGO_MAP_ENV } = await import("../carried-cargo.mjs");
1984
2260
  return {
2261
+ 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
2262
  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
2263
  // 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
2264
  check: async () => { const { readLedger, UNSCOPED } = await import("../cargo-ledger.mjs"); return readLedger(installed.ledgerDir, UNSCOPED).problems ?? []; },
@@ -1991,7 +2268,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1991
2268
 
1992
2269
  /**
1993
2270
  * `--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
2271
+ * repository's build ownership, sync its snapshot, run class A of the snapshot (`check --commit <sha> --full-a --snapshot <dir>`), and build
1995
2272
  * the unit from the snapshot (`--workspace <snapshot>`). Commit and tree come from the selection; no HEAD or status is read. The unit
1996
2273
  * 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
2274
  * 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 +2281,12 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2004
2281
  const match = /^(?:npm:@rightkit\/([a-z0-9-]+)|cargo:(rightkit-[a-z0-9-]+))$/.exec(selector);
2005
2282
  const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
2006
2283
  if (!manifest) { o.err(`${tag} BLOCKED: unit needs npm:@rightkit/<name> or cargo:rightkit-<name>`); return 1; }
2284
+ // `--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.
2285
+ const toolRun = option("--to") === "tool";
2286
+ if (toolRun) {
2287
+ const { toolPlan } = await import("../tool-build.mjs");
2288
+ try { toolPlan({ selector, platform: o.platform }); } catch (e) { o.err(`${tag} BLOCKED: ${e.message}; nothing ran`); return 1; }
2289
+ }
2007
2290
  const snap = await import("../snapshot.mjs");
2008
2291
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
2009
2292
  let selected;
@@ -2013,7 +2296,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2013
2296
  // The canonical manifest and the local-build grant must be tracked in the SELECTED COMMIT (read with the object database, never the checkout).
2014
2297
  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
2298
  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);
2299
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2017
2300
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2018
2301
  { const decided = routePlan(o, { to: option("--to"), stages: [], devTest: true, executor, capabilities: await brokerCapabilityStateWithHandshake(o, { to: option("--to"), executor }) });
2019
2302
  if (decided.refusal?.code === "RK_BROKER_CAPABILITY_MISSING") { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran`); return 1; } }
@@ -2023,7 +2306,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2023
2306
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2024
2307
  { const mismatch = await declaredSnapshotRootRefusal(o, executor, snapshotsAt); if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; } }
2025
2308
  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 }); }
2309
+ 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
2310
  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
2311
  o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}`);
2029
2312
  try {
@@ -2035,7 +2318,14 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2035
2318
  // A unit's test or pack that reaches Cargo must never write `target/` into the snapshot (npm units included): see bindUnitCargo.
2036
2319
  // A check starts no unit command: its own Class A path binds read-only Cargo calls. Resolving every unit's compile output
2037
2320
  // 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)] });
2321
+ // A crate's consumer rows (ci/gate.config.json rust.<crate>.consumerRows, local-publish.mjs) build their own Cargo workspaces under ci/consumers/: they
2322
+ // 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).
2323
+ let consumerScope = [];
2324
+ if (match[2] && !toolRun) {
2325
+ 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))))]; }
2326
+ catch { consumerScope = []; }
2327
+ }
2328
+ 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
2329
  if (bound.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${bound.refusal}; nothing ran`); return 1; }
2040
2330
  if (bound.note) o.out(`${tag} ${bound.note}`);
2041
2331
  const envExtra = bound.env;
@@ -2045,17 +2335,27 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
2045
2335
  const reportOut = option("--receipt-out");
2046
2336
  const { runUnitGate, resolveUnit } = await import("../unit-gate.mjs");
2047
2337
  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) => {
2338
+ const stageGuard = { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
2053
2339
  if (!(await closeSnapshotStage({ o, ds, root, stage: `unit:${stage}`, step: `unit:${stage}`, tag })).ok) return false;
2054
2340
  const calls = bound.check ? await bound.check() : [];
2055
2341
  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
2342
  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,
2343
+ } };
2344
+ 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);
2345
+ if (toolRun) {
2346
+ const { runUnitTool } = await import("../tool-build.mjs");
2347
+ 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 ?? {},
2348
+ resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
2349
+ ...(o.toolBuildDeps ?? {}), // test seam: run, enroll, dataRoot, readKnownRed
2350
+ stageGuard, check: runCheck, out: o.out, err: o.err,
2351
+ ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
2352
+ });
2353
+ }
2354
+ 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,
2355
+ resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
2356
+ ...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
2357
+ ...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
2358
+ stageGuard, check: runCheck, out: o.out, err: o.err,
2059
2359
  ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
2060
2360
  });
2061
2361
  } finally { bound.cleanup(); }
@@ -2095,7 +2395,7 @@ function sourceSplitOf({ root, ds, appDir }) {
2095
2395
  */
2096
2396
  export function snapshotRouteRefusal(o) {
2097
2397
  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 });
2398
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2099
2399
  if (executor.kind === "refuse") return { code: "RK_SNAPSHOT_BROKER_UNAVAILABLE", message: executor.problem };
2100
2400
  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
2401
  return null;
@@ -2270,7 +2570,7 @@ async function runRouteProbe(o, args, root, git, { to, platform, stages, devTest
2270
2570
  try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
2271
2571
  catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2272
2572
  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 });
2573
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2274
2574
  let plan;
2275
2575
  const capabilities = await brokerCapabilityStateWithHandshake(o, { to, executor });
2276
2576
  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 +2844,7 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
2544
2844
  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
2845
  }
2546
2846
  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);
2847
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
2548
2848
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2549
2849
  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
2850
  // Full class A of the commit before right-release compiles anything: of the snapshot, the checks that execute repository code included.
@@ -2574,6 +2874,11 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
2574
2874
  // app's own prepare step may stage them too; the ledger's attribution check, not an environment marker, is what keeps that single and exact.
2575
2875
  if (ds) {
2576
2876
  const sidecarConfigs = committedSidecarConfigs({ o, git, commit, appDir, base: ds.path });
2877
+ // The declared web build and Windows identity inputs first: the sidecar builds are broker-admitted only when every declared input is in the snapshot.
2878
+ const rr = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
2879
+ if (!(await stageWebBuildOnce({ o, ds, root, git, appDir, cli: rr, commonDir, commit, tree, stage: to, executor, tag })) || !(await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli: rr, commonDir, commit, tree, stage: to, executor, tag }))) {
2880
+ o.err(`${tag} --to ${to} stopped: the declared compile inputs could not be produced; nothing compiled`); return 1;
2881
+ }
2577
2882
  const staged = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli: fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url)), commonDir, commit, tree, stage: to, executor, tag });
2578
2883
  if (!staged) { o.err(`${tag} --to ${to} stopped: the declared sidecars could not be staged; nothing compiled`); return 1; }
2579
2884
  }
@@ -2686,7 +2991,7 @@ export async function main(argv, io = {}) {
2686
2991
  // commit, never a diff. Nothing compiles. The RustRight broker runs exactly this before it admits any compile. `--list`,
2687
2992
  // `--print-baseline` and `--only` stay app-check-only views.
2688
2993
  // `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
2994
+ // 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
2995
  // 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
2996
  // 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
2997
  // `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 +3011,7 @@ export async function main(argv, io = {}) {
2706
3011
  const selected = sc.commitOf(checkout, option("--commit") ?? "HEAD", o.spawn);
2707
3012
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2708
3013
  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; }
3014
+ 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
3015
  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
3016
  o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
2712
3017
  }
@@ -2722,12 +3027,19 @@ export async function main(argv, io = {}) {
2722
3027
  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
3028
  }
2724
3029
  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
3030
+ // Per-repository build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
2726
3031
  // end of the check, so no other request can sync the snapshot while class A runs in it. A nested gate whose
2727
3032
  // RIGHTKIT_BUILD_OWNERSHIP names the current owner joins it. Busy (or liveness unknown) runs nothing.
2728
3033
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
3034
+ const snap = await import("../snapshot.mjs");
3035
+ let key;
3036
+ try { key = snap.repositoryOf(option("--repo") ? path.resolve(o.cwd, option("--repo")) : o.cwd).key; }
3037
+ 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; }
3038
+ // The record is keyed by the repository --repo/cwd names while --snapshot names a separate directory: they must be one repository.
3039
+ const snapshotRootHere = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
3040
+ 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
3041
  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 }); }
3042
+ 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
3043
  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
3044
  o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
2733
3045
  try { return await main(argv, { ...io, buildOwnership: own }); }
@@ -2900,7 +3212,11 @@ export async function main(argv, io = {}) {
2900
3212
  const scopeDirs = args.flatMap((a, i) => a === "--scope" ? [args[i + 1]] : []);
2901
3213
  if (scopeDirs.length) {
2902
3214
  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; }
3215
+ // Full Class A needs a snapshot or the committed hosted checkout. The content pass (`check --commit <sha> --scope <dir>`: publication
3216
+ // admission, GC-A03) reads the commit the same way it always did (the warm snapshot, or the runner checkout proven to be that commit
3217
+ // in place) and only narrows which executing workspace scans run, so it is the one other form --scope accepts; never the live tree.
3218
+ const scopeForm = fullA ? Boolean(snapshotA || hostedCommitA) : classAOnly && !args.includes("--worktree");
3219
+ 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
3220
  }
2905
3221
  if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
2906
3222
 
@@ -2981,7 +3297,7 @@ export async function main(argv, io = {}) {
2981
3297
  if (!e.code?.startsWith("RK_BROKER_EXCEPTION_")) { o.err(`rightkit-gate: cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2982
3298
  o.err(`rightkit-gate: BLOCKED: ${e.message}; nothing ran`); return e.code === BROKER_EXCEPTION_ERRORS.usage ? 2 : 1;
2983
3299
  }
2984
- const brokerResolver = o.resolveExecutor ?? resolveExecutor;
3300
+ const brokerResolver = o.resolveExecutor ?? gateResolveExecutor;
2985
3301
  o.resolveExecutor = (a) => brokerResolver({ ...a, forceDirect: true });
2986
3302
  o.brokerException = granted;
2987
3303
  io = { ...io, resolveExecutor: o.resolveExecutor, brokerException: granted };
@@ -3022,7 +3338,7 @@ export async function main(argv, io = {}) {
3022
3338
  const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
3023
3339
  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
3340
  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);
3341
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3026
3342
  if (executor.kind === "refuse") { o.err(`rightkit-gate: BLOCKED: ${executor.problem}`); return 1; }
3027
3343
  const reportOut = option("--receipt-out");
3028
3344
  // Loaded here, not at the top: the unit path reads Cargo manifests (smol-toml), which an installed copy of this package without
@@ -3082,7 +3398,7 @@ export async function main(argv, io = {}) {
3082
3398
  // The executor is chosen here, never by the caller (Adrian, 2026-10-25): the RustRight broker where this machine has it, a refusal
3083
3399
  // on a broker-managed machine whose broker cannot be reached, direct cargo on any other host (a cloud container, a CI runner).
3084
3400
  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);
3401
+ const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
3086
3402
  // A broker-managed host whose broker is unreachable refuses the moment a Cargo manifest is in the snapshot (below): a repository
3087
3403
  // 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
3404
  const cargoCommand = executor.cargo ?? ["cargo"];
@@ -3245,6 +3561,59 @@ export async function main(argv, io = {}) {
3245
3561
  // digested in the snapshot, and the stage's own timings.
3246
3562
  let outputResolution = {}; let directDeclared = []; let directDeclaredNotes = [];
3247
3563
  const directTiming = { bindStart: 0, bindEnd: 0, cargoStart: 0, cargoEnd: 0, afterStart: 0, afterEnd: 0, diagnostics: 0 };
3564
+ // The wrapper and each workspace's output directory, resolved before ANY scan runs (rightkit#1328): Class A runs once, before the compile,
3565
+ // 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.
3566
+ // `wrapRoot` is the verified snapshot, `wrapBroker` the broker of a broker host (the compile's, or the check's discovery broker).
3567
+ const bindCargoWrapper = async (wrapRoot, wrapBroker) => {
3568
+ targetDirs = {};
3569
+ // A scoped run (--unit or a nested app) wraps its closure's workspaces (GC-A03) and, always, every workspace inside a scope
3570
+ // directory: a unit's own workspace is wrapped even when closure membership names its crates differently.
3571
+ const inScope = (ws) => scopeDirs.some((d) => { const s = d.replace(/\/+$/, ""); return ws.cargoDir === s || ws.cargoDir.startsWith(`${s}/`) || s.startsWith(`${ws.cargoDir}/`); });
3572
+ const wrapped = closure ? rust.workspaces.filter((ws) => inScope(ws) || ws.units.some((u) => closure.rust.has(u.name))) : rust.workspaces;
3573
+ for (const ws of wrapBroker ? [] : wrapped) {
3574
+ const manifest = path.join(root, ws.cargoDir, "Cargo.toml");
3575
+ 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; }
3576
+ // Resolved in the PRIMARY CHECKOUT with the stage's environment, before the source location changes (row 7.09: CARGO_TARGET_DIR,
3577
+ // a relative or absolute build.target-dir, or the default target/ all resolve as for a developer's own build); the cwd and
3578
+ // environment used are recorded. A directory inside the snapshot is refused before anything compiles (a second output).
3579
+ let resolved;
3580
+ try { resolved = resolveTargetDirectory({ spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, manifest, env: o.env, checkoutRoot: root, snapshotRoot: wrapRoot, workspace: ws.cargoDir }); }
3581
+ catch (error) { o.err(`${tag} BLOCKED: ${error.message}; nothing compiled`); return 1; }
3582
+ targetDirs[ws.cargoDir] = resolved.dir; outputResolution[ws.cargoDir] = resolved.inputs;
3583
+ o.out(`${tag} output directory for workspace ${ws.cargoDir}: ${targetDirs[ws.cargoDir]} (the checkout's)`);
3584
+ }
3585
+ // CARGO_HOME, sccache & temp are inherited as absolute values, never snapshot-relative (GATE-CONTRACT "Output directory & cache"):
3586
+ // a relative one would be resolved inside the snapshot, a second cache or output location. Refused before anything compiles.
3587
+ const relativeEnv = SNAPSHOT_ABSOLUTE_ENV.filter((name) => typeof o.env[name] === "string" && o.env[name] !== "" && !path.isAbsolute(o.env[name]));
3588
+ 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; }
3589
+ // The runtime cargo wrapper (item 7): first on PATH and named in CARGO for every scan, it binds each real cargo invocation to its
3590
+ // own workspace's checkout target directory or refuses it (RK_CARGO_*), whatever path reaches cargo. Direct POSIX hosts only (a
3591
+ // broker host never reaches here; a direct Windows host keeps the fail-closed static walk). Removed when the gate ends (cleanup).
3592
+ if (o.platform !== "win32" && rust.workspaces.length) {
3593
+ const realCargo = realCargoOf(which("cargo"));
3594
+ if (!realCargo && !wrapBroker) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return 1; }
3595
+ try {
3596
+ const shimModule = await import("../cargo-target-shim.mjs");
3597
+ let broker = null;
3598
+ if (wrapBroker) {
3599
+ // Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
3600
+ // 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.
3601
+ const checkoutRoot = fs.realpathSync.native(root);
3602
+ const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
3603
+ const { brokerSection } = await import("../snapshot-broker.mjs");
3604
+ broker = { checkoutDirs, section: brokerSection(wrapBroker) };
3605
+ }
3606
+ // Its own base directory: the wrapper's directory is named by its identity, and a unit stage's outer wrapper over the same
3607
+ // workspaces has the same identity, so a shared base would let this run's cleanup delete the wrapper the next stage runs under.
3608
+ const shimBase = fs.mkdtempSync(path.join(os.tmpdir(), "rk-gate-shim-"));
3609
+ cargoShimDir = shimBase;
3610
+ cargoShim = shimModule.installCargoShim({ base: shimBase, snapshotRoot: wrapRoot, targetDirs, broker, realCargo: realCargo ?? null, platform: o.platform });
3611
+ cargoShim.scanEnv = shimModule.shimScanEnv(o.env, cargoShim);
3612
+ } catch (error) { o.err(`${tag} BLOCKED: the runtime cargo wrapper cannot be installed (${error.message}); nothing compiled`); return 1; }
3613
+ 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`);
3614
+ }
3615
+ return 0;
3616
+ };
3248
3617
  if (snapshotB) {
3249
3618
  const sc = await import("../snapshot-check.mjs");
3250
3619
  try {
@@ -3281,45 +3650,14 @@ export async function main(argv, io = {}) {
3281
3650
  } catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
3282
3651
  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
3652
  }
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
- }
3653
+ { const rc = await bindCargoWrapper(snapshotB, snapshotBroker); if (rc) return rc; }
3654
+ }
3655
+
3656
+ // 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
3657
+ // the one Class A pass is the one that sees a cargo bypass (rightkit#1328). The compile pass runs class B only.
3658
+ if (!planOnly && fullA && snapshotA && rust.workspaces.length) {
3659
+ const rc = await bindCargoWrapper(snapshotA, executor.kind === "broker" ? discoveryBroker : null);
3660
+ if (rc) return rc;
3323
3661
  }
3324
3662
 
3325
3663
  // 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 +3685,15 @@ export async function main(argv, io = {}) {
3347
3685
  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
3686
  if (allFilesMode) base = snapshot ?? "the working tree";
3349
3687
 
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 */ }
3688
+ let crateFeatures = {}; let packageFeatures = {}; let swiftPackages = {};
3689
+ 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 */ }
3690
+ // 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
3691
+ // 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).
3692
+ let e2eCrateDirs = null;
3693
+ if (rust.units.length) {
3694
+ try { const { e2eCrates } = await import("../app-check.mjs"); e2eCrateDirs = new Set(e2eCrates(repo, files).crates.map((c) => c.dir)); }
3695
+ 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; }
3696
+ }
3352
3697
  // One lock and one lock scope per Cargo workspace. A workspace whose lock-only change reaches a subset puts only that subset's
3353
3698
  // units (by unit name) in scope; every other workspace keeps all of its units in scope (planScans reads the scope only for the
3354
3699
  // workspaces whose own root files changed).
@@ -3402,6 +3747,7 @@ export async function main(argv, io = {}) {
3402
3747
  // The door's snapshot compile already executes the repository's own code (its tests) in the verified snapshot, guarded like
3403
3748
  // full class A, so the docs generator runs there too instead of being deferred (step 4c review).
3404
3749
  quietTests: fullA || o.env.RK_GATE_QUIET_TESTS === "local" ? "local" : "ci", packageFeatures, liveExecution: (fullA && (args.includes("--worktree") || Boolean(snapshotA))) || Boolean(snapshotB),
3750
+ e2eCrateDirs, swiftPackages,
3405
3751
  rustUnits: rust.units, npmUnits: npm, npmRoots, cargoDir: ".", rustLocks, crateFeatures, crateDependents, externalInputs,
3406
3752
  // The broker (or, on a direct host, the gate itself) admits compiles; no separate grant file decides it here.
3407
3753
  localCompile: true,
@@ -3416,7 +3762,7 @@ export async function main(argv, io = {}) {
3416
3762
  if (closure) {
3417
3763
  let kept;
3418
3764
  try {
3419
- kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units });
3765
+ kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units, npmUnits: npm });
3420
3766
  plan = closureTools.scopedPlan(fullPlan, kept.scans, closure);
3421
3767
  } catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
3422
3768
  plan.notes = plan.notes.filter((n) => !n.includes("every Rust unit is in scope"));
@@ -3428,9 +3774,21 @@ export async function main(argv, io = {}) {
3428
3774
  const cargoExcluded = kept.dropped.filter((s) => closureTools.isCargoScan(s)).length;
3429
3775
  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
3776
  }
3777
+ // 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
3778
+ // receipt and refuses to qualify without a declared run route (rightkit#1332, #1333). Never dropped silently.
3779
+ o.onQualifyOwed?.((plan.qualifyOwed ?? []).filter((x) => x.kind !== "e2e-crate" || plan.scans.some((s) => s.id === x.id)));
3431
3780
  const jobs = Math.max(1, Number(o.env.RK_GATE_JOBS ?? 4) || 4);
3432
3781
  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);
3782
+ // 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.
3783
+ const brokerHost = executor.kind === "broker";
3784
+ const classBJobs = brokerHost ? LOCAL_CLASS_B_JOBS : Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
3785
+ // 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
3786
+ // 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.
3787
+ // 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
3788
+ // (`["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.
3789
+ const executorClient = path.basename(String(cargoCommand[0] ?? "")).replace(/\.(exe|cmd|bat)$/i, "") === "rustright" ? (brokerClientOf(o, executor) ?? cargoCommand[0]) : null;
3790
+ const brokerClient = !brokerHost ? null : declaredClient ? "rustright" : managedRuntimeClient(o.env, o.platform, o.exists) ?? executorClient;
3791
+ const brokerLive = brokerClient ? createBrokerLive({ client: brokerClient, env: { ...o.env, ...(executor.env ?? {}) }, spawn: withDeclaredClient(defaultSpawnAsync), platform: o.platform }) : null;
3434
3792
 
3435
3793
  if (planOnly) {
3436
3794
  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 +3804,7 @@ export async function main(argv, io = {}) {
3446
3804
  for (const n of plan.notes) o.out(`${tag} note: ${n}`);
3447
3805
  o.out(`${tag} executor: ${executor.kind}${executor.kind === "broker" ? ` (${cargoCommand.join(" ")})` : " (no broker on this host: cargo runs directly, through the same gates)"}`);
3448
3806
  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` : ""}`);
3807
+ 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
3808
  let signalCode = 0;
3451
3809
  for (const sig of ["SIGINT", "SIGTERM"]) {
3452
3810
  const handler = () => {
@@ -3520,6 +3878,8 @@ export async function main(argv, io = {}) {
3520
3878
  if (boundFullA) {
3521
3879
  const orphans = orphanedPnpmStores({ root: liveBase, files: live.files });
3522
3880
  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; }
3881
+ const pinProblem = pnpmPinProblem({ root: liveBase, files: live.files, spawn: o.spawn, readFile: o.readFile });
3882
+ if (pinProblem) { o.err(`${tag} BLOCKED: ${pinProblem.message}; no class A row ran`); return 1; }
3523
3883
  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
3884
  catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
3525
3885
  }
@@ -3581,11 +3941,15 @@ export async function main(argv, io = {}) {
3581
3941
  if (enrolledTools.length) o.out(`${tag} scan-tool policy: ${enrolledTools.map((name) => `${name} (${scanTools.sources?.[name] ?? "enrolled"})`).join(", ")}`);
3582
3942
  // The direct compile's freshness census (row 7.19): the fingerprint units before the scans, and the fingerprint log of each cargo
3583
3943
  // process that compiles (CARGO_LOG on that process only, read from its own standard error). A broker host's census is the broker's.
3944
+ // 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
3945
+ // cargo wrapper (rightkit#1328): this pass runs class B alone. Set by the door (io option), never by a command-line flag.
3946
+ const classAEarlier = Boolean(snapshotB && o.classAEarlier === true);
3947
+ if (classAEarlier) o.out(`${tag} class A is not repeated: it ran once before the compile, under the cargo wrapper, on this snapshot`);
3584
3948
  const census = snapshotB && !snapshotBroker ? createFreshnessCensus({ targetDirs }) : null;
3585
3949
  directTiming.cargoStart = Date.now();
3586
3950
  census?.start();
3587
3951
  const scanResult = await (o.runScans ?? runScans)(plan, {
3588
- classes: classAOnly ? ["A"] : ["A", "B"], jobs, testJobs, classBJobs, signal: ac.signal, ...(cache ? { cache } : {}),
3952
+ classes: classAOnly ? ["A"] : classAEarlier ? ["B"] : ["A", "B"], ...(classAEarlier ? { classAEarlier: true } : {}), jobs, testJobs, classBJobs, ...(brokerHost ? { brokerHost: true } : {}), ...(brokerLive ? { brokerLive } : {}), signal: ac.signal, ...(cache ? { cache } : {}),
3589
3953
  ...(census ? { spawn: census.wrapSpawn(defaultSpawnAsync) } : {}),
3590
3954
  // 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
3955
  // executor's validated client, never a PATH lookup (withDeclaredClient).
@@ -3685,7 +4049,21 @@ export async function main(argv, io = {}) {
3685
4049
  // A broker host's evidence is the broker's own signed receipts (each carried scan's row.snapshotEvidence, verified above); the
3686
4050
  // gate writes no receipt that says `executor: direct` for it.
3687
4051
  // 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));
4052
+ // With them, per output directory, the binaries this compile linked there (rustright#209 item 5). The plan has no `cargo build`: a bin is
4053
+ // 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
4054
+ // rebuilds or proves fresh for this commit. A unit only checked, linted, documented, deferred to CI or without such a target counts for nothing.
4055
+ const unitByName = new Map(rust.units.map((u) => [u.name, u]));
4056
+ const known = new Set(unitByName.keys());
4057
+ const builtBins = {};
4058
+ const addBins = (dir, unit) => { if (dir && unit?.testLinksBins && unit.bins?.length) builtBins[dir] = [...new Set([...(builtBins[dir] ?? []), ...unit.bins])].sort(); };
4059
+ for (const r of scanResult.scans) {
4060
+ // A row answered from the scan cache compiled nothing in this run: the bytes in the target are not vouched for by it.
4061
+ if (r.status !== "pass" || r.cached === true || !/^test:rust(?:-serial)?:/.test(String(r.id))) continue;
4062
+ const unit = unitByName.get(rustUnitOfScanId(r.id, known));
4063
+ addBins(snapshotBroker ? r.snapshotEvidence?.outputDir : targetDirs?.[unit?.cargoDir ?? "."], unit);
4064
+ }
4065
+ const outputWorkspaces = (snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir) : Object.values(targetDirs ?? {})).filter(Boolean).map((dir) => [dir]);
4066
+ o.onCompileOutputs?.(outputWorkspaces.map(([dir]) => dir), builtBins);
3689
4067
  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
4068
  else {
3691
4069
  // The direct host's own receipt (GATE-CONTRACT "Broker interface", row 7.18): the contract's returned-evidence names minus the