@rightkit/release 0.6.23 → 0.6.25

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 (59) hide show
  1. package/README.md +11 -35
  2. package/admission-host.mjs +5 -2
  3. package/android-dev.mjs +2 -1
  4. package/app-check.mjs +1 -2
  5. package/app-closure.mjs +3 -2
  6. package/assembled-app.mjs +11 -3
  7. package/bin/darwin/rightkit-swap +0 -0
  8. package/bin/darwin/rightkit-swap.sign.json +6 -0
  9. package/bin/rightkit-gate.mjs +83 -26
  10. package/build-helper-trace.mjs +264 -0
  11. package/build-helpers-command.mjs +184 -0
  12. package/build-launch.mjs +11 -75
  13. package/build-release.mjs +10 -11
  14. package/cargo-profile-contract.mjs +19 -29
  15. package/cargo-target-shim.mjs +31 -0
  16. package/cli/right-release.mjs +5 -7
  17. package/create-mac-updater.mjs +4 -0
  18. package/cross-targets.mjs +94 -0
  19. package/deterministic-scans.mjs +310 -20
  20. package/development.mjs +8 -8
  21. package/freshness-census.mjs +1 -1
  22. package/gate-stages.mjs +1 -1
  23. package/install-assembled-app.mjs +1 -1
  24. package/install-tauri-authenticode.mjs +3 -3
  25. package/local-publish.mjs +27 -5
  26. package/mac-package-recipe.mjs +55 -2
  27. package/managed-roots.mjs +12 -4
  28. package/package.json +1 -5
  29. package/pipeline-admission.mjs +1 -1
  30. package/pipeline-receipts.mjs +302 -302
  31. package/provenance-verify.mjs +3 -1
  32. package/qa-build-features.mjs +11 -2
  33. package/release-qa-audit.mjs +9 -27
  34. package/release-qa-command.mjs +23 -64
  35. package/release-qa-fixtures.mjs +9 -5
  36. package/release-qa-floor.json +0 -1
  37. package/release-qa-policy.mjs +9 -10
  38. package/release-qa-receipt.mjs +6 -44
  39. package/release-source-identity.mjs +1 -12
  40. package/release-switch-registry.json +4 -1
  41. package/rightkit-registry-resolver.mjs +35 -3
  42. package/rightkit-versions.json +2 -2
  43. package/sign-windows.mjs +1 -1
  44. package/snapshot-broker.mjs +33 -8
  45. package/snapshot-metadata.mjs +32 -0
  46. package/snapshot-request.mjs +6 -3
  47. package/stage-sidecar.mjs +30 -12
  48. package/suite-signing-policy.mjs +8 -58
  49. package/templates/cargo-profiles.toml +6 -25
  50. package/test-command.mjs +40 -140
  51. package/updater-shipped-key.mjs +26 -0
  52. package/web-build.mjs +29 -4
  53. package/qa-journeys-prebuilt-run.mjs +0 -18
  54. package/qa-journeys-prebuilt.mjs +0 -148
  55. package/qa-layout.mjs +0 -229
  56. package/qa-snapshot-run.mjs +0 -53
  57. package/qa-snapshot.mjs +0 -164
  58. package/sign-qa-bundle.mjs +0 -210
  59. package/suite-signing.mjs +0 -259
@@ -25,6 +25,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { spawn as spawnChild, spawnSync } from "./background-process.mjs";
26
26
  import { spawnUnderOwnership } from "./build-ownership.mjs";
27
27
  import { checkAdvisoryExceptions } from "./advisory-exceptions.mjs";
28
+ import { crossCompileEnv, crossTargetsFor, oneOutput } from "./cross-targets.mjs";
28
29
  import { CLIPPY_UNIVERSAL_FEATURE_FLAGS, canonicalJson, clippyBatchPlan, clippySelectionOf, digestScanSet, requiredScanIds, rustCratePlan, rustScanId } from "./scan-digest.mjs";
29
30
  import { reach } from "./lock-graph.mjs";
30
31
  import { cargoManifestState } from "./cargo-manifest-text.mjs";
@@ -33,7 +34,7 @@ import { PYTHON_LOCK_FILE, pythonLockProblems } from "./python-lock.mjs";
33
34
  import { lockOwnerOf, packageLockProblems, xcodeProjectLockProblems } from "./swift-lock.mjs";
34
35
  import { LAUNCH_GUARD_IMPORT } from "./class-a-launch-guard.mjs";
35
36
  import { advisoryCacheRoot, ensureAdvisoryDb, prepareDenyOverlay, readAdvisoryPin } from "./advisory-db.mjs";
36
- import { observedEnvOf, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest } from "./snapshot-request.mjs";
37
+ import { isInsideGenerationTarget, observedEnvOf, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest } from "./snapshot-request.mjs";
37
38
  import { SCAN_TOKEN_ENV, appendLedger, readLedger, scanTokenOf, unclaimedLedgers } from "./cargo-ledger.mjs";
38
39
  import { brokerSection, carryBrokerAdapterAsync, verifyBrokerLedger } from "./snapshot-broker.mjs";
39
40
  import { ADMISSION_POLL_MS, CARRIED_CANCEL_GRACE_MS, QUEUED_CEILING_FACTOR, createBrokerLive, createRunClock, createStatusPoller, watchBrokerAdmission } from "./broker-admission.mjs";
@@ -63,13 +64,14 @@ export const SCAN_CLASS = Object.freeze({ A: "A", B: "B" });
63
64
  export const UNOBSERVED_NESTED_CARGO_NOTE = "cargo calls that a build script makes through $CARGO are unobserved: cargo hands build scripts its own path, not the gate's wrapper, so they are not in the cargo ledger; they inherit the parent's bound CARGO_TARGET_DIR, and the per-scan sentinel and the after-compile snapshot verify are the backstops";
64
65
 
65
66
  /** The ledger token of the gate's own tool version probes (they reach the cargo wrapper like any scan does). */
67
+ const BUILD_HELPERS_COMMAND = fileURLToPath(new URL("./build-helpers-command.mjs", import.meta.url));
66
68
  const PROBE_LEDGER_TOKEN = scanTokenOf("tool-probe");
67
69
 
68
70
  /** Scan id prefixes (the text before the first ":") that belong to each class. */
69
71
  export const CLASS_A_SCAN_PREFIXES = Object.freeze([
70
72
  "rustfmt", "node-check", "json-parse", "toml-parse", "yaml-parse", "ownership-catalog",
71
73
  "actionlint", "shellcheck", "pnpm-lockfile", "swift-lock", "gradle-lock", "python-lock", "pnpm-audit", "cargo-metadata", "cargo-deny", "cargo-audit", "cargo-package-list",
72
- "changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions", "suite-api",
74
+ "changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions", "suite-api", "build-helpers",
73
75
  // Everything mechanical and deterministic that does not compile is class A (Adrian, 2026-10-27): typecheck, lint, generated docs.
74
76
  "tsc", "eslint", "biome", "generated-docs",
75
77
  ]);
@@ -154,7 +156,13 @@ const BUILD_ENV_NAMES = new Set(["CI", "GITHUB_ACTIONS", "CC", "CXX", "AR", "LD"
154
156
  "VCINSTALLDIR", "LIB", "INCLUDE", "LIBPATH", "CFLAGS", "CXXFLAGS", "LDFLAGS", "CPPFLAGS", "PKG_CONFIG_PATH", "PKG_CONFIG_LIBDIR", "OPENSSL_DIR", "OPENSSL_LIB_DIR", "OPENSSL_INCLUDE_DIR",
155
157
  "LIBCLANG_PATH", "LLVM_CONFIG_PATH", "ORT_LIB_LOCATION", "ORT_STRATEGY", "PROTOC", "WEBVIEW2_", "TAURI_"]);
156
158
  const BUILD_ENV_PREFIXES = ["CARGO_", "RUST", "SCCACHE_", "RK_", "RIGHTKIT_", "RUSTRIGHT_", "PNPM_", "NPM_CONFIG_", "CMAKE_", "CFLAGS_", "CXXFLAGS_", "OPENSSL_", "ORT_", "ONNX", "TAURI_", "WEBVIEW2_", "WINDOWS_SDK", "MSVC"];
157
- const BUILD_ENV_EXCLUDED = /^(RK_GATE_|RUSTRIGHT_QUIET_LEASE$)/;
159
+ // Per-run identities never key a result (they change on every run, so the cache could never hit; measured 2026-11-02: HR Class A wrote
160
+ // 3,809 entries and hit none across identical runs): the run's ownership handle, the attest nonce, the request-id file, the carried call's
161
+ // snapshot request and cargo map, the direct receipt path.
162
+ const BUILD_ENV_EXCLUDED = /^(RK_GATE_|RUSTRIGHT_QUIET_LEASE$|RIGHTKIT_BUILD_OWNERSHIP$|RIGHTKIT_CARRIED_CARGO_MAP$|RUSTRIGHT_ATTEST_NONCE$|RUSTRIGHT_REQUEST_ID_FILE$|RUSTRIGHT_SNAPSHOT_REQUEST$|RK_DIRECT_RECEIPT$)/;
163
+ // A per-run temporary directory inside a value (the runtime cargo wrapper lives under a fresh `rk-gate-shim-XXXXXX`) is normalised: the
164
+ // wrapper's own identity (`rk-cargo-shim-<identity>`) still keys the result, the random parent does not.
165
+ const normalizeRunTemp = (value) => String(value).replace(/rk-gate-shim-[A-Za-z0-9]+/g, "rk-gate-shim-*");
158
166
  export const buildEnvName = (name) => {
159
167
  const upper = String(name).toUpperCase();
160
168
  if (BUILD_ENV_EXCLUDED.test(upper)) return false;
@@ -163,7 +171,7 @@ export const buildEnvName = (name) => {
163
171
 
164
172
  /** sha256 of the build-relevant part (buildEnvName) of the environment a scan's child receives. Pass the merged `{ ...deps.env, ...scan.env }`. */
165
173
  export function inheritedEnvDigest(env = {}) {
166
- const rows = Object.entries(env).filter(([name, value]) => value !== undefined && value !== null && buildEnvName(name)).map(([name, value]) => [String(name).toUpperCase(), String(value)]);
174
+ const rows = Object.entries(env).filter(([name, value]) => value !== undefined && value !== null && buildEnvName(name)).map(([name, value]) => [String(name).toUpperCase(), normalizeRunTemp(value)]);
167
175
  rows.sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0));
168
176
  return sha256(canonicalJson(rows));
169
177
  }
@@ -202,9 +210,9 @@ export function fileScanCache(dir, { now = () => new Date(), fsApi = fs } = {})
202
210
 
203
211
  const packageFlags = (units) => [].concat(units).flatMap((unit) => ["-p", unit]);
204
212
 
205
- /** `cargo clippy -p <units> [--message-format=...] --no-deps --all-targets <features> [--locked] -- -D warnings` (argv after `cargo`). */
206
- export function clippyArgs(units, { features = [], messageFormat, noDeps = true, locked = false } = {}) {
207
- return ["clippy", ...packageFlags(units), ...(messageFormat ? [`--message-format=${messageFormat}`] : []), ...(noDeps ? ["--no-deps"] : []), "--all-targets", ...features, ...(locked ? ["--locked"] : []), "--", "-D", "warnings"];
213
+ /** `cargo clippy -p <units> [--message-format=...] --no-deps --all-targets [--target <t>] <features> [--locked] -- -D warnings` (argv after `cargo`). */
214
+ export function clippyArgs(units, { features = [], messageFormat, noDeps = true, locked = false, target = null } = {}) {
215
+ return ["clippy", ...packageFlags(units), ...(messageFormat ? [`--message-format=${messageFormat}`] : []), ...(noDeps ? ["--no-deps"] : []), "--all-targets", ...(target ? ["--target", target] : []), ...features, ...(locked ? ["--locked"] : []), "--", "-D", "warnings"];
208
216
  }
209
217
 
210
218
  /** `cargo check -p <unit> [--message-format=...] --all-targets <features> [--locked]`. */
@@ -849,6 +857,8 @@ export function declaredBrokerCall(unit, value) {
849
857
  export function planScans({
850
858
  changed = [], contentRoot, liveRoot = contentRoot, trustedRoot = null, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8"),
851
859
  platform = process.platform, includeTests = true, localCompile = true,
860
+ // The host's CPU architecture for its own Rust target (oneOutput, cross-targets.mjs).
861
+ nativeArch = process.arch,
852
862
  // Where the quiet test scans (ci/gate.config.json `quietTests`) run: "local" plans them for the quiet lane; "ci" leaves them to the
853
863
  // CI shards (one crate per runner, where the port, pipe, desktop and wall-clock conflicts that force the serial lane do not exist)
854
864
  // and records a note naming them (Adrian 2026-10-22: compile locally on the warm cache, qualify the conflicting tests on VMs).
@@ -981,9 +991,18 @@ export function planScans({
981
991
  const plainDir = (d) => !ABSOLUTE_PATH.test(d) && !d.split("/").includes("..") && SAFE_UNIT_DIR.test(d);
982
992
  const cargoWs = cleanDir(cargoDir);
983
993
  if (!plainDir(cargoWs)) throw new TypeError("planScans: cargoDir must be a plain repository-relative directory");
984
- const cargoBase = (ws, args) => ({ tool: cargoCommand[0], args: [...cargoCommand.slice(1), ...everyFailure(args)], ...(ws !== "." ? { cwd: ws } : {}), ...(targetDirs?.[ws] ? { env: { CARGO_TARGET_DIR: targetDirs[ws] } } : {}) });
994
+ // Every compile lands in the one output directory (oneOutput, cross-targets.mjs): release profile, the host's target unless another is named.
995
+ // A refused argv (RK_PROFILE_REFUSED, RK_TEST_MODE_REFUSED) becomes a scan that fails with the refusal: in scope it fails the run, out of
996
+ // scope it is dropped with the rest, and it never aborts planning for the whole repository.
997
+ const refusedScan = (error) => ({ tool: "node", args: ["-e", `console.error(${JSON.stringify(String(error.message))}); process.exit(1)`] });
998
+ const cargoBase = (ws, args, { raw = false } = {}) => { let argv; try { argv = oneOutput(raw ? args : everyFailure(args), { platform, arch: nativeArch }); } catch (error) { return { ...refusedScan(error), ...(ws !== "." ? { cwd: ws } : {}) }; } return { tool: cargoCommand[0], args: [...cargoCommand.slice(1), ...argv], ...(ws !== "." ? { cwd: ws } : {}), ...(targetDirs?.[ws] ? { env: { CARGO_TARGET_DIR: targetDirs[ws] } } : {}) }; };
985
999
  // Class B (compiles): started from the primary checkout on a broker host, the snapshot carrier names the source.
986
1000
  const cargoIn = (ws, ...args) => ({ ...cargoBase(ws, args), ...(cargoLiveRoot ? { root: "cargoLive" } : {}) });
1001
+ // A Rust test scan on a BROKER host (cargoLiveRoot set; GATE-CONTRACT GC-A08): the broker compiles with the plain test command plus --locked and
1002
+ // --message-format=json (the broker adds --no-run itself and never executes a test), and the gate runs the listed executables outside it. --no-fail-fast and
1003
+ // --test-threads=1 are RUN arguments (runOutside.args), never part of the compile. A direct host (no broker, cargoLiveRoot unset) has nothing to keep tests
1004
+ // out of: its test scan stays the one-step `test ... --locked --no-fail-fast`, compile and run together, with no runOutside.
1005
+ const cargoTestIn = (ws, ...args) => ({ ...cargoBase(ws, [...args, "--message-format=json"], { raw: true }), root: "cargoLive" });
987
1006
  // Class A (reads Cargo inputs, compiles nothing): always the content root, the snapshot or the commit's own tree. These still execute
988
1007
  // cargo (through the broker on a broker host), so a unit-scoped check follows the Rust closure for them (app-closure.mjs scopeScans reads
989
1008
  // the ids `cargo-metadata|cargo-deny|cargo-audit[:<label>]` and `cargo-package-list:<unit>` and the scan's `cwd`; rename one there too).
@@ -1228,6 +1247,11 @@ export function planScans({
1228
1247
  }
1229
1248
  if (rustTouched) {
1230
1249
  for (const ws of touchedWs) add({ id: `cargo-metadata${wsIdSuffix(ws)}`, class: A, ...cargoReadIn(ws, ...metadataArgs()) });
1250
+ // rightkit#1450 (Adrian, 2026-11-02): every build script and proc macro of the workspace must have been traced once on this host, writing only
1251
+ // inside its build directories; an untraced or out-of-bounds unit refuses Class A, naming it. Runs the gate's own command, directly (GC-A11).
1252
+ // It runs in its workspace (cwd), so a unit's scope keeps it only for the Cargo workspaces of its own closure (app-closure CARGO_WORKSPACE_SCAN),
1253
+ // like metadata, deny and audit: an npm-only unit traces nothing.
1254
+ for (const ws of touchedWs) add({ id: `build-helpers${wsIdSuffix(ws)}`, class: A, tool: "node", args: [BUILD_HELPERS_COMMAND, "check", "--manifest-path", "Cargo.toml", ...(platform === "linux" ? ["--trace-missing"] : [])], ...(ws !== "." ? { cwd: ws } : {}), root: "content" });
1231
1255
  // Locked package listings are observations in RustRight (#1216). They run through the selected executor
1232
1256
  // from the checked Cargo inputs, just like metadata; an executor refusal remains a failed scan.
1233
1257
  for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, ...cargoReadIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
@@ -1309,6 +1333,18 @@ export function planScans({
1309
1333
  checkSets.forEach((set, i) => add({ id: rustScanId.checkFeatures(name, i), class: B, ...cargoIn(ws, ...checkArgs(crate, { features: set, locked: true })) }));
1310
1334
  if (clippyBatching) lintSets.forEach((set) => clippyEntries.push({ name, crate, ws, set, count: lintSets.length, env: targetDirs?.[ws] ?? null }));
1311
1335
  else lintSets.forEach((set, i) => add({ id: rustScanId.clippy(name, lintSets.length, i), class: B, ...cargoIn(ws, ...clippyArgs(crate, { features: set, locked: true })) }));
1336
+ // The other two OS targets, every lint set, -D warnings, on every host (cross-targets.mjs; Adrian 2026-11-02/03, no skip). The zig C
1337
+ // toolchain must be provisioned (crossCcDir); without it the scan fails naming the fix, never skipped. On a broker host the broker must
1338
+ // admit the cross target and its CC_/CXX_/AR_ variables; a refusal there fails the scan.
1339
+ for (const target of crossTargetsFor(own, platform)) {
1340
+ const crossEnv = crossCompileEnv(target);
1341
+ lintSets.forEach((set, i) => {
1342
+ const id = `${rustScanId.clippy(name, lintSets.length, i)}@${target}`;
1343
+ if (!crossEnv) { add({ id, class: B, tool: "node", args: ["-e", `console.error("RK_CROSS_CC_UNPROVISIONED: the zig C toolchain for ${target} is not provisioned; run node scripts/ci/provision-zig.mjs <managed data root>/zig (or set RIGHTKIT_CROSS_CC_DIR)"); process.exit(1)`] }); return; }
1344
+ const scan = cargoIn(ws, ...clippyArgs(crate, { features: set, locked: true, target }));
1345
+ add({ id, class: B, ...scan, env: { ...(scan.env ?? {}), ...crossEnv } });
1346
+ });
1347
+ }
1312
1348
  // rustdoc flags go after `--` for this crate's library only: the broker owns RUSTDOCFLAGS and refuses
1313
1349
  // a request that sets it. A crate with no library has no API docs to check.
1314
1350
  if (exists(path.join(contentRoot, rustDir(name), "src", "lib.rs"))) {
@@ -1432,8 +1468,8 @@ export function planScans({
1432
1468
  const e2e = e2eCrateDirs instanceof Set && e2eCrateDirs.has(rustDir(name));
1433
1469
  if (e2e) { qualifyOwed.push({ id: rustScanId.testRust(name, sets.length, 0), kind: "e2e-crate", dir: rustDir(name), crate: crateOf(name), reason: "end-to-end crate: its tests drive a built app and never run as class B unit tests (P7)" }); notes.push(`${crateOf(name)} is an end-to-end crate: its test scan compiles with --no-run here; its tests run in the qualify stage against the built app (qa.journeys), never as class B unit tests`); }
1434
1470
  sets.forEach((set, i) => {
1435
- add({ id: rustScanId.testRust(name, sets.length, i), class: B, ...quiet, ...(e2e ? { e2e: true } : {}), ...cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", ...(e2e ? ["--no-run"] : ["--no-fail-fast"])) });
1436
- if (own.alsoSerial) add({ id: rustScanId.testRustSerial(name, sets.length, i), class: B, ...quiet, ...(e2e ? { e2e: true } : {}), ...cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", ...(e2e ? ["--no-run"] : ["--no-fail-fast", "--", "--test-threads=1"])) });
1471
+ add({ id: rustScanId.testRust(name, sets.length, i), class: B, ...quiet, ...(e2e ? { e2e: true } : {}), ...(e2e ? cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", "--no-run") : cargoLiveRoot ? { ...cargoTestIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked"), runOutside: { args: [] } } : cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", "--no-fail-fast")) });
1472
+ if (own.alsoSerial) add({ id: rustScanId.testRustSerial(name, sets.length, i), class: B, ...quiet, ...(e2e ? { e2e: true } : {}), ...(e2e ? cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", "--no-run") : cargoLiveRoot ? { ...cargoTestIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked"), runOutside: { args: ["--test-threads=1"] } } : cargoIn(wsOf(name), "test", "-p", crateOf(name), ...set, "--locked", "--no-fail-fast", "--", "--test-threads=1")) });
1437
1473
  });
1438
1474
  }
1439
1475
  for (const name of pkgNames) {
@@ -1763,6 +1799,200 @@ export function parseTestSummary(text) {
1763
1799
  return null;
1764
1800
  }
1765
1801
 
1802
+ // ---- Rust tests run outside the broker (GATE-CONTRACT GC-A08) -------------------------------------------------------------------------
1803
+ // On a broker host a Rust test scan compiles through the broker with the plain test command and --message-format=json (the broker adds
1804
+ // --no-run itself) and the gate then runs the listed test executables itself, never through the broker. A direct host (no broker) keeps the
1805
+ // one-step test command and uses none of this. Doctests
1806
+ // are not planned: the broker refuses `test --doc`, and a --no-run listing has no doctest executable. These are the pure
1807
+ // parts of that second phase; they mirror crates/rightkit-qa/src/run_tests (artifacts.rs, exec.rs, libtest.rs).
1808
+
1809
+ /** The package name and version of a cargo package id: `path+file:///w/fx#fx@0.1.0`, `path+file:///w/fx#0.1.0` (the name is the last
1810
+ * path segment) or the old `fx 0.1.0 (path+file:///w/fx)`. Null parts when the id says nothing usable. */
1811
+ export function packageOfId(id) {
1812
+ const text = String(id ?? "");
1813
+ const old = /^([^\s#@]+) (\d[^\s]*) \(/.exec(text);
1814
+ if (old) return { name: old[1], version: old[2] };
1815
+ const hash = text.indexOf("#");
1816
+ if (hash === -1) return { name: null, version: null };
1817
+ const fragment = text.slice(hash + 1);
1818
+ const at = fragment.indexOf("@");
1819
+ if (at > 0) return { name: fragment.slice(0, at), version: fragment.slice(at + 1) || null };
1820
+ if (/^\d/.test(fragment)) {
1821
+ const segments = text.slice(0, hash).replace(/\/+$/, "").split("/");
1822
+ return { name: segments.at(-1) || null, version: fragment };
1823
+ }
1824
+ return { name: fragment || null, version: null };
1825
+ }
1826
+
1827
+ /**
1828
+ * What a `cargo test --no-run --message-format=json` listing says. Only `compiler-artifact` records of the test profile that name an
1829
+ * executable are test executables (every unit-test binary and integration test; never a build script, library or plain binary). Lines
1830
+ * that are not JSON objects (a broker banner) are ignored. `buildFinished` is true when every `build-finished` record said success,
1831
+ * false when one said otherwise, null when the listing has none (a compile cut short).
1832
+ */
1833
+ export function parseTestListing(text) {
1834
+ const out = { binaries: [], buildFinished: null, artifacts: 0 };
1835
+ const seen = new Set();
1836
+ for (const raw of String(text ?? "").split(/\r?\n/)) {
1837
+ const line = raw.trimStart();
1838
+ if (!line.startsWith("{")) continue;
1839
+ let v; try { v = JSON.parse(line); } catch { continue; }
1840
+ if (v?.reason === "build-finished") out.buildFinished = (out.buildFinished ?? true) && v.success === true;
1841
+ else if (v?.reason === "compiler-artifact") {
1842
+ out.artifacts += 1;
1843
+ if (v.profile?.test !== true || typeof v.executable !== "string" || v.executable === "" || seen.has(v.executable)) continue;
1844
+ seen.add(v.executable);
1845
+ const pkg = packageOfId(v.package_id);
1846
+ const manifest = typeof v.manifest_path === "string" ? v.manifest_path : "";
1847
+ const target = typeof v.target?.name === "string" ? v.target.name : "";
1848
+ out.binaries.push({ package: pkg.name ?? target, version: pkg.version, target, kind: Array.isArray(v.target?.kind) ? v.target.kind.filter((k) => typeof k === "string") : [], executable: v.executable, manifestDir: manifest ? path.dirname(manifest) : "" });
1849
+ }
1850
+ }
1851
+ out.binaries.sort((a, b) => (a.executable < b.executable ? -1 : a.executable > b.executable ? 1 : 0));
1852
+ return out;
1853
+ }
1854
+
1855
+ /** The text a failed compile leaves for a reader: the rendered error diagnostics of the JSON listing plus every non-JSON line (a broker refusal). */
1856
+ export function listingFailureText(text, limit = 8000) {
1857
+ const out = [];
1858
+ for (const raw of String(text ?? "").split(/\r?\n/)) {
1859
+ const line = raw.trimEnd();
1860
+ if (!line.trim()) continue;
1861
+ if (!line.trimStart().startsWith("{")) { out.push(line); continue; }
1862
+ try { const v = JSON.parse(line); if (v?.reason === "compiler-message" && v.message?.level === "error" && typeof v.message.rendered === "string") out.push(v.message.rendered.trimEnd()); } catch { out.push(line.slice(0, 300)); }
1863
+ }
1864
+ return out.join("\n").slice(-limit);
1865
+ }
1866
+
1867
+ /** Sum of every `test result: ok. N passed; M failed; ...` line a libtest binary printed; null when it printed none (a custom harness, a crash). A line counts only when it starts the line and carries both a passed and a failed count. */
1868
+ export function parseLibtestSummary(text) {
1869
+ const sum = { results: 0, ok: true, passed: 0, failed: 0, ignored: 0, measured: 0, filteredOut: 0 };
1870
+ for (const line of String(text ?? "").split(/\r?\n/)) {
1871
+ const m = /^test result: (ok|FAILED)\. (.*)$/.exec(line.trimEnd());
1872
+ if (!m) continue;
1873
+ const counts = {}; for (const c of m[2].matchAll(/(\d+) (passed|failed|ignored|measured|filtered out)/g)) counts[c[2]] = Number(c[1]);
1874
+ if (counts.passed === undefined || counts.failed === undefined) continue;
1875
+ sum.results += 1; sum.ok = sum.ok && m[1] === "ok";
1876
+ sum.passed += counts.passed; sum.failed += counts.failed; sum.ignored += counts.ignored ?? 0; sum.measured += counts.measured ?? 0; sum.filteredOut += counts["filtered out"] ?? 0;
1877
+ }
1878
+ return sum.results > 0 ? sum : null;
1879
+ }
1880
+
1881
+ const TEST_RUN_OPS = Object.freeze({
1882
+ realpath: (p) => fs.realpathSync.native(p),
1883
+ isFile: (p) => fs.statSync(p).isFile(),
1884
+ isDir: (p) => fs.statSync(p).isDirectory(),
1885
+ digestFile: (p) => sha256(fs.readFileSync(p)),
1886
+ });
1887
+
1888
+ const insideDir = (real, dirReal, platform) => {
1889
+ const mod = platform === "win32" ? path.win32 : path;
1890
+ const fold = (p) => (platform === "win32" ? p.toLowerCase() : p);
1891
+ const rel = mod.relative(fold(dirReal), fold(real));
1892
+ return rel !== "" && !mod.isAbsolute(rel) && rel.split(/[\\/]/)[0] !== "..";
1893
+ };
1894
+
1895
+ /**
1896
+ * Every test executable must be a regular file whose real path is inside the verified output directory (`boundDirs`: the receipt's
1897
+ * outputDir on a broker host, the scan's bound target directory otherwise); with `generationRoots` its lexical directory must also be a
1898
+ * managed generation target. An executable anywhere else is refused: the gate never runs a binary the verified compile did not write.
1899
+ * Returns { accepted: [binary + real + sha256], problems: [text] }.
1900
+ */
1901
+ export function containTestExecutables(binaries, { boundDirs, generationRoots = null, platform = process.platform, ops = TEST_RUN_OPS } = {}) {
1902
+ const mod = platform === "win32" ? path.win32 : path;
1903
+ const problems = []; const accepted = [];
1904
+ const dirs = [];
1905
+ for (const d of boundDirs ?? []) { try { dirs.push(ops.realpath(d)); } catch { /* an output directory that does not exist contains nothing */ } }
1906
+ if (dirs.length === 0) return { accepted, problems: [`no verified output directory exists (${(boundDirs ?? []).join(", ") || "none bound"}); no test executable can be shown to come from the compile`] };
1907
+ for (const b of binaries) {
1908
+ const exe = b.executable;
1909
+ if (!mod.isAbsolute(exe)) { problems.push(`${exe} is not an absolute path`); continue; }
1910
+ if (generationRoots && !isInsideGenerationTarget(mod.dirname(exe), generationRoots, platform)) { problems.push(`${exe} is not inside a managed generation target`); continue; }
1911
+ let real;
1912
+ try { real = ops.realpath(exe); if (!ops.isFile(real)) throw new Error("not a regular file"); } catch (e) { problems.push(`${exe} is not a readable file (${e.message})`); continue; }
1913
+ if (!dirs.some((d) => insideDir(real, d, platform))) { problems.push(`${exe} resolves to ${real}, outside the verified output directory ${dirs.join(", ")}`); continue; }
1914
+ let sha = null; try { sha = ops.digestFile(real); } catch (e) { problems.push(`${exe} cannot be read (${e.message})`); continue; }
1915
+ accepted.push({ ...b, real, sha256: sha });
1916
+ }
1917
+ return { accepted, problems };
1918
+ }
1919
+
1920
+ /**
1921
+ * Run the accepted test executables one after another, outside the broker, each from its package directory with the CARGO_MANIFEST_DIR,
1922
+ * CARGO_PKG_NAME and CARGO_PKG_VERSION cargo would set, with `scan.runOutside.args`, under one deadline (`limitMs`) that kills the
1923
+ * process tree. Every executable runs (no-fail-fast); the row fails if any failed. The judgment is per crate (the scan), not per
1924
+ * executable: the libtest summaries are summed and the same rules as a node:test row apply (testResultsProblem), because many test
1925
+ * binaries legitimately run 0 tests. Returns { status, code?, detail?, results, executables }.
1926
+ */
1927
+ /**
1928
+ * Every accepted test executable must be in the broker's signed list (receipt.testExecutables: [{path, sha256, ...}]) with the same sha256.
1929
+ * A null list (the broker could not enumerate them, e.g. no JSON output) refuses everything, naming the broker's reason.
1930
+ */
1931
+ export function signedTestExecutableProblems(accepted, signed, reason = null, platform = process.platform) {
1932
+ if (!Array.isArray(signed)) return [`the broker signed no test executable list (${reason ?? "none given"})`];
1933
+ const norm = (p) => { const r = (platform === "win32" ? path.win32 : path.posix).normalize(String(p)); return platform === "win32" ? r.toLowerCase() : r; };
1934
+ const byPath = new Map(signed.map((e) => [norm(e?.path), e]));
1935
+ const problems = [];
1936
+ for (const b of accepted) {
1937
+ const e = byPath.get(norm(b.executable)) ?? byPath.get(norm(b.real));
1938
+ if (!e) problems.push(`${b.executable} is not in the broker's signed test executables`);
1939
+ else if (e.sha256 !== b.sha256) problems.push(`${b.executable} has sha256 ${b.sha256}, the broker signed ${e.sha256}`);
1940
+ }
1941
+ return problems;
1942
+ }
1943
+
1944
+ /** Test executables run at once (ADR-001 C2, rightkit#1454): the host's physical cores minus 2, at least 1. */
1945
+ export function testRunConcurrency({ cpus = os.cpus().length } = {}) {
1946
+ return Math.max(1, cpus - 2);
1947
+ }
1948
+
1949
+ export async function runTestBinaries({ scan, binaries, spawn, env, limitMs, signal = null, ops = TEST_RUN_OPS, concurrency = testRunConcurrency() }) {
1950
+ const args = [...(scan.runOutside?.args ?? [])];
1951
+ const ctl = new AbortController();
1952
+ const startedAt = Date.now();
1953
+ const clock = setTimeout(() => ctl.abort(), limitMs);
1954
+ const runSignal = signal ? AbortSignal.any([signal, ctl.signal]) : ctl.signal;
1955
+ const executables = []; const failures = []; const total = { tests: 0, pass: 0, fail: 0, skipped: 0, todo: 0 };
1956
+ let stopped = null;
1957
+ // ADR-001 C2 (kept by Adrian 2026-11-02, rightkit#1454): the executables run in parallel, `concurrency` at a time (physical cores minus 2), each
1958
+ // recording into its own slot so the report keeps the listing order.
1959
+ const slots = new Array(binaries.length);
1960
+ const runOne = async (b, index) => {
1961
+ const executables = { push: (e) => { slots[index] = e; } };
1962
+ {
1963
+ const label = `${b.package}::${b.target}`;
1964
+ if (runSignal.aborted) { stopped = signal?.aborted ? "CANCELLED" : "TIMEOUT"; executables.push({ label, sha256: b.sha256, status: "not-run" }); return; }
1965
+ try { if (!b.manifestDir || !ops.isDir(b.manifestDir)) { failures.push(`${label}: its package directory ${b.manifestDir || "(none)"} does not exist`); executables.push({ label, sha256: b.sha256, status: "fail" }); return; } } catch (e) { failures.push(`${label}: its package directory ${b.manifestDir} is unreadable (${e.message})`); executables.push({ label, sha256: b.sha256, status: "fail" }); return; }
1966
+ const childEnv = { ...env, CARGO_MANIFEST_DIR: b.manifestDir, CARGO_PKG_NAME: b.package, ...(b.version ? { CARGO_PKG_VERSION: b.version } : {}) };
1967
+ const remaining = Math.max(1, limitMs - (Date.now() - startedAt));
1968
+ let r;
1969
+ try { r = await spawn(b.real, args, { cwd: b.manifestDir, env: childEnv, signal: runSignal, boundedMs: remaining }); } catch (e) { r = { error: e }; }
1970
+ if (runSignal.aborted && (!r || r.error || r.status !== 0)) { stopped = signal?.aborted ? "CANCELLED" : "TIMEOUT"; executables.push({ label, sha256: b.sha256, status: "not-run" }); return; }
1971
+ if (r?.error?.code === "ETIMEDOUT" || r?.timedOut === true) { stopped = "TIMEOUT"; executables.push({ label, sha256: b.sha256, status: "fail" }); failures.push(`${label}: killed with its process tree at the run limit`); return; }
1972
+ const summary = parseLibtestSummary(`${r?.stdout ?? ""}\n${r?.stderr ?? ""}`);
1973
+ if (summary) { total.tests += summary.passed + summary.failed + summary.ignored; total.pass += summary.passed; total.fail += summary.failed; total.skipped += summary.ignored; }
1974
+ const entry = { label, sha256: b.sha256, status: "pass", ...(summary ? { passed: summary.passed, failed: summary.failed, ignored: summary.ignored } : {}) };
1975
+ if (!r || r.error) { entry.status = "fail"; failures.push(`${label}: ${r?.error?.message ?? "no result"}`); }
1976
+ else if (r.status !== 0) { entry.status = "fail"; failures.push(`${label}: ${failureDetail(r) || `exit ${r.status}`}`); }
1977
+ else if (summary && (!summary.ok || summary.failed > 0)) { entry.status = "fail"; failures.push(`${label}: exited 0 but its summary reports ${summary.failed} failed test(s)`); }
1978
+ executables.push(entry);
1979
+ }
1980
+ };
1981
+ try {
1982
+ let next = 0;
1983
+ const worker = async () => { while (next < binaries.length) { const i = next; next += 1; await runOne(binaries[i], i); } };
1984
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, binaries.length)) }, () => worker()));
1985
+ for (const e of slots) if (e) executables.push(e);
1986
+ } finally { clearTimeout(clock); }
1987
+ const results = { runner: "libtest", ...total, binaries: executables.length };
1988
+ if (stopped === "CANCELLED") return { status: "not-run", code: "CANCELLED", detail: "cancelled while running", results, executables };
1989
+ if (stopped === "TIMEOUT") return { status: "fail", code: "TIMEOUT", detail: `${scan.id}: its test executables ran longer than the ${Math.round(limitMs / 1000)} s run limit and were killed with their process tree${failures.length ? `; ${failures.slice(0, 3).join(" | ")}` : ""}`, results, executables };
1990
+ if (failures.length) return { status: "fail", code: "TEST_FAILED", detail: `${failures.length} of ${executables.length} test executable(s) failed: ${failures.slice(0, 5).join(" | ")}`.slice(0, 4000), results, executables };
1991
+ const problem = testResultsProblem(results);
1992
+ if (problem) return { status: "fail", code: problem.code, detail: `${problem.detail} (summed over ${executables.length} test executable(s) of ${scan.id})`, results, executables };
1993
+ return { status: "pass", results, executables };
1994
+ }
1995
+
1766
1996
  /** The largest inline `run:` script a workflow may carry while the pinned actionlint is 1.7.12 (rhysd/actionlint#702). */
1767
1997
  export const RUN_SCRIPT_LIMIT_BYTES = 3800;
1768
1998
  /** Every block `run:` script (`run: |` or `run: >`) of a workflow text larger than `limit` bytes, as "<file>:<line> <bytes> bytes".
@@ -2218,7 +2448,9 @@ export async function runScans(plan, options = {}) {
2218
2448
  const selected = plan.scans.filter((s) => classes.includes(s.class) && !exclude(s));
2219
2449
  const ordered = [...selected.filter((s) => s.class === SCAN_CLASS.A), ...selected.filter((s) => s.class === SCAN_CLASS.B)];
2220
2450
  const ctx = { deps, contentRoot, read: (f) => deps.readFile(path.join(contentRoot, f)) };
2221
- const cargoReadsAreCarried = Boolean(deps.snapshotBroker) || options.cargoReadsCarried === true;
2451
+ // GATE-CONTRACT GC-A11 (Adrian, 2026-11-02): Class A never runs in the broker. Class A cargo reads are never carried, in any stage; the gate
2452
+ // runs them directly (snapshot-metadata.mjs directReadSpawn), and cargo-deny / cargo-audit are its own enrolled tools.
2453
+ const cargoReadsAreCarried = false;
2222
2454
  // A broker host: the gate says so (options.brokerHost, from its executor) and a snapshot carrier implies it. One predicate (isBrokerCarriedScan) names the scans the broker carries.
2223
2455
  const brokerHost = options.brokerHost === true || Boolean(deps.snapshotBroker);
2224
2456
  const carriedCtx = { brokerHost, snapshotBroker: deps.snapshotBroker ?? null };
@@ -2320,7 +2552,14 @@ export async function runScans(plan, options = {}) {
2320
2552
  async function runOneBody(s, extraEnv = null) {
2321
2553
  if (signal?.aborted) return { id: s.id, class: s.class, command: s.command, tool: s.tool ?? null, toolVersion: null, status: "not-run", code: "CANCELLED", detail: "cancelled before it started" };
2322
2554
  const started = Date.now();
2323
- const row = await scanRow(s, extraEnv);
2555
+ let row = await scanRow(s, extraEnv);
2556
+ // rightkit#1459: a class A scan that timed out is run once more before it fails Class A (a hung tool, e.g. actionlint on 2026-11-02, ran
2557
+ // 27 min and failed a run whose rerun passed in 1 s). Class B and carried scans are not retried: a compile that times out is a real failure.
2558
+ if (row.status === "fail" && row.code === "TIMEOUT" && s.class === SCAN_CLASS.A && !signal?.aborted) {
2559
+ const first = row;
2560
+ row = await scanRow(s, extraEnv);
2561
+ row = { ...row, retried: { code: first.code, detail: first.detail } };
2562
+ }
2324
2563
  done += 1;
2325
2564
  try { onResult(row, { done, total, seconds: Math.round((Date.now() - started) / 1000) }); } catch { /* reporting never fails a scan */ }
2326
2565
  return row;
@@ -2459,6 +2698,7 @@ export async function runScans(plan, options = {}) {
2459
2698
  }
2460
2699
  async function scanRowIn(s, extraEnv, owned) {
2461
2700
  const row = { id: s.id, class: s.class, command: s.command, tool: s.tool ?? null, toolVersion: null, status: "fail" };
2701
+ const scanStartedAt = Date.now();
2462
2702
  if (s.class === SCAN_CLASS.B && classBBlocked) {
2463
2703
  return { ...row, status: "not-run", code: "CLASS_A_REQUIRED", detail: "class B refused: class A did not run in this call and no valid prior class A evidence for the same scan set was supplied" };
2464
2704
  }
@@ -2511,22 +2751,25 @@ export async function runScans(plan, options = {}) {
2511
2751
  else if (depVersionProblem) { status = "fail"; code = "TOOL_VERSION"; detail = `TOOL_VERSION: ${s.tool} runs ${depVersionProblem}`; }
2512
2752
  else {
2513
2753
  let cacheKey = null;
2514
- // A cargo request that builds the commit snapshot through the broker (options.snapshotBroker) is never served from or written
2515
- // to the scan cache: each run must carry its own request and verify the broker's own signed evidence.
2754
+ // GATE-CONTRACT GC-T14: every unchanged scan result is reused by its full input key, a broker-carried compile included (measured
2755
+ // 2026-11-02: HR class B re-ran 92 broker jobs, most with no dirty unit, because carried compiles were never cached). The key covers the
2756
+ // crate's closure, its lock entries, the toolchain files, the tool version and the build environment; a miss runs the compile and
2757
+ // verifies the broker's signed evidence as before, and only a verified pass is written.
2516
2758
  const carriedScan = isBrokerCarriedScan(s, carriedCtx);
2517
2759
  const viaSnapshotBroker = carriedScan && Boolean(deps.snapshotBroker) && s.root === "cargoLive" && (s.tool === "rustright" || s.tool === "cargo");
2518
- const carriedRead = cargoReadsAreCarried && s.class === SCAN_CLASS.A && (s.tool === "rustright" || s.tool === "cargo"); // a snapshot compile never reuses a cargo read either
2519
- if (cache && !viaSnapshotBroker && !carriedRead) {
2760
+ const carriedRead = cargoReadsAreCarried && s.class === SCAN_CLASS.A && (s.tool === "rustright" || s.tool === "cargo");
2761
+ if (cache && !carriedRead) {
2520
2762
  try {
2521
2763
  const digest = cache.digest(s);
2522
2764
  // The analyzers a scan runs through (actionlint's shellcheck) judge it too, so their versions key it as well.
2523
2765
  const toolVersion = [p.identity ? `${p.version}@${p.identity.sha256}` : p.version, ...depVersions].join(" + ");
2524
- if (digest) cacheKey = scanCacheKey({ id: s.id, command: s.command, toolVersion, env: s.env ?? {}, inheritedEnv: inheritedEnvDigest({ ...deps.env, ...s.env }), digest });
2766
+ if (digest) cacheKey = scanCacheKey({ id: s.id, command: s.runOutside ? `${s.command} ;; run ${canonicalJson(s.runOutside)}` : s.command, toolVersion, env: s.env ?? {}, inheritedEnv: inheritedEnvDigest({ ...deps.env, ...s.env }), digest });
2525
2767
  } catch { cacheKey = null; }
2526
2768
  const hit = cacheKey ? cache.read(cacheKey) : null;
2527
2769
  // A cached test pass counts only with the counts it recorded: without them its coverage is unknown, so it runs again.
2528
2770
  // ...and only if it passed at least one test: a cached all-skipped "pass" (written before D17) runs again (rightkit#1236).
2529
- const testEvidence = s.kind !== "test" || testResultsProblem(hit?.row?.results) === null;
2771
+ // A Rust test scan (runOutside) is judged the same way: the executables are unknown until the compile, so its key is the source digest and the cached row must carry the summed libtest counts.
2772
+ const testEvidence = (s.kind !== "test" && !s.runOutside) || testResultsProblem(hit?.row?.results) === null;
2530
2773
  if (hit && hit.key === cacheKey && hit.row?.status === "pass" && hit.row.id === s.id && hit.row.command === s.command && testEvidence) {
2531
2774
  cacheHits.push(s.id);
2532
2775
  return { ...row, status: "pass", cached: true, detail: `cached ${String(hit.at ?? "").slice(0, 10) || "earlier"}`, ...(s.kind === "test" ? { results: hit.row.results } : {}) };
@@ -2663,6 +2906,9 @@ export async function runScans(plan, options = {}) {
2663
2906
  fs.mkdirSync(sentinel, { recursive: true });
2664
2907
  }
2665
2908
  const sentinelEnv = sentinel ? { CARGO_TARGET_DIR: sentinel, CARGO_BUILD_TARGET_DIR: sentinel } : {};
2909
+ // A Rust test scan's JSON listing (every compiler-artifact record) streams whole to a file of its own: the kept tail of a spawn would lose the early artifact records of a noisy compile.
2910
+ let listingDir = null; let listingFile = null; let listingText = ""; let listingCut = false;
2911
+ if (s.runOutside) { listingDir = fs.mkdtempSync(path.join(tmpRoot, "rk-testlist-")); listingFile = path.join(listingDir, "listing.jsonl"); }
2666
2912
  try {
2667
2913
  // The ownership records this request's sub-handle immediately before it is sent (the reclaim proof reads broker.submitted): a
2668
2914
  // missing hook or a mark that did not land sends nothing.
@@ -2716,7 +2962,14 @@ export async function runScans(plan, options = {}) {
2716
2962
  admission = watchBrokerAdmission({ clock: runClock, refFiles, live: brokerLive, poller: statusPoller });
2717
2963
  admission.start();
2718
2964
  }
2719
- try { r = await deps.spawn(p.command, spawnArgs, { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...sentinelEnv, ...readTargetEnv, ...s.env, ...(extraEnv ?? {}), ...guardEnv, ...ledgerEnv, ...(carrier ? carrier.made.env : {}), ...refEnv, ...(carriedScan && !watched ? scanOperationEnv({ ...deps.env, ...s.env }, limitMs) : {}), ...trustEnv }, signal: scanSignal, ...(watched ? {} : { timeout: limitMs }) }); } finally { stopClock(); advisory?.cleanup?.(); }
2965
+ try { r = await deps.spawn(p.command, spawnArgs, { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...sentinelEnv, ...readTargetEnv, ...s.env, ...(extraEnv ?? {}), ...guardEnv, ...ledgerEnv, ...(carrier ? carrier.made.env : {}), ...refEnv, ...(carriedScan && !watched ? scanOperationEnv({ ...deps.env, ...s.env }, limitMs) : {}), ...trustEnv }, signal: scanSignal, ...(listingFile ? { stdoutFile: listingFile } : {}), ...(watched ? {} : { timeout: limitMs }) }); } finally { stopClock(); advisory?.cleanup?.(); }
2966
+ if (s.runOutside && r && !r.error) {
2967
+ // The whole listing: the file the spawn streamed, else (a spawn that does not know stdoutFile) its stdout. What a reader sees of a failed compile is the rendered errors.
2968
+ let fromFile = false;
2969
+ try { if (fs.existsSync(listingFile) && fs.statSync(listingFile).size > 0) { listingText = fs.readFileSync(listingFile, "utf8"); fromFile = true; } } catch { /* falls back to stdout */ }
2970
+ if (!fromFile) { listingText = String(r.stdout ?? ""); listingCut = r.truncated === true; }
2971
+ r = { ...r, stdout: listingFailureText(listingText) };
2972
+ }
2720
2973
  }
2721
2974
  if (signal?.aborted && (!r || r.error || r.status !== 0)) return { ...row, status: "not-run", code: "CANCELLED", detail: "cancelled while running" };
2722
2975
  if ((clockExpired || timer.signal.aborted || r?.error?.code === "ETIMEDOUT") && (!r || r.error || r.status !== 0)) {
@@ -2767,7 +3020,7 @@ export async function runScans(plan, options = {}) {
2767
3020
  const ref = readRequestRef(path.join(carrier.refDir, "request.json"));
2768
3021
  const receipt = await deps.snapshotBroker.receiptOf(ref.requestId);
2769
3022
  const { evidence } = verifySnapshotReceipt(receipt, { pins: deps.snapshotBroker.pins, jobId: ref.requestId, carrier: carrier.made.carrier, argv: carrier.cargoArgv, workspace: ref.workspace, observedEnv: observedEnvOf({ ...deps.env, ...s.env, ...(extraEnv ?? {}) }, deps.platform ?? process.platform), checkout: deps.snapshotBroker.checkout, outputRoots: deps.snapshotBroker.outputRoots, platform: deps.snapshotBroker.platform ?? deps.platform ?? process.platform });
2770
- row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", targetTriple: typeof receipt?.targetTriple === "string" && /^[A-Za-z0-9_.]+(?:-[A-Za-z0-9_.]+){1,4}$/.test(receipt.targetTriple) ? receipt.targetTriple : null, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
3023
+ row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", targetTriple: typeof receipt?.targetTriple === "string" && /^[A-Za-z0-9_.]+(?:-[A-Za-z0-9_.]+){1,4}$/.test(receipt.targetTriple) ? receipt.targetTriple : null, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, ...(receipt && Object.hasOwn(receipt, "testExecutables") ? { testExecutables: receipt.testExecutables, testExecutablesReason: receipt.testExecutablesReason ?? null } : {}) };
2771
3024
  } catch (e) {
2772
3025
  status = "fail"; code = "SNAPSHOT_EVIDENCE"; detail = `${s.id}: the broker's snapshot evidence did not verify (${e.code ?? "error"}: ${e.message})`;
2773
3026
  }
@@ -2775,6 +3028,43 @@ export async function runScans(plan, options = {}) {
2775
3028
  } finally {
2776
3029
  if (carrier) { removeCarrier(carrier.made.file); fs.rmSync(carrier.refDir, { recursive: true, force: true }); }
2777
3030
  if (tscProject) fs.rmSync(tscProject.dir, { recursive: true, force: true });
3031
+ if (listingDir) fs.rmSync(listingDir, { recursive: true, force: true });
3032
+ }
3033
+ // GATE-CONTRACT GC-A08: the compile above never ran a test. The gate runs the listed test executables here, outside the broker.
3034
+ // Quiet lane: a quiet suite keeps the broker's quiet lease (taken by runScans around this whole scan) through this run phase. The lease exists so nothing else
3035
+ // runs beside a conflict-prone suite, and the suite's tests now execute here, so releasing it after the compile would defeat it; the run phase is bounded
3036
+ // to what is left of QUIET_SCAN_MAX_MS so compile + run stay under the broker's lease ceiling.
3037
+ if (s.runOutside && status === "pass" && r && !r.error) {
3038
+ const fail = (c, d) => { status = "fail"; code = c; detail = d; };
3039
+ const listing = parseTestListing(listingText);
3040
+ const platform = deps.snapshotBroker?.platform ?? deps.platform ?? process.platform;
3041
+ if (listingCut) fail("TEST_LISTING", `${s.id}: the compile listing was cut at the kept output tail, so the test executables cannot be enumerated`);
3042
+ else if (listing.buildFinished !== true) fail("TEST_LISTING", `${s.id}: the compile exited 0 but its listing has no successful build-finished record (${listing.buildFinished === false ? "a build-finished record said failure" : "none found"})`);
3043
+ else if (listing.binaries.length === 0) {
3044
+ // A test scan that lists no test executable proves nothing; it is never a pass (rightkit-qa run_tests: "the compile listed no test executable").
3045
+ fail("NO_TEST_EXECUTABLES", `${s.id}: the compile listed no test executable (${listing.artifacts} artifact record(s), none of the test profile with an executable); a test scan that runs nothing is not a pass`);
3046
+ } else {
3047
+ let boundDirs; let generationRoots = null;
3048
+ if (viaSnapshotBroker) { boundDirs = row.snapshotEvidence?.outputDir ? [row.snapshotEvidence.outputDir] : []; generationRoots = deps.snapshotBroker.outputRoots ?? []; }
3049
+ else {
3050
+ const scanDir = path.join(rootDir(s.root), s.cwd ?? "");
3051
+ const target = s.env?.CARGO_TARGET_DIR ?? deps.env?.CARGO_TARGET_DIR ?? "target";
3052
+ boundDirs = [path.resolve(scanDir, target)];
3053
+ }
3054
+ const ops = { ...TEST_RUN_OPS, ...(options.testRunOps ?? {}) };
3055
+ const contained = containTestExecutables(listing.binaries, { boundDirs, generationRoots, platform, ops });
3056
+ // ADR-001 C2 (rightkit#1454): a broker that signs the compile's test executables (receipt.testExecutables, rustright) binds each one;
3057
+ // an executable whose path or sha256 is not in that signed list never runs. An older broker that signs no list leaves containment only.
3058
+ const signedProblems = viaSnapshotBroker && row.snapshotEvidence && Object.hasOwn(row.snapshotEvidence, "testExecutables") ? signedTestExecutableProblems(contained.accepted, row.snapshotEvidence.testExecutables, row.snapshotEvidence.testExecutablesReason, platform) : [];
3059
+ if (contained.problems.length) fail("TEST_EXECUTABLE_OUTSIDE_OUTPUT", `${s.id}: ${contained.problems.slice(0, 5).join("; ")}; the row fails and nothing was run`);
3060
+ else if (signedProblems.length) fail("TEST_EXECUTABLE_UNSIGNED", `${s.id}: ${signedProblems.slice(0, 5).join("; ")}; the row fails and nothing was run`);
3061
+ else {
3062
+ const budget = s.quiet === true ? Math.max(1, Math.min(limitMs, QUIET_SCAN_MAX_MS - (Date.now() - scanStartedAt))) : limitMs;
3063
+ const ran = await runTestBinaries({ scan: s, binaries: contained.accepted, spawn: deps.spawn, env: { ...deps.env, ...s.env, ...(extraEnv ?? {}) }, limitMs: budget, signal, ops });
3064
+ row.results = ran.results; row.testRun = { executables: ran.executables };
3065
+ if (ran.status !== "pass") { status = ran.status; code = ran.code; detail = ran.detail; }
3066
+ }
3067
+ }
2778
3068
  }
2779
3069
  if (status === "fail" && options.scanLogDir && r) {
2780
3070
  try { fs.mkdirSync(options.scanLogDir, { recursive: true }); const file = path.join(options.scanLogDir, `${s.id.replace(/[^A-Za-z0-9._-]+/g, "_")}.log`); fs.writeFileSync(file, `$ ${s.command ?? s.id}\nexit ${r.status}\n--- stdout\n${r.stdout ?? ""}\n--- stderr\n${r.stderr ?? ""}\n`); row.log = file; detail = `${detail} (full output: ${file})`; } catch { /* the tail in the row remains */ }
package/development.mjs CHANGED
@@ -29,10 +29,10 @@ import { packLinuxArtifacts, validateLinuxArchive } from "./linux-artifacts.mjs"
29
29
  import { buildAndEmbedNativeHelpers, nativeHelpersFromConfig } from "./native-helpers.mjs";
30
30
  import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
31
31
 
32
- // One standard Cargo profile for every app (templates/cargo-profiles.toml): Cargo's built-in `dev`.
33
- // `--profile dev` is valid and no flag also means dev.
34
- export const DEVELOPMENT_PROFILE = "dev";
35
- // Cargo writes the `dev` (and `test`) profile to target/<triple>/debug, never .../dev; `bench` goes to release.
32
+ // GATE-CONTRACT GC-A07: the dev installer is the release Cargo profile plus the `qa-native` feature
33
+ // (`tauri build --features qa-native`, no `--debug`). One unit set, one cache, one output directory (GC-A15): no dev profile anywhere.
34
+ export const DEVELOPMENT_PROFILE = "release";
35
+ // Cargo writes the `dev` (and `test`) profile to target/<triple>/debug, never .../dev; `release` and `bench` go to release.
36
36
  // Every path built from a profile name must go through this.
37
37
  export function cargoProfileDirectory(profile) {
38
38
  if (profile === "dev" || profile === "test") return "debug";
@@ -52,7 +52,7 @@ export function developmentEnvironment(env = process.env, platform = process.pla
52
52
  };
53
53
  for (const key of RELEASE_ONLY_APPLE_ENV) delete result[key];
54
54
  delete result.CARGO_INCREMENTAL;
55
- // A dev build is debug-signed: a release signing-properties path never reaches it.
55
+ // A dev build uses the dev signing identity: a release signing-properties path never reaches it.
56
56
  delete result.RIGHTKIT_ANDROID_SIGNING_PROPERTIES;
57
57
  // A direct (Linux) host never gets the managed agent-bin on PATH or a discovered socket, even when it DECLARES a broker route: managedRuntimeClient then returns the declared,
58
58
  // pinned client, but the agent-bin directory and socket here come from environment-derived managed roots that nothing validated. Whatever the build needs from the broker
@@ -154,7 +154,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
154
154
  let releaseLease = true;
155
155
  try {
156
156
  const startedMs = Date.now();
157
- // Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
157
+ // Android: cargo-ndk (the release profile) fills the generated jniLibs, then the Gradle debug build packages it.
158
158
  if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, door, label: "native build" }); }
159
159
  // RA-09: the iOS compile steps (xcodegen, xcodebuild build / build-for-testing / test-without-building) are broker adapter jobs of the production native runner, planned together before the first
160
160
  // launch; there is no execute() of this file's own for them. The app's own non-native `prepare` commands are the only direct steps and keep going through the door. The built app and the
@@ -195,7 +195,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
195
195
  if (helpers) {
196
196
  let version;
197
197
  try { version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version; } catch { version = undefined; }
198
- buildAndEmbedNativeHelpers({ appRoot: root, app: built, helpers, version, forceArch: String(target.install.targetTriple).startsWith("x86_64") ? "x86_64" : "arm64", configuration: "debug", replaceExisting: true, env: childEnv });
198
+ buildAndEmbedNativeHelpers({ appRoot: root, app: built, helpers, version, forceArch: String(target.install.targetTriple).startsWith("x86_64") ? "x86_64" : "arm64", configuration: "release", replaceExisting: true, env: childEnv });
199
199
  }
200
200
  releaseLease = false;
201
201
  return installTauriDevId({ appRoot: root, config: target.install, releaseConfig: config, built, env: childEnv })
@@ -368,7 +368,7 @@ export function receiptRelativeOutputPath({ declared, root, manifestDir = ".", m
368
368
  const manifestParts = String(manifestDir).split(/[\\/]/).filter((part) => part && part !== ".");
369
369
  const at = segments.findIndex((segment, index) => segment === "target" && segments.slice(0, index).every((part, i) => part === manifestParts[i]));
370
370
  if (at < 0) {
371
- const error = new Error(`${label} ${JSON.stringify(declared)} is a source-relative path, but this build is receipt-managed: its outputs are under ${managedOutputDir}, not the source tree or the snapshot. Declare it relative to the Cargo target directory (target/<triple>/debug/bundle/...), or omit it so the receipt's directory is used`);
371
+ const error = new Error(`${label} ${JSON.stringify(declared)} is a source-relative path, but this build is receipt-managed: its outputs are under ${managedOutputDir}, not the source tree or the snapshot. Declare it relative to the Cargo target directory (target/<triple>/release/bundle/...), or omit it so the receipt's directory is used`);
372
372
  error.code = "DEV_OUTPUT_SOURCE_RELATIVE";
373
373
  throw error;
374
374
  }
@@ -29,7 +29,7 @@ export const FINGERPRINT_LOG_FILTER = "cargo::core::compiler::fingerprint=info";
29
29
  const FINGERPRINT_TARGET = "cargo::core::compiler::fingerprint";
30
30
  export const FRESHNESS_SOURCE = "cargo-fingerprint-log+fingerprint-mtime";
31
31
  /** Reasons kept in the receipt; the rest are counted. */
32
- export const REASONS_CAP = 200;
32
+ export const REASONS_CAP = 10_000;
33
33
  /** Characters kept of one reason (Cargo prints it with `Debug`, paths and mtimes included). */
34
34
  export const REASON_TEXT_CAP = 512;
35
35
  /** Unit directories and files visited before the census is declared unavailable rather than partial. */
package/gate-stages.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The stages behind the one door (Adrian, 2026-10-25): rightkit-gate --to <stage> runs every stage up to <stage>, in order, for the
2
2
  // commit at HEAD, and stops at the first that fails. Class A always runs first. The stages reuse the existing owners instead of
3
- // reimplementing them: the gate's own scans for check/compile, `right-release` for package (source QA, then the build that seals the
3
+ // reimplementing them: the gate's own scans for check/compile, `right-release` for package (the build that seals the
4
4
  // exact artifact set), installed QA on that sealed set, and upload of that same sealed set. Those right-release commands refuse to
5
5
  // run unless a live door run launched them (requireDoorRun), so there is no side door into a stage.
6
6
  //
@@ -145,7 +145,7 @@ export function buildOuterApp({ root, spec, storage, profile, env = process.env,
145
145
  }
146
146
 
147
147
  export function resolveAssemblyStorage({ root, profile, env, run }) {
148
- return resolveXcodeStorage({ env, run, cwd: root, fallbackRoot: path.join(root, ".right-release", "assembled-app", profile) });
148
+ return resolveXcodeStorage({ env, run, cwd: root, fallbackRoot: path.join(root, ".right-release", "assembled-app") }); // one Xcode build folder, never one per profile (GC-A15)
149
149
  }
150
150
 
151
151
  // ---- macOS --------------------------------------------------------------------------------------------------------