@rightkit/release 0.6.8 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +30 -12
  2. package/admission-files.mjs +4 -1
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +105 -27
  7. package/assemble-app-command.mjs +151 -0
  8. package/assembled-app.mjs +445 -0
  9. package/bin/darwin/rightkit-swap +0 -0
  10. package/bin/darwin/rightkit-swap.sign.json +6 -0
  11. package/bin/rightkit-gate.mjs +1608 -345
  12. package/broker-pins.mjs +152 -0
  13. package/build-launch.mjs +88 -0
  14. package/build-ownership.mjs +1231 -0
  15. package/build-release.mjs +82 -28
  16. package/cargo-contract.mjs +3 -1
  17. package/cargo-ledger.mjs +62 -0
  18. package/cargo-target-shim.mjs +238 -0
  19. package/class-a-launch-guard.mjs +126 -0
  20. package/cli/right-release.mjs +31 -5
  21. package/clippy-equivalence.mjs +202 -0
  22. package/clippy-route.mjs +543 -0
  23. package/codesign-shim.mjs +11 -1
  24. package/committed-source.mjs +39 -5
  25. package/crate-workspaces.mjs +87 -0
  26. package/create-mac-updater.mjs +14 -2
  27. package/deterministic-scans.mjs +997 -71
  28. package/development-signing.mjs +4 -1
  29. package/development.mjs +120 -21
  30. package/direct-receipt.mjs +135 -0
  31. package/door-policy.mjs +71 -6
  32. package/door.mjs +6 -6
  33. package/freshness-census.mjs +257 -0
  34. package/gate-executor.mjs +14 -2
  35. package/gate-stages.mjs +91 -15
  36. package/heavy-command.mjs +3 -2
  37. package/hook-retirement.mjs +65 -0
  38. package/init-command.mjs +1 -1
  39. package/install-app.mjs +11 -2
  40. package/install-assembled-app.mjs +423 -0
  41. package/inventory-check.mjs +11 -8
  42. package/ios-dev.mjs +70 -3
  43. package/ios-upload.mjs +20 -15
  44. package/linux-artifacts.mjs +93 -0
  45. package/local-publish.mjs +224 -53
  46. package/local-qualify.mjs +5 -3
  47. package/local-runtime.mjs +5 -4
  48. package/mac-package-recipe.mjs +4 -1
  49. package/managed-cargo.mjs +47 -5
  50. package/managed-root-guard.mjs +34 -0
  51. package/mirror-root-artifact.mjs +18 -5
  52. package/native-helpers.mjs +355 -0
  53. package/ownership-class-rules.mjs +1 -1
  54. package/ownership-evidence.mjs +37 -13
  55. package/ownership-hooks.mjs +20 -59
  56. package/ownership-lockfiles.mjs +14 -1
  57. package/ownership-policy-bundle.mjs +3 -2
  58. package/ownership-staged.mjs +2 -1
  59. package/ownership.mjs +24 -0
  60. package/package-recipe-targets.mjs +84 -10
  61. package/package.json +4 -3
  62. package/pipeline-admission.mjs +4 -4
  63. package/pipeline-receipts.mjs +302 -302
  64. package/preflight.mjs +27 -4
  65. package/project-type.mjs +2 -2
  66. package/provenance-verify.mjs +115 -0
  67. package/publish-cargo.mjs +12 -9
  68. package/qa-admission-policy.json +10 -1
  69. package/recipe-build.mjs +106 -14
  70. package/release-artifact-path.mjs +341 -0
  71. package/release-config.mjs +1 -1
  72. package/release-invocation.mjs +8 -1
  73. package/release-qa-command.mjs +25 -9
  74. package/release-source-brand.mjs +17 -0
  75. package/release-source-identity.mjs +171 -0
  76. package/release.mjs +59 -22
  77. package/rightkit-versions.json +73 -47
  78. package/runtime-budgets.mjs +6 -2
  79. package/scan-digest.mjs +71 -5
  80. package/sign-updater.mjs +6 -3
  81. package/snapshot-broker.mjs +172 -0
  82. package/snapshot-check.mjs +312 -0
  83. package/snapshot-git.mjs +166 -0
  84. package/snapshot-request.mjs +363 -0
  85. package/snapshot-root-config.mjs +76 -0
  86. package/snapshot.mjs +969 -0
  87. package/source-split.mjs +73 -0
  88. package/stage-commands.mjs +60 -0
  89. package/stage-ledger.mjs +204 -0
  90. package/stage-sidecar-command.mjs +5 -1
  91. package/stage-sidecar.mjs +157 -27
  92. package/swap-helper-bundle.mjs +5 -2
  93. package/test-command.mjs +17 -4
  94. package/unit-gate.mjs +301 -0
  95. package/web-build.mjs +40 -6
  96. package/win-package-recipe.mjs +20 -7
@@ -23,11 +23,16 @@ import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { spawn as spawnChild, spawnSync } from "./background-process.mjs";
26
+ import { spawnUnderOwnership } from "./build-ownership.mjs";
26
27
  import { checkAdvisoryExceptions } from "./advisory-exceptions.mjs";
27
- import { canonicalJson, digestScanSet, requiredScanIds, rustCratePlan, rustScanId } from "./scan-digest.mjs";
28
+ import { CLIPPY_UNIVERSAL_FEATURE_FLAGS, canonicalJson, clippyBatchPlan, clippySelectionOf, digestScanSet, requiredScanIds, rustCratePlan, rustScanId } from "./scan-digest.mjs";
28
29
  import { reach } from "./lock-graph.mjs";
29
30
  import { cargoManifestState } from "./cargo-manifest-text.mjs";
31
+ import { LAUNCH_GUARD_IMPORT } from "./class-a-launch-guard.mjs";
30
32
  import { advisoryCacheRoot, ensureAdvisoryDb, prepareDenyOverlay, readAdvisoryPin } from "./advisory-db.mjs";
33
+ import { observedEnvOf, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest } from "./snapshot-request.mjs";
34
+ import { SCAN_TOKEN_ENV, readLedger, scanTokenOf, unclaimedLedgers } from "./cargo-ledger.mjs";
35
+ import { verifyBrokerLedger } from "./snapshot-broker.mjs";
31
36
 
32
37
  export const SCAN_RESULT_SCHEMA = "rightkit.deterministic-scans/1";
33
38
  /**
@@ -45,16 +50,29 @@ export const CHECKER_IDENTITY = (() => {
45
50
  })();
46
51
  export const CHECKER_VERSION = CHECKER_IDENTITY;
47
52
  export const SCAN_CLASS = Object.freeze({ A: "A", B: "B" });
53
+ /**
54
+ * Recorded in the result of every wrapped snapshot compile (item 7 row 7.10, lead re-decision F7): cargo hands a build script its own
55
+ * path in CARGO (measured, cargo 1.98.0), so a cargo a build script runs through $CARGO never reaches the wrapper's ledger.
56
+ */
57
+ 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";
58
+
59
+ /** The ledger token of the gate's own tool version probes (they reach the cargo wrapper like any scan does). */
60
+ const PROBE_LEDGER_TOKEN = scanTokenOf("tool-probe");
48
61
 
49
62
  /** Scan id prefixes (the text before the first ":") that belong to each class. */
50
63
  export const CLASS_A_SCAN_PREFIXES = Object.freeze([
51
64
  "rustfmt", "node-check", "json-parse", "toml-parse", "yaml-parse", "ownership-catalog",
52
65
  "actionlint", "shellcheck", "pnpm-lockfile", "pnpm-audit", "cargo-metadata", "cargo-deny", "cargo-audit", "cargo-package-list",
53
66
  "changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions", "suite-api",
67
+ // Everything mechanical and deterministic that does not compile is class A (Adrian, 2026-10-27): typecheck, lint, generated docs.
68
+ "tsc", "eslint", "biome", "generated-docs",
54
69
  ]);
70
+ // Class B includes compiles and tests that execute project code.
55
71
  export const CLASS_B_SCAN_PREFIXES = Object.freeze([
56
- "clippy", "check", "check-all-features", "check-features", "check-dependent", "doc", "tsc", "eslint", "biome", "swift-build", "test", "generated-docs",
72
+ "clippy", "check", "check-all-features", "check-features", "check-dependent", "doc", "swift-build",
57
73
  ]);
74
+ // Test scripts can invoke a compiler indirectly, so all tests run after Class A.
75
+ export const TEST_SCAN_CLASS = Object.freeze({ "test:rust": "B", "test:rust-serial": "B", "test:npm": "B", "test:scripts-ci": "B" });
58
76
 
59
77
  /**
60
78
  * Rollout switch for the CI integration (ci/gate.config.json: { "deterministicScans": { "enabled": true } }).
@@ -92,8 +110,14 @@ export function stripJsonComments(text) {
92
110
  }
93
111
 
94
112
  /** "A", "B" or null for an id such as "clippy:rightkit-fs". */
95
- export function scanClassOf(id) {
96
- const prefix = String(id).split(":")[0];
113
+ export function scanClassOf(id, scan = null) {
114
+ const parts = String(id).split(":");
115
+ const prefix = parts[0];
116
+ if (prefix === "test") {
117
+ const kind = TEST_SCAN_CLASS[`test:${parts[1]}`] ?? null;
118
+ if (kind === "A" && scan?.brokerCall) return SCAN_CLASS.B;
119
+ return kind === null ? null : SCAN_CLASS[kind];
120
+ }
97
121
  if (CLASS_A_SCAN_PREFIXES.includes(prefix)) return SCAN_CLASS.A;
98
122
  if (CLASS_B_SCAN_PREFIXES.includes(prefix)) return SCAN_CLASS.B;
99
123
  return null;
@@ -183,6 +207,19 @@ export function checkArgs(unit, { features = [], messageFormat, locked = false }
183
207
  }
184
208
 
185
209
  /** `cargo doc -p <unit> --no-deps --all-features --locked`; run it with RUSTDOCFLAGS=DOC_RUSTDOCFLAGS (CI). */
210
+ // Every compile reports every failure (Adrian, 2026-10-29: "even to ship, why isn't it better to get all errors and fix them all
211
+ // wholesale?"; rightkit#1293): a compile keeps going past a failed crate (`--keep-going`) and a test run past a failed test binary
212
+ // (`--no-fail-fast`). The run still fails if anything failed; the crates that compiled stay warm for the next run.
213
+ // Cargo 1.98: build, check, clippy (through its cargo check) and doc take `--keep-going`; test and bench reject it and take
214
+ // `--no-fail-fast` instead (the broker's BR29 adds the same split).
215
+ const KEEP_GOING_SUBCOMMANDS = new Set(["build", "check", "clippy", "doc"]);
216
+ const NO_FAIL_FAST_SUBCOMMANDS = new Set(["test", "bench"]);
217
+ export function everyFailure(args) {
218
+ const [sub, ...rest] = args;
219
+ const until = rest.indexOf("--"); const own = until === -1 ? rest : rest.slice(0, until);
220
+ const flag = KEEP_GOING_SUBCOMMANDS.has(sub) ? "--keep-going" : NO_FAIL_FAST_SUBCOMMANDS.has(sub) ? "--no-fail-fast" : null;
221
+ return flag && !own.includes(flag) ? [sub, flag, ...rest] : args;
222
+ }
186
223
  export function docArgs(unit) { return ["doc", "-p", unit, "--no-deps", "--all-features", "--locked"]; }
187
224
  export const DOC_RUSTDOCFLAGS = "-D warnings";
188
225
  /** The same check as rustdoc arguments (`cargo rustdoc ... -- -D warnings`), for the broker path. */
@@ -194,6 +231,35 @@ export function fmtCheckArgs(unit = null) { return unit ? ["fmt", "-p", unit, "-
194
231
  /** `cargo metadata` that validates manifests and the lock without resolving anything new. */
195
232
  export function metadataArgs({ noDeps = true } = {}) { return ["metadata", ...(noDeps ? ["--no-deps"] : []), "--format-version", "1", "--locked"]; }
196
233
 
234
+ // ---- Clippy batching (GATE-CONTRACT §1 "Clippy route & batching"; item 8 rows 8.6, 8.8, 8.15) -----------------------------------------
235
+
236
+ // The batch planner itself (clippyBatchPlan, clippySelectionOf, CLIPPY_UNIVERSAL_FEATURE_FLAGS) lives in scan-digest.mjs, beside the scan
237
+ // ids, and is re-exported here.
238
+ export { CLIPPY_UNIVERSAL_FEATURE_FLAGS, clippyBatchPlan, clippySelectionOf };
239
+
240
+ /**
241
+ * Does this crate's directory declare sidecars its build needs staged first? A Tauri crate with `bundle.externalBin` refuses to build
242
+ * until its sidecars are in place (bin/rightkit-gate.mjs, the compile stage), so it is scheduled after the independent crates. The
243
+ * declaration is read from the crate's own Tauri configuration (JSON, JSON5 or TOML), in its directory or its `src-tauri`.
244
+ */
245
+ export function sidecarBoundCrate({ dir, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8") }) {
246
+ for (const base of [dir, path.join(dir, "src-tauri")]) {
247
+ for (const name of ["tauri.conf.json", "tauri.conf.json5", "Tauri.toml"]) {
248
+ const file = path.join(base, name);
249
+ let body;
250
+ try { if (!exists(file)) continue; body = readFile(file); } catch { continue; }
251
+ try {
252
+ const conf = JSON.parse(body);
253
+ const list = conf?.bundle?.externalBin ?? conf?.tauri?.bundle?.externalBin;
254
+ if (Array.isArray(list) && list.length) return { bound: true, file, externalBin: list };
255
+ continue;
256
+ } catch { /* not strict JSON: read as text */ }
257
+ if (/(^|[\s{,"'])externalBin["']?\s*[:=]\s*\[\s*["']/.test(body)) return { bound: true, file, externalBin: null };
258
+ }
259
+ }
260
+ return { bound: false };
261
+ }
262
+
197
263
  // ---- plan ---------------------------------------------------------------------------------------------
198
264
 
199
265
  const posix = (p) => p.replaceAll("\\", "/");
@@ -224,6 +290,49 @@ const PNPM_NO_HOOKS_ENV = Object.freeze({ npm_config_ignore_pnpmfile: "true", np
224
290
  // fails in the tool or the test with its own error. The `pnpm_config_` spelling is the one pnpm 11.25 honours for this setting
225
291
  // (`npm_config_verify_deps_before_run=false` still aborted, checked on a stale tree), the lowercase spelling PNPM_NO_HOOKS_ENV uses for its settings.
226
292
  const PNPM_RUN_ENV = Object.freeze({ pnpm_config_verify_deps_before_run: "false" });
293
+
294
+ // The placeholder a tsc row passes as its project when its package depends on a workspace sibling (planScans; runScans replaces it).
295
+ export const TSC_PROJECT = "{tscProject}";
296
+ const DIST_TYPES = /^\.\/dist\/(.+)\.d\.ts$/;
297
+ /**
298
+ * Writes the project a tsc row checks through: it extends the package's own tsconfig (its include, exclude and options, resolved from
299
+ * there), emits nothing, widens rootDir to the checked tree, keeps the package's own @types lookup chain (typeRoots), and maps every
300
+ * exported entry point of each workspace sibling whose types
301
+ * are built output (`types: ./dist/<x>.d.ts`) to that sibling's committed source (`src/<x>.ts`, `.tsx`, `/index.ts`, `/index.tsx`).
302
+ * `workspace` is the siblings' tree-relative directories. Returns { dir, file }; the caller removes `dir`. Throws when a sibling's
303
+ * manifest cannot be read or an exported entry point has no committed source: such a sibling could only be checked against a build.
304
+ */
305
+ export function writeTscProject({ contentRoot, packageDir, workspace, tmpdir = os.tmpdir() }) {
306
+ const paths = {};
307
+ for (const rel of workspace) {
308
+ const dir = path.join(contentRoot, rel);
309
+ let manifest;
310
+ try { manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8").replace(/^/, "")); }
311
+ catch (e) { throw new Error(`${rel}/package.json: ${e.message}`); }
312
+ if (typeof manifest.name !== "string" || path.resolve(dir) === path.resolve(packageDir)) continue;
313
+ const exported = manifest.exports && typeof manifest.exports === "object" ? Object.entries(manifest.exports) : [];
314
+ for (const [sub, target] of exported) {
315
+ const types = target && typeof target === "object" ? target.types : null;
316
+ const m = typeof types === "string" ? DIST_TYPES.exec(types) : null;
317
+ if (!m) continue;
318
+ const source = [`src/${m[1]}.ts`, `src/${m[1]}.tsx`, `src/${m[1]}/index.ts`, `src/${m[1]}/index.tsx`].map((f) => path.join(dir, f)).find((f) => fs.existsSync(f));
319
+ if (!source) throw new Error(`${manifest.name} exports ${sub} with types ${types}, and ${rel}/src has no committed source for it`);
320
+ paths[sub === "." ? manifest.name : `${manifest.name}${sub.slice(1)}`] = [source];
321
+ }
322
+ }
323
+ // TypeScript looks type libraries up (`types`, and every @types package by default) in node_modules/@types of the CONFIG's directory
324
+ // and its ancestors. The project lives outside the tree, so it names the chain the package's own tsconfig would have used.
325
+ const typeRoots = [];
326
+ for (let d = path.resolve(packageDir); ; d = path.dirname(d)) {
327
+ const at = path.join(d, "node_modules", "@types");
328
+ if (fs.existsSync(at)) typeRoots.push(at);
329
+ if (path.dirname(d) === d) break;
330
+ }
331
+ const out = fs.mkdtempSync(path.join(tmpdir, "rk-tsc-"));
332
+ const file = path.join(out, "tsconfig.json");
333
+ fs.writeFileSync(file, `${JSON.stringify({ extends: path.join(packageDir, "tsconfig.json"), compilerOptions: { noEmit: true, rootDir: path.resolve(contentRoot), typeRoots, paths } }, null, 2)}\n`);
334
+ return { dir: out, file };
335
+ }
227
336
  const CONFIG_CRATE_FILE = /(^|\/)Cargo\.(toml|lock)$/;
228
337
  const JS_TOOLING_FILE = /(^|\/)(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml)$/;
229
338
  // Root files that change how EVERY crate (or package) builds: a change to one puts every unit in scope,
@@ -248,6 +357,11 @@ export const ancestorConfigChanged = (ws, changed, pattern = ANCESTOR_RUST_CONFI
248
357
  const rel = dir === "." ? f : f.startsWith(`${dir}/`) ? f.slice(dir.length + 1) : null;
249
358
  return rel !== null && pattern.test(rel);
250
359
  }));
360
+ // Genuinely shared inputs of a repository whose every crate is its own Cargo workspace (rightkit#1285): no crate directory holds them,
361
+ // so a change to one reaches every unit. Anything else outside a crate directory reaches no crate (a vendored source maps to the crates
362
+ // that patch or path-depend on it through `externalInputs`; a repository-level toolchain, .cargo, clippy or rustfmt file is an
363
+ // ANCESTOR_RUST_CONFIG_FILE of every workspace). The gate configuration, the pipeline's toolchain pins and the gate itself are named here.
364
+ export const SHARED_RUST_SCOPE_FILE = /^(ci\/gate\.config\.json|ci\/rk-pipeline\.json|packages\/release\/(bin\/rightkit-gate|deterministic-scans|scan-digest|advisory-exceptions|crate-workspaces|lock-graph|cargo-target-shim)\.mjs)$/;
251
365
  const WORKSPACE_JS_FILE = /^(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|\.npmrc)$/;
252
366
  // The linter a package has is the one it configures. ESLint reads a flat `eslint.config.*` or a legacy `.eslintrc*` from the package directory or
253
367
  // any ancestor of it; Biome reads `biome.json` / `biome.jsonc` the same way. A Biome-only app (ViewRight, ScrapeRight: `"lint": "biome lint ."`)
@@ -278,6 +392,26 @@ const PROBES = Object.freeze({
278
392
  * provision exactly this version; scripts/ci/scan-receipt.test.mjs holds them to it.
279
393
  */
280
394
  export const PINNED_SCAN_TOOLS = Object.freeze({ shellcheck: "0.11.0" });
395
+ /** Scans whose answer depends on a remote service at run time, not only on their inputs: `pnpm-audit` asks the registry's advisory
396
+ * service. A verdict that includes them is reusable only after they are re-run (`rightkit-gate live-scans`; GATE-CONTRACT §1, E18),
397
+ * and a red caused by one is never deterministic. Ids are `<prefix>` or `<prefix>:<root>`; rustright's REMOTE_SCANS mirrors this list. */
398
+ export const LIVE_SCAN_IDS = Object.freeze(["pnpm-audit"]);
399
+ export const isLiveScan = (id) => LIVE_SCAN_IDS.some((p) => id === p || String(id).startsWith(`${p}:`));
400
+ /** Scans whose FAILURE may come from the registry rather than their inputs: `pnpm-lockfile` (`install --frozen-lockfile --lockfile-only`)
401
+ * re-verifies lockfile entries against the registry when pnpm's cache is cold, so an outage fails it. Their pass is deterministic, but a
402
+ * red they cause is never a reusable deterministic verdict (rightkit#1257; Oracle review of 79c4427b). */
403
+ export const REGISTRY_SCAN_IDS = Object.freeze(["pnpm-lockfile"]);
404
+ export const isRegistryScan = (id) => REGISTRY_SCAN_IDS.some((p) => id === p || String(id).startsWith(`${p}:`));
405
+ /** Files per rustfmt scan: 120 paths of ~100 bytes stay under Windows' 32 KB command line with the tool and flags. */
406
+ export const RUSTFMT_BATCH = 120;
407
+
408
+ /** True when this module is the checked repository's own in-tree copy (packages/release of `contentRoot`), where its scripts/ live. */
409
+ export function inTreeGate(contentRoot, exists = fs.existsSync) {
410
+ const here = path.dirname(fileURLToPath(import.meta.url));
411
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
412
+ const own = real(path.resolve(here, "..", ".."));
413
+ return own === real(contentRoot) && exists(path.join(own, "scripts", "gen-suite-api.mjs"));
414
+ }
281
415
  // The version a probe's output names, for a tool whose first line is not its version (`shellcheck --version` opens with a banner).
282
416
  const PROBE_VERSION = Object.freeze({
283
417
  shellcheck: (text) => { const m = /^version:\s*(\S+)\s*$/m.exec(text); return m ? `shellcheck ${m[1]}` : null; },
@@ -410,6 +544,195 @@ export function scriptBrokerCall(scripts, entry = "test", { maxDepth = 8 } = {})
410
544
  return visit(String(entry), []);
411
545
  }
412
546
 
547
+ /**
548
+ * Whether the package script `entry` builds (emits) code before or while it tests: `tsc` without --noEmit, or a package-manager
549
+ * run of a `build` / `build:*` script in this package or another (`pnpm run build`, `pnpm --filter <pkg> build`), followed through
550
+ * the scripts it starts in the same package.json like scriptBrokerCall. Such a test is not a pre-compile check: it runs after
551
+ * class A, with the compile scans (Codex reviewer 2026-10-28). Returns `{ via, command }` or null.
552
+ */
553
+ export function scriptBuildCall(scripts, entry = "test", { maxDepth = 8 } = {}) {
554
+ if (scripts === null || typeof scripts !== "object" || Array.isArray(scripts)) return null;
555
+ const textOf = (name) => (Object.hasOwn(scripts, name) && typeof scripts[name] === "string" ? scripts[name].slice(0, SCRIPT_TEXT_LIMIT) : null);
556
+ const builds = (words) => {
557
+ let i = 0;
558
+ while (i < words.length && (ENV_ASSIGNMENT.test(words[i]) || COMMAND_WRAPPERS.has(path.posix.basename(words[i])) || PACKAGE_RUNNERS.has(path.posix.basename(words[i])))) i += 1;
559
+ const word = path.posix.basename(String(words[i] ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "");
560
+ if (word === "tsc") return !words.slice(i + 1).some((w) => /^--?noEmit(=true)?$/i.test(w));
561
+ if (!PACKAGE_MANAGERS.has(word)) return false;
562
+ // pnpm's `-w` is `--workspace-root` (a flag); npm's `-w` is `--workspace <name>` (takes a value).
563
+ const rest = words.slice(i + 1); const values = new Set(word === "pnpm" ? ["--filter", "-F", "--dir", "-C"] : word === "npm" ? ["--workspace", "-w", "--prefix"] : ["--cwd", "--filter"]);
564
+ for (let j = 0; j < rest.length; j += 1) {
565
+ if (values.has(rest[j])) { j += 1; continue; }
566
+ if (rest[j].startsWith("-") || rest[j] === "run" || rest[j] === "run-script") continue;
567
+ return /^build(:|$)/.test(rest[j]);
568
+ }
569
+ return false;
570
+ };
571
+ const seen = new Set();
572
+ const visit = (name, via) => {
573
+ if (via.length >= maxDepth || seen.has(name)) return null;
574
+ seen.add(name);
575
+ for (const part of [`pre${name}`, name, `post${name}`]) {
576
+ const text = textOf(part);
577
+ if (text === null) continue;
578
+ for (const words of shellCommands(text)) {
579
+ if (builds(words)) return { via: [...via, part], command: words.join(" ").slice(0, 200) };
580
+ const found = inspectCommand(words, scripts);
581
+ if (found.run !== undefined) { const deeper = visit(found.run, [...via, part]); if (deeper) return deeper; }
582
+ }
583
+ }
584
+ return null;
585
+ };
586
+ return visit(String(entry), []);
587
+ }
588
+
589
+ // Cargo redirections on a reachable command line or script: an explicit manifest or output directory, or a working-directory change.
590
+ const CARGO_REDIRECT = /--manifest-path|--target-dir|(^|[\s;&|(])(CARGO_TARGET_DIR|CARGO_BUILD_TARGET_DIR)=|(^|[\s;&|(])(cd|pushd)\s/;
591
+
592
+ // The forms a test's path to cargo may take for its target directory to be bound (cargoReachOf). Everything else is unprovable.
593
+ // Tools that run no repository code and start no cargo: any arguments.
594
+ const REACH_SAFE_TOOLS = new Set(["tsc", "eslint", "prettier", "biome", "oxlint", "echo", "true"]);
595
+ // cargo options that move its manifest, workspace or output, or change its configuration (a short-option cluster holding C or Z too).
596
+ const CARGO_REDIRECT_ARG = /^(--manifest-path|--target-dir|--config)(=|$)|^-[A-Za-z]*[CZ]/;
597
+ // The cargo subcommands a bound test may run; anything else (an alias, cargo make, xtask, install, a plugin) is unprovable.
598
+ const CARGO_REACH_SUBCOMMANDS = new Set(["test", "build", "check", "clippy"]);
599
+ // Package-manager built-ins: never one of the package's scripts, whatever its package.json names (npm pack runs pack, not a script).
600
+ const PM_BUILTINS = new Set(["install", "i", "add", "ci", "pack", "publish", "version", "link", "unlink", "rebuild", "prune", "update", "up", "upgrade", "uninstall", "remove", "rm", "init", "create", "exec", "dlx", "x", "workspace", "workspaces", "config", "set", "import", "patch", "deploy", "audit", "outdated", "why", "store", "cache", "env", "setup", "self-update", "fetch"]);
601
+ // Characters a shell gives meaning outside quotes beyond the separators shellCommands splits on: an escape, a glob, a home or
602
+ // redirection, a brace or history expansion, a comment, and cmd.exe's ^ and %VAR%. A script holding one unquoted is unprovable.
603
+ const UNQUOTED_SHELL_META = new Set(["\\", "*", "?", "[", "]", "~", "<", ">", "{", "}", "!", "#", "^", "%"]);
604
+ /** The first character a shell would read specially outside quotes in `text`, or null. */
605
+ function unquotedShellMeta(text) {
606
+ let quote = null;
607
+ for (const c of String(text)) {
608
+ if (quote) { if (c === quote) quote = null; continue; }
609
+ if (c === '"' || c === "'") { quote = c; continue; }
610
+ if (UNQUOTED_SHELL_META.has(c)) return c;
611
+ }
612
+ return null;
613
+ }
614
+ // Package-manager options the walk resolves; any other option is unprovable.
615
+ const PM_DIR_OPTIONS = new Set(["--dir", "-C", "--prefix", "--cwd"]);
616
+ const PM_PACKAGE_OPTIONS = new Set(["--filter", "-F", "--filter-prod", "--workspace"]);
617
+ const PM_PLAIN_FLAGS = new Set(["-s", "--silent", "--if-present", "--color", "--no-color", "--stream"]);
618
+ const PM_VALUE_FLAGS = new Set(["--reporter", "--loglevel"]);
619
+
620
+ /**
621
+ * Where a package's `test` script makes cargo run, for a commit-snapshot compile (joint plan step 4c): `{ none: true }` when it reaches
622
+ * no cargo, `{ dir }` (repository-relative) when every cargo it reaches runs from that one directory, `{ dirs }` when from several (the
623
+ * caller requires them to be one workspace), or `{ unprovable }` with the reason. One CARGO_TARGET_DIR binds the whole test.
624
+ *
625
+ * Fail-closed allowlist (root reviews of 431fbeae, f883d5ca, 15300d82): a test is bound only when every command on every path it
626
+ * reaches (each script with its pre/post hooks) parses completely into one of these forms:
627
+ * - `cargo <args>` or `rustright cargo <args>` with no option that moves the manifest, workspace or output (--manifest-path,
628
+ * --target-dir, --config, -C, -Z): cargo runs from the script's package directory;
629
+ * - `pnpm|npm|yarn` with only resolvable options in any position before the script (--dir/-C/--prefix/--cwd, --filter/-F/
630
+ * --filter-prod/--workspace naming one package of this repository, pnpm -w, npm -w <workspace>, and output flags) running
631
+ * `run <script>`, `test`, `<script>` with no further arguments, or `exec <command>` (walked the same way in that package);
632
+ * - a tool that runs no repository code (tsc, eslint, prettier, biome, oxlint, echo, true).
633
+ * Anything else is unprovable: `cd`, an environment assignment, a shell expansion, node or any test runner, an unknown command or
634
+ * option, a recursive run, an unresolvable package, arguments passed on to a script, a script past the size limit, the depth limit,
635
+ * and a requiresBroker declaration (the declared path is not in the scripts). Over-refusal is the accepted cost.
636
+ */
637
+ export function cargoReachOf({ unitDir, scripts, declared = null, readScripts = () => null, packageDirOf = () => null, maxDepth = 8 }) {
638
+ const dirs = new Set();
639
+ // Keyed on package and script alone: the walk refuses everything that could change how a script runs (cd, environment), so a
640
+ // script reached twice from one package runs the same way both times.
641
+ const seen = new Set();
642
+ const unprovable = (why) => { throw Object.assign(new Error(why), { reach: true }); };
643
+ const walkScript = (dirScripts, name, dir, via) => {
644
+ const key = `${dir}\0${name}`;
645
+ if (seen.has(key)) return;
646
+ if (via.length >= maxDepth) unprovable(`the path to ${dir}'s ${name} (${via.join(" -> ")}) is deeper than ${maxDepth} scripts`);
647
+ seen.add(key);
648
+ for (const part of [`pre${name}`, name, `post${name}`]) {
649
+ if (!Object.hasOwn(dirScripts, part)) continue;
650
+ const text = dirScripts[part];
651
+ if (typeof text !== "string") unprovable(`${dir}'s ${part} script is not text`);
652
+ if (text.length > SCRIPT_TEXT_LIMIT) unprovable(`${dir}'s ${part} script is longer than ${SCRIPT_TEXT_LIMIT} characters`);
653
+ const meta = unquotedShellMeta(text);
654
+ if (meta !== null) unprovable(`${dir}'s ${part} script has an unquoted ${JSON.stringify(meta)}, which a shell would expand or redirect`);
655
+ const step = [...via, dir === unitDir ? part : `${dir}:${part}`];
656
+ for (const words of shellCommands(text)) walkCommand(words, dirScripts, dir, step);
657
+ }
658
+ };
659
+ const walkCommand = (words, dirScripts, dir, via) => {
660
+ const where = `${via.join(" -> ")}`;
661
+ const shown = words.join(" ").slice(0, 120);
662
+ if (words.some((w) => /[`$]/.test(w))) unprovable(`${where} runs \`${shown}\`, whose shell expansion this check cannot follow`);
663
+ if (ENV_ASSIGNMENT.test(words[0])) unprovable(`${where} runs \`${shown}\` with its own environment`);
664
+ const tool = path.posix.basename(words[0].replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "");
665
+ if (tool === "cargo" || tool === "rustright") {
666
+ const args = tool === "rustright" ? (words[1] === "cargo" ? words.slice(2) : unprovable(`${where} runs \`${shown}\`, a rustright command other than cargo`)) : words.slice(1);
667
+ const end = args.indexOf("--");
668
+ const own = end === -1 ? args : args.slice(0, end);
669
+ if (own.some((w) => CARGO_REDIRECT_ARG.test(w))) unprovable(`the path to cargo (${where}) redirects it`);
670
+ const sub = own.find((w) => !w.startsWith("-") && !w.startsWith("+"));
671
+ if (!CARGO_REACH_SUBCOMMANDS.has(sub)) unprovable(`${where} runs \`${shown}\`, a cargo command (${sub ?? "none"}) other than ${[...CARGO_REACH_SUBCOMMANDS].join(", ")}`);
672
+ dirs.add(dir);
673
+ return;
674
+ }
675
+ if (PACKAGE_MANAGERS.has(tool)) return walkPackageManager(tool, words.slice(1), dirScripts, dir, via, shown);
676
+ if (REACH_SAFE_TOOLS.has(tool)) return;
677
+ unprovable(`${where} runs \`${shown}\` (${tool}), which this check cannot follow to cargo`);
678
+ };
679
+ const walkPackageManager = (tool, args, dirScripts, dir, via, shown) => {
680
+ const where = `${via.join(" -> ")}`;
681
+ let target = dir;
682
+ let i = 0;
683
+ let retargets = 0;
684
+ // Options, in any position before the script or command: each one resolved, or the command is unprovable. At most one of them
685
+ // may name the package or directory (two would leave which one wins to the package manager).
686
+ const options = () => {
687
+ while (i < args.length && args[i].startsWith("-")) {
688
+ const [name, ...inline] = args[i].split("=");
689
+ const value = inline.length ? inline.join("=") : args[i + 1];
690
+ const width = inline.length ? 1 : 2;
691
+ if (PM_DIR_OPTIONS.has(name) || PM_PACKAGE_OPTIONS.has(name) || ((name === "-w" || name === "--workspace-root") && (tool === "pnpm" || tool === "npm"))) {
692
+ retargets += 1;
693
+ if (retargets > 1) unprovable(`${where}: \`${shown}\` names its package or directory more than once`);
694
+ }
695
+ if (PM_DIR_OPTIONS.has(name)) { if (value === undefined) unprovable(`${where}: \`${shown}\` names no directory`); target = path.posix.normalize(path.posix.join(dir, String(value))); i += width; }
696
+ else if (PM_PACKAGE_OPTIONS.has(name)) { const d = packageDirOf(String(value)); if (d === null || d === undefined) unprovable(`${where}: \`${shown}\` runs a package (${value}) this repository cannot resolve`); target = d; i += width; }
697
+ else if ((name === "-w" || name === "--workspace-root") && tool === "pnpm") { target = "."; i += 1; }
698
+ else if ((name === "-w") && tool === "npm") { const d = packageDirOf(String(value)); if (d === null || d === undefined) unprovable(`${where}: \`${shown}\` runs a workspace (${value}) this repository cannot resolve`); target = d; i += width; }
699
+ else if (PM_PLAIN_FLAGS.has(name) && !inline.length) i += 1;
700
+ else if (PM_VALUE_FLAGS.has(name)) i += width;
701
+ else unprovable(`${where}: \`${shown}\` has an option (${name}) this check does not resolve`);
702
+ }
703
+ };
704
+ options();
705
+ const sub = args[i++];
706
+ if (sub === undefined) unprovable(`${where}: \`${shown}\` runs no script`);
707
+ // Options after `run`/`exec` count as well: the target is known only once every option before the script has been read.
708
+ if (sub === "run" || sub === "run-script" || (sub === "exec" && tool === "pnpm")) options();
709
+ if (target.startsWith("..")) unprovable(`${where}: \`${shown}\` runs outside this repository (${target})`);
710
+ const targetScripts = target === dir ? dirScripts : readScripts(target);
711
+ if (targetScripts === null || targetScripts === undefined) unprovable(`${where}: \`${shown}\` runs scripts of ${target}, which has no readable package.json`);
712
+ const hop = target === dir ? via : [...via, `${tool} ${args.join(" ")}`.slice(0, 120)];
713
+ if (sub === "exec" && tool === "pnpm") {
714
+ if (i >= args.length) unprovable(`${where}: \`${shown}\` executes nothing`);
715
+ return walkCommand(args.slice(i), targetScripts, target, hop);
716
+ }
717
+ let script;
718
+ if (sub === "run" || sub === "run-script") { script = args[i++]; if (script === undefined) unprovable(`${where}: \`${shown}\` names no script`); }
719
+ else if (sub === "test" || sub === "t" || sub === "tst") script = "test";
720
+ else if (!PM_BUILTINS.has(sub) && Object.hasOwn(targetScripts, sub)) script = sub;
721
+ else unprovable(`${where}: \`${shown}\` is a ${tool} command (${sub}) this check cannot follow`);
722
+ if (i < args.length) unprovable(`${where}: \`${shown}\` passes arguments to ${target}'s ${script}`);
723
+ walkScript(targetScripts, script, target, hop);
724
+ };
725
+ if (declared?.call) return { unprovable: "it is declared to reach the broker in ways its scripts do not show (requiresBroker)" };
726
+ try {
727
+ walkScript(scripts ?? {}, "test", unitDir, []);
728
+ } catch (e) {
729
+ if (e.reach) return { unprovable: e.message };
730
+ throw e;
731
+ }
732
+ const list = [...dirs];
733
+ return list.length === 0 ? { none: true } : list.length === 1 ? { dir: list[0] } : { dirs: list };
734
+ }
735
+
413
736
  // A test script can reach the broker in ways scriptBrokerCall cannot read from package.json alone: `node -e "...spawnSync('pnpm', ['run', n])..."`,
414
737
  // `node ./scripts/verify-local.mjs` (a script that spawns `rustright cargo test`), `sh -c`, `pnpm --dir <elsewhere> run verify`, `pnpm --filter`. HeardRight's
415
738
  // root `test` is a `node -e` over `verify`, which is `pnpm --dir tauri-app-next run verify`, which is `node ./scripts/verify-local.mjs`, which runs
@@ -434,12 +757,25 @@ export function declaredBrokerCall(unit, value) {
434
757
  * managed `rustright cargo` and `pnpm` run in. A scan whose files did not change is not planned.
435
758
  */
436
759
  export function planScans({
437
- changed = [], contentRoot, liveRoot = contentRoot, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8"),
760
+ changed = [], contentRoot, liveRoot = contentRoot, trustedRoot = null, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8"),
438
761
  platform = process.platform, includeTests = true, localCompile = true,
439
762
  // Where the quiet test scans (ci/gate.config.json `quietTests`) run: "local" plans them for the quiet lane; "ci" leaves them to the
440
763
  // CI shards (one crate per runner, where the port, pipe, desktop and wall-clock conflicts that force the serial lane do not exist)
441
764
  // and records a note naming them (Adrian 2026-10-22: compile locally on the warm cache, qualify the conflicting tests on VMs).
442
765
  quietTests = "local",
766
+ // Which scans this plan owes (Adrian, 2026-10-26; app-check.mjs TIERS): "compile" leaves out the scans added after @rightkit/release
767
+ // 0.6.6 (shellcheck, and actionlint's shellcheck requirement) until every app builds through the door again; "release" plans everything.
768
+ tier = "compile",
769
+ // A broker host (rustright): cargo scans run in this primary checkout instead of the content root, because the RustRight client refuses
770
+ // every cargo command but a locked metadata outside the app's primary checkout (RK_NOT_PRIMARY_CHECKOUT). The gate sets it only after
771
+ // proving the checkout's Cargo inputs are blob-identical to the snapshot's (cargoInputsDiffer); null keeps the content/live roots.
772
+ cargoLiveRoot = null,
773
+ // { [cargoDir]: absolute directory }: the checkout's resolved target directory per Cargo workspace, set as CARGO_TARGET_DIR on
774
+ // that workspace's cargo scans when the scans read a snapshot (the door's commit compile, joint plan step 4c).
775
+ targetDirs = null,
776
+ // True when the gate's runtime cargo wrapper (cargo-target-shim.mjs) is first on PATH for every scan: each cargo invocation binds its
777
+ // own workspace's checkout target directory or is refused at run time, so the static walk (cargoReachOf) is advisory, never a refusal.
778
+ cargoShim = false,
443
779
  // The command prefix every cargo scan runs through: `rustright cargo` on a managed host, plain `cargo` in CI.
444
780
  cargoCommand = ["rustright", "cargo"],
445
781
  // Physical containment: a changed path whose real path leaves the real content root (a symlink) is refused.
@@ -452,13 +788,15 @@ export function planScans({
452
788
  crateFeatures = {},
453
789
  // ci/gate.config.json `npm`: per-package gate settings (`quietTests`), read by the package's unit name (packages/<dir> → rightkit-<dir>).
454
790
  packageFeatures = {},
791
+ // True for full class A on the live checkout, which runs the repository's own code anyway (tests, typecheck, lint).
792
+ liveExecution = false,
455
793
  // Crates a workspace-wide Rust change (Cargo.lock, vendor/) actually reaches, computed by the caller from
456
794
  // the lock graph (scripts/ci/select.mjs). null keeps every crate in scope.
457
795
  rustScope = null,
458
796
  // Repository-specific units for any layout: [{ name, dir, publish? }], dir repo-relative ("." for a root unit). When given they
459
797
  // replace the crates/<name> and packages/<name> layout (rightkit-gate). `cargoDir` is the Cargo workspace root the cargo scans run in.
460
798
  // A rust unit may name its own workspace: { name, dir, cargoDir, crate, publish? } runs its scans with cwd cargoDir and `-p <crate>`;
461
- // `name` is then the unit's identity in scan ids and feature config (rightkit-gate names it `<cargoDir>:<crate>` below the root), and
799
+ // `name` is then the unit's identity in scan ids and feature config (rightkit-gate names it `<cargoDir>:<crate>` below the root, and the plain crate for a `crates/<crate>` workspace of its own), and
462
800
  // `crateFeatures` is read by `name` first, then by `crate`. Units of different workspaces never share a lock, a deny/audit run or a
463
801
  // `cargo metadata`; a unit that sets no cargoDir belongs to the global `cargoDir` above.
464
802
  rustUnits = null, npmUnits = null, cargoDir = ".",
@@ -476,6 +814,19 @@ export function planScans({
476
814
  rustLocks = null,
477
815
  // The pinned RustSec advisory database ({ url, commit }, advisory-db.json): cargo-deny and cargo-audit read exactly that commit and never fetch.
478
816
  advisoryPin = null,
817
+ // Cross-workspace reach of a crate-per-workspace repository (rightkit#1285): `crateDependents(unitNames: Set) -> Set` answers the
818
+ // units that path-depend on any of `unitNames`, transitively, across Cargo workspaces (the gate builds it from packages/release/
819
+ // crate-workspaces.mjs; manifests only). Every such unit that is not itself in scope gets a `check-dependent` scan. null plans none beyond
820
+ // the per-workspace lock graph (rustLocks).
821
+ crateDependents = null,
822
+ // `{ "<repo-relative directory>": [unit names] }`: sources outside every unit that a unit compiles (vendor/citationberg is patched into
823
+ // rightkit-typeset). A changed file under the directory puts those units in scope. null maps none.
824
+ externalInputs = null,
825
+ // Clippy batching (item 8; clippyBatchPlan). false keeps the production path: one Clippy scan per crate and lint feature set. It is
826
+ // switched on only after the batch-equivalence run (clippy-equivalence.mjs, row 8.7) has passed on the hosts the map names; then the
827
+ // planner emits one scan per compatible batch and no per-crate Clippy scan for a crate that belongs to one (row 8.15).
828
+ // `shardSolo` is ci/shards.json `solo`; `clippySeparate` maps a unit name to the reason equivalence found batching changed it.
829
+ clippyBatching = false, shardSolo = [], clippySeparate = new Map(),
479
830
  } = {}) {
480
831
  if (!contentRoot) throw new TypeError("planScans: contentRoot is required");
481
832
  if (!Array.isArray(cargoCommand) || cargoCommand.length === 0 || !["rustright", "cargo"].includes(cargoCommand[0]) || cargoCommand.slice(1).some((a) => a !== "cargo")) {
@@ -503,9 +854,14 @@ export function planScans({
503
854
  const bytesDigest = (f) => { try { return sha256(readBytes(path.join(contentRoot, f))); } catch { return "unreadable"; } };
504
855
  const inputDigest = sha256(canonicalJson({ files: [...files].sort().map((f) => [f, bytesDigest(f)]), refused: refusals.map((r) => r.raw).sort() }));
505
856
  const under = (prefix) => files.filter((f) => f.startsWith(prefix));
857
+ if (!["compile", "release"].includes(tier)) throw new Error(`planScans: tier must be compile or release, not ${JSON.stringify(tier)}`);
506
858
  let scans = []; const notes = [];
859
+ if (cargoLiveRoot) notes.push(`broker host: cargo scans run in the primary checkout ${cargoLiveRoot}, whose Cargo inputs match the checked snapshot (rustright refuses cargo outside the primary checkout)`);
860
+ // A snapshot compile's scans carry its target map (item 7): the map is part of every scan's cache key, so no result cached by a
861
+ // live-checkout run (or a compile with another map) is taken for one that writes to these directories.
862
+ const snapshotTargets = targetDirs ? { RK_SNAPSHOT_TARGETS: canonicalJson(targetDirs) } : {};
507
863
  const add = (d) => {
508
- const entry = { root: "live", env: {}, args: [], ...d };
864
+ const entry = { root: "live", args: [], ...d, env: { ...snapshotTargets, ...(d.env ?? {}) } };
509
865
  entry.command = commandText(entry);
510
866
  scans.push(entry);
511
867
  };
@@ -524,7 +880,7 @@ export function planScans({
524
880
  const plainDir = (d) => !ABSOLUTE_PATH.test(d) && !d.split("/").includes("..") && SAFE_UNIT_DIR.test(d);
525
881
  const cargoWs = cleanDir(cargoDir);
526
882
  if (!plainDir(cargoWs)) throw new TypeError("planScans: cargoDir must be a plain repository-relative directory");
527
- const cargoIn = (ws, ...args) => ({ tool: cargoCommand[0], args: [...cargoCommand.slice(1), ...args], ...(ws !== "." ? { cwd: ws } : {}) });
883
+ const cargoIn = (ws, ...args) => ({ tool: cargoCommand[0], args: [...cargoCommand.slice(1), ...everyFailure(args)], ...(ws !== "." ? { cwd: ws } : {}), ...(cargoLiveRoot ? { root: "cargoLive" } : {}), ...(targetDirs?.[ws] ? { env: { CARGO_TARGET_DIR: targetDirs[ws] } } : {}) });
528
884
  const unitMap = (units, kind) => {
529
885
  if (units === null) return null;
530
886
  if (!Array.isArray(units)) throw new TypeError(`planScans: ${kind}Units must be an array of { name, dir }`);
@@ -579,7 +935,13 @@ export function planScans({
579
935
  };
580
936
  // Units of an explicit-workspace layout (some unit names its cargoDir) get workspace-suffixed ids for the per-workspace scans.
581
937
  const perWorkspaceIds = Array.isArray(rustUnits) && rustUnits.some((u) => u?.cargoDir !== undefined);
582
- const wsIdSuffix = (ws) => (perWorkspaceIds && ws !== "." ? `:${ws}` : "");
938
+ // The label a per-workspace scan id carries: the workspace directory, except for a workspace that is exactly one crate-named unit
939
+ // (`crates/rightkit-fs` holding rightkit-fs, rightkit#1285 D2), which is labelled by the crate (`cargo-deny:rightkit-fs`).
940
+ const wsLabel = (ws) => {
941
+ if (rustMap) for (const [name, u] of rustMap) if (u.cargoDir === ws && name === u.crate && ws === `crates/${u.crate}`) return name;
942
+ return ws;
943
+ };
944
+ const wsIdSuffix = (ws) => (perWorkspaceIds && ws !== "." ? `:${wsLabel(ws)}` : "");
583
945
  const wsOf = (n) => (rustMap ? rustMap.get(n).cargoDir : cargoWs);
584
946
  const crateOf = (n) => (rustMap?.get(n)?.crate ?? n);
585
947
  const featuresOf = (n) => crateFeatures?.[n] ?? crateFeatures?.[crateOf(n)] ?? {};
@@ -613,13 +975,32 @@ export function planScans({
613
975
  const verbatimUpstream = (f) => f.startsWith("vendor/") && typeof upstream[f] === "string" && bytesDigest(f) === upstream[f];
614
976
  const recordedPatch = (f) => f.startsWith("vendor/") && DIGEST.test(patched[f]?.base ?? "") && typeof patched[f]?.note === "string"
615
977
  && patched[f].note.startsWith("vendor/") && DIGEST.test(patched[f]?.sha256 ?? "") && bytesDigest(f) === patched[f].sha256;
978
+ // Declared fixture files (Adrian, 2026-10-30: "there should be a .rkignore list that somehow allows for those to be passed";
979
+ // rightkit#1291): `.rightkit/fixture-files.json` { schema: 1, files: { "<path>": { sha256, note } } } lists files that are
980
+ // deliberately malformed (an audit's known-answer inputs). A listed file whose bytes still match its sha256 skips the per-file
981
+ // format and parse scans only (rustfmt, node --check, json-parse, toml-parse); every other scan still reads it. Any edit changes
982
+ // the digest, so the file is checked again until re-recorded. A malformed manifest, a bad digest or an empty note refuses.
983
+ let fixtureFiles = {};
984
+ if (exists(path.join(contentRoot, ".rightkit/fixture-files.json"))) {
985
+ try {
986
+ const manifest = JSON.parse(readFile(path.join(contentRoot, ".rightkit/fixture-files.json")));
987
+ if (manifest?.schema !== 1 || !manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) throw new Error("schema 1 with a files object is required");
988
+ for (const [f, row] of Object.entries(manifest.files)) {
989
+ if (!/^[0-9a-f]{64}$/.test(row?.sha256 ?? "")) throw new Error(`${f}: sha256 must be 64 lowercase hex characters`);
990
+ if (typeof row?.note !== "string" || !row.note.trim()) throw new Error(`${f}: a note saying why the file is malformed is required`);
991
+ if (f.startsWith("/") || f.split("/").includes("..")) throw new Error(`${f}: paths are repository-relative`);
992
+ }
993
+ fixtureFiles = manifest.files;
994
+ } catch (error) { refuse(".rightkit/fixture-files.json", error.message); }
995
+ }
996
+ const declaredFixture = (f) => Object.hasOwn(fixtureFiles, f) && bytesDigest(f) === fixtureFiles[f].sha256;
616
997
  // Generated Rust is not hand-formatted: bindgen output, or rustfmt's own `@generated` marker in the first five lines.
617
998
  const generatedRust = (f) => {
618
999
  let head = "";
619
1000
  try { head = readFile(path.join(contentRoot, f)).split("\n", 5).join("\n"); } catch { return false; }
620
1001
  return /@generated\b|automatically generated by rust-bindgen\b/.test(head);
621
1002
  };
622
- const rustFiles = files.filter((f) => f.endsWith(".rs") && !verbatimUpstream(f) && !recordedPatch(f) && !generatedRust(f));
1003
+ const rustFiles = files.filter((f) => f.endsWith(".rs") && !verbatimUpstream(f) && !recordedPatch(f) && !generatedRust(f) && !declaredFixture(f));
623
1004
  const byEdition = new Map();
624
1005
  const editionOf = rustEditionResolver({ contentRoot, exists, readFile });
625
1006
  const refusedEditions = new Set();
@@ -632,15 +1013,21 @@ export function planScans({
632
1013
  }
633
1014
  byEdition.set(found.edition, [...(byEdition.get(found.edition) ?? []), f]);
634
1015
  }
1016
+ // One rustfmt scan per edition, batched: Windows refuses a command line past ~32 KB (spawn ENAMETOOLONG on a large repository,
1017
+ // HeardRight on the Dell, 2026-10-27), so a batch holds at most RUSTFMT_BATCH files and the first batch keeps the plain id.
635
1018
  for (const [edition, group] of [...byEdition].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
636
- add({ id: `rustfmt:${edition}`, class: A, tool: "rustfmt", args: ["--check", "--edition", edition, ...group.sort()], root: "content" });
1019
+ const sorted = group.sort();
1020
+ for (let i = 0; i < sorted.length; i += RUSTFMT_BATCH) {
1021
+ const batch = sorted.slice(i, i + RUSTFMT_BATCH);
1022
+ add({ id: `rustfmt:${edition}${i ? `:${i / RUSTFMT_BATCH + 1}` : ""}`, class: A, tool: "rustfmt", args: ["--check", "--edition", edition, ...batch], root: "content" });
1023
+ }
637
1024
  }
638
- for (const f of files.filter((f) => /\.(mjs|cjs|js)$/.test(f) && !f.includes("node_modules")).sort()) {
1025
+ for (const f of files.filter((f) => /\.(mjs|cjs|js)$/.test(f) && !f.includes("node_modules") && !declaredFixture(f)).sort()) {
639
1026
  add({ id: `node-check:${f}`, class: A, tool: "node", args: ["--check", f], root: "content" });
640
1027
  }
641
- const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.includes("node_modules")).sort();
1028
+ const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.includes("node_modules") && !declaredFixture(f)).sort();
642
1029
  if (jsonFiles.length) add({ id: "json-parse", class: A, internal: "json-parse", files: jsonFiles });
643
- const tomlFiles = files.filter((f) => f.endsWith(".toml") && !f.includes("node_modules")).sort();
1030
+ const tomlFiles = files.filter((f) => f.endsWith(".toml") && !f.includes("node_modules") && !declaredFixture(f)).sort();
644
1031
  if (tomlFiles.length) add({ id: "toml-parse", class: A, internal: "toml-parse", files: tomlFiles });
645
1032
  // The strict workflow YAML parser covers the subset GitHub workflows and actions use; other YAML is not parsed with it.
646
1033
  const yamlFiles = files.filter((f) => /^\.github\/.+\.ya?ml$/.test(f)).sort();
@@ -658,7 +1045,12 @@ export function planScans({
658
1045
  const wideJsRoots = new Set(pnpmRoots.filter((r) => files.some((f) => { const rel = wsRel(r, f); return rel !== null && WORKSPACE_JS_FILE.test(rel); })));
659
1046
  const wideCrates = wideWs.size ? (rustMap ? [...rustMap.keys()].filter((n) => wideWs.has(wsOf(n))) : listDir(path.join(contentRoot, "crates"))).filter((n) => !rustScope || rustScope.has(n)) : [];
660
1047
  if (wideWs.size && rustScope) notes.push(`workspace Rust change reaches ${wideCrates.length} crate(s) through the lock graph`);
661
- const crateNames = safeUnits([...new Set([...(rustMap ? files.map((f) => unitOf(rustMap, f)).filter(Boolean) : under("crates/").map((f) => f.split("/")[1])), ...wideCrates])].filter((n) => exists(path.join(contentRoot, rustDir(n), "Cargo.toml"))).sort(), "crate");
1048
+ // A source outside every unit that a unit compiles (externalInputs), and a shared input, reach their units; nothing else outside a
1049
+ // unit's directory does (a crate-per-workspace repository has no root manifest or lock to widen the scope).
1050
+ const externalUnits = rustMap && externalInputs ? Object.entries(externalInputs).flatMap(([dir, names]) => (files.some((f) => f === dir || f.startsWith(`${dir.replace(/\/+$/, "")}/`)) ? names : [])).filter((n) => rustMap.has(n)) : [];
1051
+ const sharedUnits = rustMap && files.some((f) => SHARED_RUST_SCOPE_FILE.test(f)) ? [...rustMap.keys()] : [];
1052
+ if (sharedUnits.length) notes.push(`a shared build input changed (${files.filter((f) => SHARED_RUST_SCOPE_FILE.test(f)).join(", ")}); every Rust unit is in scope`);
1053
+ const crateNames = safeUnits([...new Set([...(rustMap ? files.map((f) => unitOf(rustMap, f)).filter(Boolean) : under("crates/").map((f) => f.split("/")[1])), ...wideCrates, ...externalUnits, ...sharedUnits])].filter((n) => exists(path.join(contentRoot, rustDir(n), "Cargo.toml"))).sort(), "crate");
662
1054
  // The workspace a changed file belongs to is the deepest one that holds it (a root workspace holds only what no nested one does).
663
1055
  const wsOfFile = (f) => { let best = null; for (const ws of workspaces) if ((ws === "." || f.startsWith(`${ws}/`)) && (best === null || ws.length > best.length)) best = ws; return best; };
664
1056
  const configWs = new Set();
@@ -687,13 +1079,14 @@ export function planScans({
687
1079
  const actionlintConfig = exists(path.join(contentRoot, ".github/actionlint.yaml")) ? ["-config-file", ".github/actionlint.yaml"] : [];
688
1080
  // actionlint lints every run: script with shellcheck only when shellcheck is found, and otherwise passes silently; class A
689
1081
  // requires it (`requires`), so a host without it fails the scan instead of skipping the shell checks.
690
- if (workflowFiles.length) add({ id: "actionlint", class: A, tool: "actionlint", requires: ["shellcheck"], args: [...actionlintConfig, ...workflowFiles], root: "content" });
1082
+ if (workflowFiles.length) add({ id: "actionlint", class: A, tool: "actionlint", requires: tier === "release" ? ["shellcheck"] : [], args: [...actionlintConfig, ...workflowFiles], root: "content", runScriptLimit: { files: workflowFiles, bytes: RUN_SCRIPT_LIMIT_BYTES } });
691
1083
  // Standalone shell scripts (SellRight's deploy scripts failed SC2154, SC2030/2031 and SC2016 in CI): every tracked .sh/.bash file,
692
1084
  // with ShellCheck's own defaults and the repository's .shellcheckrc (data, not code). actionlint covers only workflow run: blocks.
693
1085
  // A script whose shebang names another shell (zsh, fish) is not ShellCheck's to judge, so it is left out, never failed.
694
1086
  const otherShell = (f) => { try { return /^#!.*\b(zsh|fish|pwsh|powershell)\b/.test(String(readFile(path.join(contentRoot, f))).split("\n", 1)[0]); } catch { return false; } };
695
1087
  const shellFiles = files.filter((f) => /\.(sh|bash)$/.test(f) && !/(^|\/)node_modules\//.test(f) && !otherShell(f)).sort();
696
- if (shellFiles.length) add({ id: "shellcheck", class: A, tool: "shellcheck", args: ["--format=gcc", "--external-sources", ...shellFiles], root: "content" });
1088
+ if (shellFiles.length && tier !== "release") notes.push("compile tier: shellcheck (added after 0.6.6) is not planned; it runs in the release tier (--to qualify|release)");
1089
+ if (shellFiles.length && tier === "release") add({ id: "shellcheck", class: A, tool: "shellcheck", tier: "release", args: ["--format=gcc", "--external-sources", ...shellFiles], root: "content" });
697
1090
  // No right-git drift scan: the one workflow right-git renders here, ci.yml, is hand-owned (its header;
698
1091
  // docs/CI.md) and held by scripts/ci/ci-scans-contract.mjs instead, so drift is always reported by design.
699
1092
 
@@ -706,22 +1099,36 @@ export function planScans({
706
1099
  for (const ws of workspaces) {
707
1100
  if (typeof ws !== "string") continue;
708
1101
  const dir = ws === "." ? "" : ws;
709
- const ignores = ["deny.toml", ".cargo/audit.toml"].some((f) => { try { return /\bignore\s*=\s*\[[^\]]*\S[^\]]*\]/s.test(readFile(path.join(contentRoot, dir, f))); } catch { return false; } });
710
- if (ignores && !(dir === "" && exists(path.join(contentRoot, "ci/advisory-exceptions.json")))) add({ id: `advisory-exceptions${dir ? `:${dir}` : ""}`, class: A, internal: "advisory-exceptions", dir, exceptionsFile: ".rightkit/advisory-exceptions.json" });
1102
+ // A non-empty list only: `[^\]\s]` is one character that is neither whitespace nor the closing bracket, so `ignore = []` followed by
1103
+ // a later `]` (the next table header) is not an ignore (Pulse, 2026-10-27: `\S` matched the `]` itself).
1104
+ const ignores = ["deny.toml", ".cargo/audit.toml"].some((f) => { try { return /\bignore\s*=\s*\[[^\]]*[^\]\s][^\]]*\]/s.test(readFile(path.join(contentRoot, dir, f))); } catch { return false; } });
1105
+ if (ignores && !(dir === "" && exists(path.join(contentRoot, "ci/advisory-exceptions.json")))) add({ id: `advisory-exceptions${dir ? `:${wsLabel(ws)}` : ""}`, class: A, internal: "advisory-exceptions", dir, exceptionsFile: ".rightkit/advisory-exceptions.json" });
711
1106
  }
712
1107
  // Like generated-docs, planned only when a changed file is one of the contract's inputs or projections. The generator runs from
713
1108
  // the trusted scanner checkout (the import below); the exported contract and projections are data it checks.
714
- if (files.some((f) => SUITE_API_INPUT.test(f)) && exists(path.join(contentRoot, "contracts/right-suite-api.v1.json"))) add({ id: "suite-api", class: A, internal: "suite-api" });
1109
+ if (files.some((f) => SUITE_API_INPUT.test(f)) && exists(path.join(contentRoot, "contracts/right-suite-api.v1.json"))) {
1110
+ // RightKit's own generator contract (scripts/gen-suite-api.mjs) is the checked repository's code: like scripts/preflight.mjs it
1111
+ // runs only when this gate is that repository's in-tree copy (and in CI), never from a pinned or installed copy checking the
1112
+ // repository (the broker's trusted gate refused RightKit's own compile on a module it could not find, 2026-10-27).
1113
+ // The trusted home is the gate's own repository (`trustedRoot`, the checkout rightkit-gate runs for: a commit check exports the
1114
+ // commit to a snapshot and hands that as contentRoot and liveRoot); the generator is code and runs only from that checkout's
1115
+ // in-tree gate, reading the snapshot as data. Without a trusted root (library callers), the live checkout or content root decides.
1116
+ const home = trustedRoot ?? (path.resolve(liveRoot) !== path.resolve(contentRoot) ? liveRoot : contentRoot);
1117
+ if (inTreeGate(home, exists)) add({ id: "suite-api", class: A, internal: "suite-api" });
1118
+ else notes.push("suite-api: RightKit's own generator contract runs from the in-tree gate and CI, not from this installed copy");
1119
+ }
715
1120
  if (rustTouched) {
716
1121
  for (const ws of touchedWs) add({ id: `cargo-metadata${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, ...metadataArgs()) });
717
- for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, ...cargoIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
1122
+ // Locked package listings are observations in RustRight (#1216). They run through the selected executor
1123
+ // from the checked Cargo inputs, just like metadata; an executor refusal remains a failed scan.
1124
+ if (tier === "release") for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, tier: "release", ...cargoIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
718
1125
  }
719
1126
  for (const ws of [...configWs].sort()) {
720
1127
  // Offline against the pinned database. The pin commit is part of the command (env), so a pin change is a different scan, and the
721
1128
  // placeholders are filled at run time from the materialised checkout (runScans).
722
1129
  const pin = advisoryPin ?? (advisoryPin = readAdvisoryPin());
723
1130
  const pinEnv = { RK_ADVISORY_DB_COMMIT: pin.commit };
724
- add({ id: `cargo-deny${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, "deny", "--offline", "--metadata-path", "{advisoryMetadata}", "--config", "{advisoryDenyConfig}", "check"), env: pinEnv, advisoryDb: "deny", advisoryPin: pin });
1131
+ add({ id: `cargo-deny${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, "deny", "--frozen", "--metadata-path", "{advisoryMetadata}", "--config", "{advisoryDenyConfig}", "check"), env: pinEnv, advisoryDb: "deny", advisoryPin: pin });
725
1132
  add({ id: `cargo-audit${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, "audit", "--db", "{advisoryDb}", "--no-fetch"), env: pinEnv, advisoryDb: "audit", advisoryPin: pin });
726
1133
  }
727
1134
  // The audit policy is the consumer's own: every package.json script that is exactly `pnpm audit <flags>` (only --prod, --dev,
@@ -770,12 +1177,15 @@ export function planScans({
770
1177
  // requiring a trusted-root run: deferred (never a pass), not run from submitted code.
771
1178
  // Planned only when a changed file is one of the generator's inputs (GENERATOR_INPUT).
772
1179
  if (exists(path.join(contentRoot, "scripts/generate-index.mjs")) && files.some((f) => GENERATOR_INPUT.test(f))) {
773
- const trusted = path.resolve(liveRoot) !== path.resolve(contentRoot) && exists(path.join(liveRoot, "scripts/generate-index.mjs"));
1180
+ // Full class A on the live checkout (`liveExecution`) already executes the repository's own code (its tests): there the
1181
+ // generator runs from that same checkout. Everywhere else the separation above holds.
1182
+ const trusted = (liveExecution || path.resolve(liveRoot) !== path.resolve(contentRoot)) && exists(path.join(liveRoot, "scripts/generate-index.mjs"));
774
1183
  add({
775
- id: "generated-docs", class: B, tool: "node", args: ["scripts/generate-index.mjs", "--check", "--root", "{contentRoot}"],
1184
+ id: "generated-docs", class: A, executes: true, tool: "node", args: ["scripts/generate-index.mjs", "--check", "--root", "{contentRoot}"],
776
1185
  ...(trusted ? {} : { deferred: "requires a trusted-root run: the generator is repository code and no separate trusted checkout provides it; the content root's copy is never executed" }),
777
1186
  });
778
1187
  }
1188
+ const clippyEntries = [];
779
1189
  for (const name of crateNames) {
780
1190
  // Feature sets exactly as CI's gate runs them (scripts/ci/gate.mjs, ci/gate.config.json rust.<crate>):
781
1191
  // clippy per `featureSets` (default features when absent); check per `checkFeatureSets` (default
@@ -788,16 +1198,25 @@ export function planScans({
788
1198
  // check runs only when no clippy set covers default features.
789
1199
  if (defaultCheck) add({ id: rustScanId.check(name), class: B, ...cargoIn(ws, ...checkArgs(crate, { locked: true })) });
790
1200
  checkSets.forEach((set, i) => add({ id: rustScanId.checkFeatures(name, i), class: B, ...cargoIn(ws, ...checkArgs(crate, { features: set, locked: true })) }));
791
- lintSets.forEach((set, i) => add({ id: rustScanId.clippy(name, lintSets.length, i), class: B, ...cargoIn(ws, ...clippyArgs(crate, { features: set, locked: true })) }));
1201
+ if (clippyBatching) lintSets.forEach((set) => clippyEntries.push({ name, crate, ws, set, count: lintSets.length, env: targetDirs?.[ws] ?? null }));
1202
+ else lintSets.forEach((set, i) => add({ id: rustScanId.clippy(name, lintSets.length, i), class: B, ...cargoIn(ws, ...clippyArgs(crate, { features: set, locked: true })) }));
792
1203
  // rustdoc flags go after `--` for this crate's library only: the broker owns RUSTDOCFLAGS and refuses
793
1204
  // a request that sets it. A crate with no library has no API docs to check.
794
1205
  if (exists(path.join(contentRoot, rustDir(name), "src", "lib.rs"))) {
795
1206
  add({ id: rustScanId.doc(name), class: B, ...cargoIn(ws, "rustdoc", "-p", crate, "--lib", ...lintSets[0], "--locked", "--", ...DOC_RUSTDOC_ARGS) });
796
1207
  }
797
1208
  }
1209
+ if (clippyBatching && clippyEntries.length) {
1210
+ // One Clippy scan per batch (row 8.6): the independent batches first, the sidecar-bound ones after them (row 8.8; the compile lane
1211
+ // drains in plan order). The reason a configuration stays separate is recorded in the plan notes.
1212
+ const sidecarBound = new Set(clippyEntries.filter((e) => sidecarBoundCrate({ dir: path.join(contentRoot, rustDir(e.name)), exists, readFile }).bound).map((e) => e.name));
1213
+ const plan = clippyBatchPlan(clippyEntries, { solo: shardSolo, separate: clippySeparate, sidecarBound, toolchain: cargoCommand.join(" ") });
1214
+ for (const b of plan.batches) add({ id: b.id, class: B, batch: { units: b.units, order: b.order, reason: b.reason }, ...cargoIn(b.ws, ...clippyArgs(b.crates, { features: b.set, locked: true })) });
1215
+ notes.push(...plan.notes);
1216
+ }
798
1217
  // Reverse dependents: a workspace crate that is not in scope but whose lock closure reaches one that is must still compile.
799
1218
  // A unit only depends on units of its own Cargo workspace (its own lock), so each workspace is judged by its own lock.
800
- const inScope = new Set(crateNames);
1219
+ const inScope = new Set(crateNames); const planned = new Set();
801
1220
  for (const ws of [...new Set(crateNames.map(wsOf))].sort()) {
802
1221
  const lock = lockOf(ws);
803
1222
  if (!lock) continue;
@@ -805,19 +1224,53 @@ export function planScans({
805
1224
  const scoped = crateNames.filter((n) => wsOf(n) === ws).map(crateOf);
806
1225
  const workspace = (rustMap ? [...rustMap.keys()] : listDir(path.join(contentRoot, "crates"))).filter((n) => wsOf(n) === ws);
807
1226
  const dependents = safeUnits(workspace.filter((n) => !inScope.has(n) && exists(path.join(contentRoot, rustDir(n), "Cargo.toml")) && scoped.some((t) => reach(lock, crateOf(n), memo).has(t))).sort(), "crate");
808
- for (const name of dependents) add({ id: `check-dependent:${name}`, class: B, ...cargoIn(ws, ...checkArgs(crateOf(name), { locked: true })) });
1227
+ for (const name of dependents) { planned.add(name); add({ id: `check-dependent:${name}`, class: B, ...cargoIn(ws, ...checkArgs(crateOf(name), { locked: true })) }); }
1228
+ }
1229
+ // Across Cargo workspaces (one workspace and lock per crate): a unit that path-depends on a unit in scope, transitively, is judged by
1230
+ // the manifests' path dependencies, since no lock holds both.
1231
+ if (typeof crateDependents === "function" && rustMap && crateNames.length) {
1232
+ const across = [...crateDependents(new Set(crateNames))].filter((n) => !inScope.has(n) && !planned.has(n) && rustMap.has(n) && exists(path.join(contentRoot, rustDir(n), "Cargo.toml"))).sort();
1233
+ for (const name of safeUnits(across, "crate")) add({ id: `check-dependent:${name}`, class: B, ...cargoIn(wsOf(name), ...checkArgs(crateOf(name), { locked: true })) });
809
1234
  }
810
1235
  // A linter config in the unit's directory or any ancestor of it (a repository-level config covers every unit, as the tools look it up).
811
1236
  const hasConfig = (dir, names) => ancestorDirsOf(dir).some((d) => names.some((f) => exists(path.join(liveRoot, inDir(d, f)))));
1237
+ // tsc reads a workspace sibling's types where its package.json exports point: built output (dist/*.d.ts) that no commit holds, so a
1238
+ // check of the committed tree (CI, a commit snapshot) cannot find them, and a checkout with an old build checks against stale types.
1239
+ // A package that depends on another workspace package is therefore checked through a generated project that maps each sibling's
1240
+ // exported entry points to its committed sources (TSC_PROJECT, built by the runner); every other package keeps its own tsconfig.
1241
+ // `--noEmit` stays on the command line as well as in the project: the class A launch guard judges a tsc launch by its argv alone.
1242
+ const workspaceDirs = (npmMap ? [...npmMap.values()].map((u) => u.dir) : listDir(path.join(liveRoot, "packages")).map((n) => `packages/${n}`)).sort();
1243
+ const manifestOf = (dir) => { try { return JSON.parse(readFile(path.join(liveRoot, dir, "package.json")).replace(/^/, "")); } catch { return null; } };
1244
+ const workspaceNames = new Map(workspaceDirs.map((d) => [manifestOf(d)?.name, d]).filter(([n]) => typeof n === "string"));
1245
+ const usesWorkspaceSibling = (dir) => {
1246
+ const m = manifestOf(dir);
1247
+ return ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((k) => Object.keys(m?.[k] ?? {}).some((n) => n !== m.name && workspaceNames.has(n)));
1248
+ };
812
1249
  for (const name of pkgNames) {
813
1250
  const dir = npmDir(name);
814
- if (exists(path.join(liveRoot, dir, "tsconfig.json"))) add({ id: `tsc:${name}`, class: B, tool: "pnpm", args: ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"], env: PNPM_RUN_ENV, cwd: dir });
815
- if (hasConfig(dir, ESLINT_CONFIG_FILES)) add({ id: `eslint:${name}`, class: B, tool: "pnpm", args: ["exec", "eslint", "."], env: PNPM_RUN_ENV, cwd: dir });
816
- if (hasConfig(dir, BIOME_CONFIG_FILES)) add({ id: `biome:${name}`, class: B, tool: "pnpm", args: ["exec", "biome", "lint", "."], env: PNPM_RUN_ENV, cwd: dir });
1251
+ if (exists(path.join(liveRoot, dir, "tsconfig.json"))) {
1252
+ add(usesWorkspaceSibling(dir)
1253
+ ? { id: `tsc:${name}`, class: A, executes: true, tool: "pnpm", args: ["exec", "tsc", "--noEmit", "-p", TSC_PROJECT], tscWorkspace: [...workspaceNames.values()].sort(), env: PNPM_RUN_ENV, cwd: dir }
1254
+ : { id: `tsc:${name}`, class: A, executes: true, tool: "pnpm", args: ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"], env: PNPM_RUN_ENV, cwd: dir });
1255
+ }
1256
+ if (hasConfig(dir, ESLINT_CONFIG_FILES)) add({ id: `eslint:${name}`, class: A, executes: true, tool: "pnpm", args: ["exec", "eslint", "."], env: PNPM_RUN_ENV, cwd: dir });
1257
+ if (hasConfig(dir, BIOME_CONFIG_FILES)) add({ id: `biome:${name}`, class: A, executes: true, tool: "pnpm", args: ["exec", "biome", "lint", "."], env: PNPM_RUN_ENV, cwd: dir });
817
1258
  }
818
- if (files.some((f) => f.startsWith("swift/") || f === "Package.swift")) {
819
- if (platform === "darwin") add({ id: "swift-build", class: B, tool: "swift", args: ["build"] });
820
- else notes.push("swift files changed; `swift build` is required on the Mac host before its push");
1259
+ // One `swift build` per Swift package a changed file belongs to: the nearest Package.swift at or above the file, built in its own
1260
+ // directory (rightkit#1229: Pulse's package is swift/RightKitHub, and a root `swift build` found no package and blocked every push).
1261
+ const swiftFiles = files.filter((f) => f.startsWith("swift/") || f === "Package.swift" || f.endsWith("/Package.swift"));
1262
+ if (swiftFiles.length) {
1263
+ const packages = new Set();
1264
+ for (const f of swiftFiles) {
1265
+ const owner = ancestorDirsOf(path.posix.dirname(f)).reverse().find((d) => exists(path.join(contentRoot, inDir(d, "Package.swift"))));
1266
+ if (owner !== undefined) packages.add(owner);
1267
+ else notes.push(`${f}: no Package.swift at or above it, so no Swift package builds it`);
1268
+ }
1269
+ for (const dir of [...packages].sort()) {
1270
+ const id = dir === "" || dir === "." ? "swift-build" : `swift-build:${dir}`;
1271
+ if (platform === "darwin") add({ id, class: B, tool: "swift", args: ["build"], ...(dir === "" || dir === "." ? {} : { cwd: dir }) });
1272
+ else notes.push(`swift files changed in ${dir === "" || dir === "." ? "the root package" : dir}; \`swift build\` is required on the Mac host before its push`);
1273
+ }
821
1274
  }
822
1275
  if (includeTests) {
823
1276
  const deferredToCi = [];
@@ -851,8 +1304,64 @@ export function planScans({
851
1304
  // A declaration (`npm.<unit>.requiresBroker`, declaredBrokerCall) covers what the script text does not show; a call the scripts do show wins.
852
1305
  const declared = declaredBrokerCall(name, own.requiresBroker);
853
1306
  if (declared.problem) notes.push(declared.problem);
1307
+ // `npm.<unit>.afterClassA.reason` (ci/gate.config.json): the unit's tests launch stand-ins named like compilers (stub scripts,
1308
+ // which can run anything), so they cannot run under the class A launch guard; they run after class A, with the compile scans.
1309
+ // A reason is required.
1310
+ const afterClassA = typeof own.afterClassA?.reason === "string" && own.afterClassA.reason.trim() !== "" ? own.afterClassA.reason.trim() : null;
854
1311
  const brokerCall = scriptBrokerCall(manifest.scripts, "test") ?? declared.call ?? null;
855
- add({ id: `test:npm:${name}`, class: B, ...(own.quietTests === true ? { quiet: true } : {}), ...(brokerCall ? { brokerCall } : {}), tool: "pnpm", args: ["--dir", npmDir(name), "run", "test"], env: { ...PNPM_RUN_ENV, RIGHTKIT_ISOLATED_CHECKOUT: "1" } });
1312
+ const buildCall = brokerCall ? null : scriptBuildCall(manifest.scripts, "test");
1313
+ const laterReason = brokerCall ? null : buildCall ? `its test script builds before testing (${buildCall.command}); it runs after class A, with the compile scans` : afterClassA ? `declared to run after class A (ci/gate.config.json npm.${name}.afterClassA): ${afterClassA}` : null;
1314
+ // Isolated only where the tests run on an export (contentRoot is not the live checkout): there sibling repositories are
1315
+ // absent by construction. On the live checkout (full class A, broker admission) the cross-app contract tests run for real.
1316
+ const isolatedCheckout = path.resolve(contentRoot) !== path.resolve(liveRoot);
1317
+ // `npm.<unit>.classATests.evidence` (ci/gate.config.json): the unit's tests were shown, by import inspection and launch observation
1318
+ // under the class A launch guard, to compile and build nothing (docs/GATE-CLASS-A-INVENTORY.md "Test units"); they run in class A,
1319
+ // still under the guard, which refuses any compile at run time. Never when the script reaches the broker, builds, or is declared after class A.
1320
+ const classAEvidence = typeof own.classATests?.evidence === "string" && own.classATests.evidence.trim() !== "" ? own.classATests.evidence.trim() : null;
1321
+ const inClassA = classAEvidence && !brokerCall && !buildCall && !afterClassA;
1322
+ if (classAEvidence && !inClassA) notes.push(`npm.${name}.classATests is ignored: ${brokerCall ? "its test script reaches the broker" : buildCall ? `its test script builds (${buildCall.command})` : "it is declared afterClassA"}; it runs after class A`);
1323
+ // A commit snapshot's compile (targetDirs): a test that reaches cargo writes where cargo puts the outputs of the workspace it
1324
+ // finds from the directory it runs in, which would be inside the snapshot. Bound to that workspace's checkout target directory
1325
+ // when the reachable path proves the directory (cargoReachOf); refused before launch only when it reaches Rust unprovably.
1326
+ let cargoTarget = null;
1327
+ if (targetDirs && cargoShim) {
1328
+ // Advisory only: the wrapper binds or refuses each cargo the test starts, whatever path reaches it.
1329
+ const reach = cargoReachOf({ unitDir: npmDir(name), scripts: manifest.scripts, declared });
1330
+ if (reach.unprovable && Object.keys(targetDirs).length) notes.push(`npm unit ${name}: the static walk cannot prove where its cargo writes (${reach.unprovable}); the runtime cargo wrapper binds each invocation to its workspace's checkout target directory or refuses it`);
1331
+ } else if (targetDirs) {
1332
+ const unitDir = npmDir(name);
1333
+ const scriptsOf = (dir) => { try { const m = JSON.parse(readFile(path.join(contentRoot, ...(dir === "." ? [] : dir.split("/")), "package.json"))); return m?.scripts && typeof m.scripts === "object" ? m.scripts : {}; } catch { return null; } };
1334
+ const packageDirOf = (pkg) => {
1335
+ for (const [unit, entry] of npmMap ?? []) {
1336
+ if (unit === pkg) return entry.dir;
1337
+ try { if (JSON.parse(readFile(path.join(contentRoot, ...(entry.dir === "." ? [] : entry.dir.split("/")), "package.json"))).name === pkg) return entry.dir; } catch { /* unreadable: not this one */ }
1338
+ }
1339
+ return null;
1340
+ };
1341
+ // Every call the test reaches, not the first: one CARGO_TARGET_DIR binds the whole test, so all of them must be one workspace.
1342
+ const reach = cargoReachOf({ unitDir, scripts: manifest.scripts, declared, readScripts: scriptsOf, packageDirOf });
1343
+ const hasRust = Object.keys(targetDirs).length > 0;
1344
+ if (reach.unprovable) {
1345
+ if (hasRust) { refuse(`npm unit ${name}`, `its test reaches cargo while the compile reads a commit snapshot, and which workspace's outputs it writes cannot be proven (${reach.unprovable}); outputs must be the checkout's`); continue; }
1346
+ } else if (reach.dir !== undefined || reach.dirs) {
1347
+ const workspaceOf = (from) => {
1348
+ let manifestDir = null;
1349
+ for (let d = from; ; d = path.posix.dirname(d) === "." || d === "." ? "." : path.posix.dirname(d)) {
1350
+ if (exists(path.join(contentRoot, ...(d === "." ? [] : d.split("/")), "Cargo.toml"))) { manifestDir = d; break; }
1351
+ if (d === ".") break;
1352
+ }
1353
+ const ws = manifestDir === null ? null : Object.hasOwn(targetDirs, manifestDir) ? manifestDir : ([...(rustMap?.values() ?? [])].find((u) => u.dir === manifestDir)?.cargoDir ?? null);
1354
+ return ws && targetDirs[ws] ? { ws } : { why: `its test reaches cargo from ${from}, ${manifestDir === null ? `which no Cargo.toml encloses` : `whose ${manifestDir}/Cargo.toml is not a known workspace or member`}; outputs must be the checkout's` };
1355
+ };
1356
+ const found = (reach.dirs ?? [reach.dir]).map(workspaceOf);
1357
+ const unknown = found.find((f) => f.why);
1358
+ if (unknown) { refuse(`npm unit ${name}`, unknown.why); continue; }
1359
+ const workspaces = [...new Set(found.map((f) => f.ws))];
1360
+ if (workspaces.length > 1) { refuse(`npm unit ${name}`, `its test reaches cargo in more than one workspace (${workspaces.join(", ")} from ${reach.dirs.join(", ")}); one CARGO_TARGET_DIR binds the whole test, so one of them would write into another's outputs`); continue; }
1361
+ cargoTarget = targetDirs[workspaces[0]];
1362
+ }
1363
+ }
1364
+ add({ id: `test:npm:${name}`, class: inClassA ? A : B, kind: "test", executes: true, classReason: inClassA ? `class A on recorded evidence (${classAEvidence}); run under the launch guard` : brokerCall ? "its test script reaches the broker (compiles); it runs with the compile scans" : laterReason ?? "test scripts execute project code and run after class A", ...(own.quietTests === true ? { quiet: true } : {}), ...(brokerCall ? { brokerCall } : {}), tool: "pnpm", args: ["--dir", npmDir(name), "run", "test"], env: { ...PNPM_RUN_ENV, ...(isolatedCheckout ? { RIGHTKIT_ISOLATED_CHECKOUT: "1" } : {}), ...(cargoTarget ? { CARGO_TARGET_DIR: cargoTarget } : {}) } });
856
1365
  }
857
1366
  if (deferredToCi.length) notes.push(`${deferredToCi.length} quiet test scan(s) left to the CI shards (one crate per runner), not run here: ${deferredToCi.join(", ")}`);
858
1367
  // scripts/ci tests run when scripts/, ci/ or .github/ changed, or a file the scripts/ci sources reference (imports and
@@ -883,7 +1392,7 @@ export function planScans({
883
1392
  // The suite is the exported commit's test files, named explicitly: an untracked WIP test in the live
884
1393
  // checkout is never run and can neither block nor pass a push.
885
1394
  const suite = listDir(path.join(contentRoot, "scripts", "ci")).filter((n) => n.endsWith(".test.mjs")).sort().map((n) => `scripts/ci/${n}`);
886
- add({ id: "test:scripts-ci", class: B, tool: "node", args: ["--test", ...(suite.length ? suite : ["scripts/ci/*.test.mjs"])] });
1395
+ add({ id: "test:scripts-ci", class: B, kind: "test", executes: true, classReason: "tests execute project code and run after class A", tool: "node", args: ["--test", ...(suite.length ? suite : ["scripts/ci/*.test.mjs"])] });
887
1396
  }
888
1397
  }
889
1398
 
@@ -904,7 +1413,7 @@ export function planScans({
904
1413
  if (dup) throw new Error(`planScans: duplicate scan id ${dup}`);
905
1414
  const classDigest = (cls) => digestScanSet(scans.filter((s) => s.class === cls));
906
1415
  return {
907
- schema: SCAN_RESULT_SCHEMA, roots: { contentRoot, liveRoot }, changed: files, inputDigest, scans, notes, deferred,
1416
+ schema: SCAN_RESULT_SCHEMA, roots: { contentRoot, liveRoot, ...(cargoLiveRoot ? { cargoLiveRoot } : {}) }, changed: files, inputDigest, scans, notes, deferred,
908
1417
  scanSetDigest: digestScanSet(scans), classDigests: { A: classDigest(A), B: classDigest(B) },
909
1418
  };
910
1419
  }
@@ -921,7 +1430,7 @@ const SAFE_SHIM_PATH = /^[A-Za-z0-9_.,:=@+ \/\\()-]+$/;
921
1430
  * run any other .cmd/.bat shim only when its path and every argument pass a strict allowlist.
922
1431
  * Returns { file, args, verbatim } or { error }.
923
1432
  */
924
- export function resolveInvocation(command, args, { platform = process.platform, env = process.env, exists = fs.existsSync, execPath = process.execPath } = {}) {
1433
+ export function resolveInvocation(command, args, { platform = process.platform, env = process.env, exists = fs.existsSync, execPath = process.execPath, readFile = (f) => fs.readFileSync(f, "utf8") } = {}) {
925
1434
  if (platform !== "win32") return { file: command, args, verbatim: false };
926
1435
  const hasExt = /\.[A-Za-z0-9]{1,4}$/.test(command);
927
1436
  const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path");
@@ -936,6 +1445,10 @@ export function resolveInvocation(command, args, { platform = process.platform,
936
1445
  if (!/\.(cmd|bat)$/i.test(found)) return { file: found, args, verbatim: false };
937
1446
  const cjs = path.win32.join(path.win32.dirname(found), "node_modules", "pnpm", "bin", "pnpm.cjs");
938
1447
  if (/^pnpm\.cmd$/i.test(path.win32.basename(found)) && exists(cjs)) return { file: execPath, args: [cjs, ...args], verbatim: false };
1448
+ // A Node cmd-shim (what npm, pnpm and nvm install: `"%~dp0\...\entry.js" %*`, npm's `SET "NPM_CLI_JS=%~dp0\...npm-cli.js"`) runs
1449
+ // its JS entry with node: started here as node + that entry, no shell, so any argument passes unchanged (rightkit#1261).
1450
+ const entry = cmdShimEntry(found, { readFile, exists });
1451
+ if (entry) return { file: execPath, args: [entry, ...args], verbatim: false };
939
1452
  const bad = args.find((a) => !SAFE_SHIM_ARG.test(a));
940
1453
  if (bad !== undefined) return { error: new Error(`refusing to pass ${JSON.stringify(bad)} to the shell shim ${found}`) };
941
1454
  if (!SAFE_SHIM_PATH.test(found)) return { error: new Error(`refusing to run shell shim ${found}: unsafe path`) };
@@ -943,14 +1456,83 @@ export function resolveInvocation(command, args, { platform = process.platform,
943
1456
  return { file: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `""${found}"${args.map((a) => ` ${a}`).join("")}"`], verbatim: true };
944
1457
  }
945
1458
 
1459
+ /**
1460
+ * The JS entry a Node cmd-shim at `shim` runs, or null: the line that passes the caller's arguments (`%*`) names it, either as a
1461
+ * `%~dp0`-relative path (`"%~dp0\\..\\pnpm.mjs" %*`) or through a variable set to one (npm's `"%NODE_EXE%" "%NPM_CLI_JS%" %*` with
1462
+ * `SET "NPM_CLI_JS=%~dp0\\node_modules\\npm\\bin\\npm-cli.js"`). Helpers the shim runs on other lines (npm's npm-prefix.js) are not it.
1463
+ */
1464
+ export function cmdShimEntry(shim, { readFile = (f) => fs.readFileSync(f, "utf8"), exists = fs.existsSync } = {}) {
1465
+ let text;
1466
+ try { text = String(readFile(shim)); } catch { return null; }
1467
+ const vars = new Map();
1468
+ // Every value a variable is set to (npm's NPM_CLI_JS is reassigned to a global-prefix copy when one exists): the %~dp0 one is used.
1469
+ for (const m of text.matchAll(/^\s*@?SET\s+"?([A-Za-z_][A-Za-z0-9_]*)=([^"\r\n]*)"?\s*$/gim)) vars.set(m[1].toUpperCase(), [...(vars.get(m[1].toUpperCase()) ?? []), m[2]]);
1470
+ const fromDp0 = (value) => {
1471
+ const m = /^%~dp0\\?(.+\.(?:mjs|cjs|js))$/i.exec(String(value ?? "").trim());
1472
+ if (!m) return null;
1473
+ const entry = path.win32.resolve(path.win32.dirname(shim), m[1]);
1474
+ return exists(entry) ? entry : null;
1475
+ };
1476
+ for (const line of text.split(/\r?\n/)) {
1477
+ if (!line.includes("%*")) continue;
1478
+ for (const q of line.matchAll(/"([^"]+)"/g)) {
1479
+ const token = q[1];
1480
+ const direct = fromDp0(token);
1481
+ if (direct) return direct;
1482
+ const v = /^%([A-Za-z_][A-Za-z0-9_]*)%$/.exec(token);
1483
+ if (v) for (const value of vars.get(v[1].toUpperCase()) ?? []) { const viaVar = fromDp0(value); if (viaVar) return viaVar; }
1484
+ }
1485
+ }
1486
+ return null;
1487
+ }
1488
+
946
1489
  /** Default spawn: synchronous, no console window, never `shell: true`. */
947
1490
  export function defaultSpawn(command, args, options = {}) {
1491
+ if (options.boundedMs) return boundedSpawn(command, args, options);
948
1492
  const inv = resolveInvocation(command, args, { env: options.env ?? process.env });
949
1493
  if (inv.error) return { status: null, stdout: "", stderr: "", error: inv.error };
950
1494
  return spawnSync(inv.file, inv.args, { encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024, windowsVerbatimArguments: inv.verbatim, ...options, shell: false });
951
1495
  }
952
1496
 
953
1497
  const OUTPUT_KEEP = 4 * 1024 * 1024;
1498
+ /**
1499
+ * defaultSpawn with a deadline (`options.boundedMs`), for short identity probes that must never hang the caller: the command runs
1500
+ * in a helper node process through defaultSpawnAsync, which on the deadline kills the command's whole process tree
1501
+ * (killProcessTree) and reports `timedOut: true`. The helper records the command's pid first; if the helper itself overruns its
1502
+ * backstop (`backstopMs`, default deadline + 15 s), it is killed and the command's tree is killed from here by that pid, so no
1503
+ * probe outlives the call. A launch failure keeps its `error` (e.g. ENOENT); a command that ran keeps its `status`.
1504
+ */
1505
+ export function boundedSpawn(command, args, { boundedMs, backstopMs = boundedMs + 15000, cwd, env, pidFile = null } = {}) {
1506
+ const dir = pidFile ? null : fs.mkdtempSync(path.join(os.tmpdir(), "rk-probe-"));
1507
+ const pids = pidFile ?? path.join(dir, "pid");
1508
+ const script = `import(${JSON.stringify(new URL(import.meta.url).href)}).then(async (m) => {
1509
+ const [command, args, ms, pidFile] = JSON.parse(process.argv[1]); const fs = await import("node:fs");
1510
+ const ac = new AbortController(); const timer = setTimeout(() => ac.abort(), ms);
1511
+ const r = await m.defaultSpawnAsync(command, args, { signal: ac.signal, onSpawn: (pid) => fs.writeFileSync(pidFile, String(pid)) }); clearTimeout(timer);
1512
+ process.stdout.write(JSON.stringify({ status: r.status, stdout: r.stdout, stderr: r.stderr, timedOut: r.cancelled === true, error: r.cancelled ? null : (r.error ? { code: r.error.code ?? null, message: r.error.message } : null) }));
1513
+ });`;
1514
+ try {
1515
+ const r = spawnSync(process.execPath, ["--input-type=module", "-e", script, JSON.stringify([command, args, boundedMs, pids])], { cwd, env: env ?? process.env, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"], timeout: backstopMs, killSignal: "SIGKILL", maxBuffer: 16 * 1024 * 1024 });
1516
+ const timeout = (why) => ({ status: null, stdout: "", stderr: "", timedOut: true, error: Object.assign(new Error(`timed out after ${boundedMs} ms${why}`), { code: "ETIMEDOUT" }) });
1517
+ if (r.error?.code === "ETIMEDOUT") {
1518
+ let pid = null; try { pid = Number(fs.readFileSync(pids, "utf8")) || null; } catch { /* never started */ }
1519
+ if (pid) killProcessTree({ pid });
1520
+ return timeout(" (backstop)");
1521
+ }
1522
+ try {
1523
+ const o = JSON.parse(r.stdout);
1524
+ if (o.timedOut) return timeout("");
1525
+ return { status: o.status, stdout: o.stdout ?? "", stderr: o.stderr ?? "", ...(o.error ? { error: Object.assign(new Error(o.error.message), { code: o.error.code }) } : {}) };
1526
+ } catch { return { status: null, stdout: "", stderr: r.stderr ?? "", error: new Error(`the bounded probe gave no result (${r.error?.message ?? `exit ${r.status}`})`) }; }
1527
+ } finally { if (dir) fs.rmSync(dir, { recursive: true, force: true }); }
1528
+ }
1529
+
1530
+ /** The environment additions that load the class A launch guard into a test row's Node processes (NODE_OPTIONS appended, not replaced). */
1531
+ export function launchGuardEnv(env = process.env) {
1532
+ const current = String(env.NODE_OPTIONS ?? "");
1533
+ return { RIGHTKIT_CLASS_A_LAUNCH_GUARD: "1", NODE_OPTIONS: current.includes(LAUNCH_GUARD_IMPORT) ? current : `${current} ${LAUNCH_GUARD_IMPORT}`.trim() };
1534
+ }
1535
+
954
1536
  /** Kill `child` and everything it started: taskkill /T /F on Windows, the whole process group elsewhere (the child leads one when spawned with a signal). */
955
1537
  export function killProcessTree(child, { platform = process.platform, run = spawnSync, kill = (pid, sig) => process.kill(pid, sig) } = {}) {
956
1538
  if (!child?.pid) return;
@@ -967,17 +1549,21 @@ export function killProcessTree(child, { platform = process.platform, run = spaw
967
1549
  * With `options.stdoutFile`, stdout is written to that file whole instead of kept in memory.
968
1550
  */
969
1551
  export function defaultSpawnAsync(command, args, options = {}) {
970
- const { signal, stdoutFile, ...rest } = options;
1552
+ const { signal, stdoutFile, onSpawn, ...rest } = options;
971
1553
  const inv = resolveInvocation(command, args, { env: rest.env ?? process.env });
972
1554
  if (inv.error) return Promise.resolve({ status: null, stdout: "", stderr: "", error: inv.error });
973
1555
  if (signal?.aborted) return Promise.resolve({ status: null, stdout: "", stderr: "", error: new Error("cancelled"), cancelled: true });
974
1556
  return new Promise((resolve) => {
975
1557
  let stdout = ""; let stderr = ""; let error = null;
976
- const keep = (buf, chunk) => { const next = buf + chunk; return next.length > OUTPUT_KEEP ? next.slice(-OUTPUT_KEEP) : next; };
1558
+ let truncated = false;
1559
+ const keep = (buf, chunk) => { const next = buf + chunk; if (next.length > OUTPUT_KEEP) { truncated = true; return next.slice(-OUTPUT_KEEP); } return next; };
977
1560
  let child;
978
1561
  try {
979
- child = spawnChild(inv.file, inv.args, { windowsHide: true, windowsVerbatimArguments: inv.verbatim, ...(signal && process.platform !== "win32" ? { detached: true } : {}), ...rest, shell: false, stdio: ["ignore", "pipe", "pipe"] });
1562
+ // stdin is closed: no scan reads it, and an open pipe nobody writes can block a tool that probes it (Windows).
1563
+ // A detached child (POSIX, its own group for the abort kill) is recorded under the held build ownership (build-ownership.mjs).
1564
+ child = spawnUnderOwnership(spawnChild, inv.file, inv.args, { windowsHide: true, windowsVerbatimArguments: inv.verbatim, stdio: ["ignore", "pipe", "pipe"], ...(signal && process.platform !== "win32" ? { detached: true } : {}), ...rest, shell: false, stdio: ["ignore", "pipe", "pipe"] });
980
1565
  } catch (e) { resolve({ status: null, stdout: "", stderr: "", error: e }); return; }
1566
+ if (onSpawn && child.pid) onSpawn(child.pid);
981
1567
  const onAbort = () => killProcessTree(child);
982
1568
  signal?.addEventListener("abort", onAbort, { once: true });
983
1569
  child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8");
@@ -989,7 +1575,7 @@ export function defaultSpawnAsync(command, args, options = {}) {
989
1575
  if (sink) await new Promise((done) => (sink.writableFinished ? done() : sink.on("finish", done).on("error", done)));
990
1576
  signal?.removeEventListener("abort", onAbort);
991
1577
  const cancelled = signal?.aborted === true;
992
- resolve({ status, stdout, stderr, ...(error ? { error } : cancelled ? { error: new Error("cancelled") } : {}), ...(cancelled ? { cancelled: true } : {}) });
1578
+ resolve({ status, stdout, stderr, ...(truncated ? { truncated: true } : {}), ...(error ? { error } : cancelled ? { error: new Error("cancelled") } : {}), ...(cancelled ? { cancelled: true } : {}) });
993
1579
  });
994
1580
  if (signal?.aborted) onAbort();
995
1581
  });
@@ -1000,6 +1586,80 @@ export function defaultSpawnAsync(command, args, options = {}) {
1000
1586
  // time; a test scan marked `quiet` (ci/gate.config.json `rust.<crate>.quietTests`: the crate's tests share ports, devices,
1001
1587
  // wall-clock budgets or the broker's own queue, so they fail under load) runs last, one at a time, under the broker's quiet
1002
1588
  // lease when the gate can take one (runScans `quietLane`).
1589
+ /** The counts a test runner printed last: node:test's `ℹ tests N` / `ℹ pass` / `ℹ fail` / `ℹ skipped` / `ℹ todo` lines, or vitest's
1590
+ * `Tests N passed | M skipped | K failed (T)` line. Null when neither is present. Several node:test summaries (a script that runs
1591
+ * the runner more than once) are summed. */
1592
+ /** null when a test summary is evidence of a pass; otherwise { code, detail }. Used for fresh and cached test rows alike. */
1593
+ export function testResultsProblem(results) {
1594
+ if (!results) return { code: "NO_TEST_SUMMARY", detail: "no test counts recorded" };
1595
+ if (!(results.tests > 0)) return { code: "NO_TESTS", detail: "the test scan executed no test" };
1596
+ if (results.fail > 0) return { code: "SUMMARY_FAILED", detail: `the test runner exited 0 but its summary reports ${results.fail} failed test(s)` };
1597
+ // A suite that ran tests but passed none of them (every test skipped or todo) proves nothing (D17, rightkit#1236).
1598
+ if (!(results.pass > 0)) return { code: "ALL_SKIPPED", detail: `the test scan passed no test: ${results.skipped ?? 0} skipped, ${results.todo ?? 0} todo of ${results.tests}` };
1599
+ return null;
1600
+ }
1601
+
1602
+ export function parseTestSummary(text) {
1603
+ const lines = String(text ?? "").split(/\r?\n/);
1604
+ const node = { tests: 0, pass: 0, fail: 0, skipped: 0, todo: 0 }; let sawNode = false;
1605
+ for (const line of lines) {
1606
+ const m = /^[ℹ#]\s+(tests|pass|fail|skipped|todo)\s+(\d+)\s*$/.exec(line.trim());
1607
+ if (m) { node[m[1]] += Number(m[2]); sawNode = true; }
1608
+ }
1609
+ if (sawNode) return { runner: "node:test", ...node };
1610
+ const vt = lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "")).reverse().find((l) => /^\s*Tests\s+.*\(\d+\)\s*$/.test(l));
1611
+ if (vt) {
1612
+ const n = (re) => Number((re.exec(vt) ?? [0, 0])[1]);
1613
+ const tests = Number(/\((\d+)\)\s*$/.exec(vt)[1]);
1614
+ return { runner: "vitest", tests, pass: n(/(\d+) passed/), fail: n(/(\d+) failed/), skipped: n(/(\d+) skipped/), todo: n(/(\d+) todo/) };
1615
+ }
1616
+ return null;
1617
+ }
1618
+
1619
+ /** The largest inline `run:` script a workflow may carry while the pinned actionlint is 1.7.12 (rhysd/actionlint#702). */
1620
+ export const RUN_SCRIPT_LIMIT_BYTES = 3800;
1621
+ /** Every block `run:` script (`run: |` or `run: >`) of a workflow text larger than `limit` bytes, as "<file>:<line> <bytes> bytes".
1622
+ * Line-based and conservative: the block is every following line indented deeper than the `run:` key (blank lines included),
1623
+ * with that common indentation removed, as YAML reads a literal block. */
1624
+ export function oversizedRunScripts(text, file, limit = RUN_SCRIPT_LIMIT_BYTES) {
1625
+ const lines = String(text).split(/\r?\n/);
1626
+ const out = [];
1627
+ for (let i = 0; i < lines.length; i += 1) {
1628
+ const m = /^(\s*)(?:-\s+)?run:\s*[|>][-+0-9]*\s*$/.exec(lines[i]);
1629
+ if (!m) continue;
1630
+ const keyIndent = lines[i].indexOf("run:");
1631
+ const body = [];
1632
+ let j = i + 1;
1633
+ for (; j < lines.length; j += 1) {
1634
+ const line = lines[j];
1635
+ if (line.trim() === "") { body.push(""); continue; }
1636
+ if (line.length - line.trimStart().length <= keyIndent) break;
1637
+ body.push(line);
1638
+ }
1639
+ while (body.length && body[body.length - 1] === "") body.pop();
1640
+ const indent = Math.min(...body.filter(Boolean).map((l) => l.length - l.trimStart().length));
1641
+ const script = body.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n") + "\n";
1642
+ const bytes = Buffer.byteLength(script);
1643
+ if (bytes > limit) out.push(`${file}:${i + 1} ${bytes} bytes`);
1644
+ i = j - 1;
1645
+ }
1646
+ return out;
1647
+ }
1648
+
1649
+ /** The time limit of one scan: its own `timeoutMs`, then the run's `scanTimeoutMs`, else 5 min for actionlint, 30 min for class A and
1650
+ * 60 min for class B (a cold compile of the largest crate). */
1651
+ /** The broker's quiet lease ceiling (rustright-state quiet.rs QUIET_LEASE_MAX_TTL_MS) and the quiet suite timeout under it. */
1652
+ export const QUIET_LEASE_MAX_MS = 30 * 60_000;
1653
+ export const QUIET_SCAN_MAX_MS = 26 * 60_000;
1654
+ /** A tool version probe's bound; with QUIET_SCAN_MAX_MS it keeps a whole leased quiet suite (probes + run) under the 30 min lease. */
1655
+ export const PROBE_TIMEOUT_MS = 60_000;
1656
+ export function scanTimeoutMs(scan, options = {}) {
1657
+ if (Number.isFinite(scan?.timeoutMs) && scan.timeoutMs > 0) return scan.timeoutMs;
1658
+ if (Number.isFinite(options.scanTimeoutMs) && options.scanTimeoutMs > 0) return options.scanTimeoutMs;
1659
+ if (scan?.tool === "actionlint") return 5 * 60_000;
1660
+ return scan?.class === SCAN_CLASS.B ? 60 * 60_000 : 30 * 60_000;
1661
+ }
1662
+
1003
1663
  export function laneOf(scan) {
1004
1664
  if (/^(clippy|check|check-features|check-dependent):/.test(scan.id)) return "compile";
1005
1665
  if (/^doc:/.test(scan.id)) return "doc";
@@ -1052,6 +1712,30 @@ async function defaultScanSecrets() {
1052
1712
  return (await import("./publish-cargo.mjs")).scanCrateForSecrets;
1053
1713
  }
1054
1714
 
1715
+ /**
1716
+ * For a content root that is a git worktree's toplevel: a predicate over absolute paths that is true for every git-ignored file or
1717
+ * directory there (`git ls-files --others --ignored --exclude-standard --directory`, which lists an ignored directory once, with a
1718
+ * trailing slash). Null when the root is not a git toplevel (a committed export) or git cannot answer: then nothing is skipped.
1719
+ */
1720
+ export async function ignoredPathSkip(ctx) {
1721
+ const git = async (args) => { try { return await ctx.deps.spawn("git", args, { cwd: ctx.contentRoot, encoding: "utf8", env: ctx.deps.env }); } catch { return null; } };
1722
+ const top = await git(["rev-parse", "--show-toplevel"]);
1723
+ if (!top || top.status !== 0) return null;
1724
+ const same = (a, b) => { try { return fs.realpathSync(a) === fs.realpathSync(b); } catch { return path.resolve(a) === path.resolve(b); } };
1725
+ if (!same(String(top.stdout ?? "").trim(), ctx.contentRoot)) return null;
1726
+ const listed = await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"]);
1727
+ if (!listed || listed.status !== 0) return null;
1728
+ const entries = String(listed.stdout ?? "").split("\0").filter(Boolean);
1729
+ const dirs = entries.filter((e) => e.endsWith("/"));
1730
+ const files = new Set(entries.filter((e) => !e.endsWith("/")));
1731
+ const root = ctx.contentRoot;
1732
+ return (absolute) => {
1733
+ const rel = path.relative(root, absolute).replaceAll("\\", "/");
1734
+ if (!rel || rel.startsWith("..")) return false;
1735
+ return files.has(rel) || dirs.some((d) => rel === d.slice(0, -1) || rel.startsWith(d));
1736
+ };
1737
+ }
1738
+
1055
1739
  // What rustfmt needs from a Cargo.toml, read by table so an `edition` under [package.metadata.x] or [dependencies.x] is not the
1056
1740
  // package's: `pkg` is the [package] edition (a literal year, or "workspace" for `edition.workspace = true` / `edition = { workspace = true }`),
1057
1741
  // `ws` the [workspace.package] edition literal, `isWorkspace` whether the manifest declares a [workspace]. An `edition` entry in one of
@@ -1196,11 +1880,15 @@ const INTERNAL = {
1196
1880
  // Each violation names its repository-relative path, so a unit that is both a crate and a package reports a file once.
1197
1881
  "secret-scan": async (s, ctx) => {
1198
1882
  const scan = await ctx.deps.scanSecrets();
1883
+ // A committed export holds only tracked files. A live tree (--to dev|test: the content root is the checkout itself) also holds
1884
+ // git-ignored build output (.right-release/, DerivedData, a vendored checkout under it), which the gate never judges: the scanner
1885
+ // skips what `git ls-files --ignored` names there. Outside a git toplevel nothing is skipped (Pulse, 2026-10-27).
1886
+ const skip = await ignoredPathSkip(ctx);
1199
1887
  const bad = new Set();
1200
1888
  for (const dir of s.dirs) {
1201
1889
  const abs = path.join(ctx.contentRoot, dir);
1202
1890
  if (!ctx.deps.exists(abs)) { bad.add(`${dir}: directory not found`); continue; }
1203
- for (const v of scan(abs, { envTemplates: true, repoRoot: ctx.contentRoot })) bad.add(v);
1891
+ for (const v of scan(abs, { envTemplates: true, repoRoot: ctx.contentRoot, skip })) bad.add(v);
1204
1892
  }
1205
1893
  return [...bad];
1206
1894
  },
@@ -1225,14 +1913,18 @@ export async function runScans(plan, options = {}) {
1225
1913
  const exclude = options.exclude ?? (() => false);
1226
1914
  // An AbortSignal: once aborted, no further scan starts, running children are killed (process tree) and every unfinished scan is "not-run" with code CANCELLED.
1227
1915
  const signal = options.signal; const sig = signal ? { signal } : {};
1228
- const { contentRoot, liveRoot } = plan.roots;
1229
- const rootDir = (r) => (r === "content" ? contentRoot : r === "parent" ? path.dirname(contentRoot) : liveRoot);
1916
+ const { contentRoot, liveRoot, cargoLiveRoot = null } = plan.roots;
1917
+ const rootDir = (r) => (r === "content" ? contentRoot : r === "parent" ? path.dirname(contentRoot) : r === "cargoLive" ? (cargoLiveRoot ?? liveRoot) : liveRoot);
1230
1918
  const selected = plan.scans.filter((s) => classes.includes(s.class) && !exclude(s));
1231
1919
  const ordered = [...selected.filter((s) => s.class === SCAN_CLASS.A), ...selected.filter((s) => s.class === SCAN_CLASS.B)];
1232
1920
  const ctx = { deps, contentRoot, read: (f) => deps.readFile(path.join(contentRoot, f)) };
1233
1921
  const toolVersions = {}; const probed = new Map(); const advisoryDbs = new Map();
1234
1922
  const results = [];
1235
- // options.cache = { read(key), write(key, row), digest(scan) => string | null } (null: not cacheable, which is how the live class A checks opt out).
1923
+ // The gate-owned cargo ledger of a wrapped snapshot compile (options.cargoLedger = { dir }): every call the wrapper decided, filed
1924
+ // under its scan's token. `ledgerSeen` is the run's one-use record of carriers and request ids across every scan.
1925
+ const cargoLedger = options.cargoLedger?.dir ? options.cargoLedger : null;
1926
+ const claimedTokens = new Set(); const ledgerSeen = { nonces: new Set(), ids: new Set() };
1927
+ // options.cache ={ read(key), write(key, row), digest(scan) => string | null } (null: not cacheable, which is how the live class A checks opt out).
1236
1928
  const cache = options.cache && typeof options.cache.read === "function" && typeof options.cache.write === "function" && typeof options.cache.digest === "function" ? options.cache : null;
1237
1929
  const cacheHits = [];
1238
1930
  let classAFailed = false;
@@ -1256,7 +1948,7 @@ export async function runScans(plan, options = {}) {
1256
1948
  const classBBlocked = !inCallA && !priorClassAValid;
1257
1949
 
1258
1950
  const toolCommand = (tool) => (tool === "node" ? deps.execPath : tool === "rustfmt" ? deps.findRustfmt({ env: deps.env, exists: deps.exists, readdir: deps.readdir, platform: deps.platform, spawn: deps.lookupSpawn }) : tool);
1259
- async function probe(tool) {
1951
+ async function probe(tool, scanDir = null) {
1260
1952
  if (probed.has(tool)) return probed.get(tool);
1261
1953
  let outcome;
1262
1954
  const command = toolCommand(tool);
@@ -1264,7 +1956,19 @@ export async function runScans(plan, options = {}) {
1264
1956
  else {
1265
1957
  // A version probe never runs inside the repository: a tool started there reads the repository's own configuration first
1266
1958
  // (pnpm's .npmrc and pnpmfile, corepack's packageManager pin, cargo's .cargo/config), before any scan's protections apply.
1267
- const r = await deps.spawn(command, PROBES[tool] ?? ["--version"], { cwd: deps.probeDir ?? probeDir(), env: { ...deps.env, ...PNPM_NO_HOOKS_ENV, COREPACK_ENABLE_AUTO_PIN: "0" }, ...sig });
1959
+ // rustright is the one exception: the RustRight client refuses to run anywhere that is not a Cargo workspace inside a repository
1960
+ // (RK_NOT_PRIMARY_CHECKOUT, "workspace discovery failed"), so from a neutral directory every cargo scan read as TOOL_MISSING on
1961
+ // every broker host (rightkit#1215). Its probe runs where the scan itself will run; `cargo --version` reads no repository config.
1962
+ const cwd = (tool === "rustright" || (tool === "cargo" && cargoLiveRoot)) && scanDir ? scanDir : (deps.probeDir ?? probeDir());
1963
+ // A version probe is bounded (PROBE_TIMEOUT_MS): a hung tool reads as unavailable instead of holding a lane, or a quiet
1964
+ // lease, open.
1965
+ const probeMs = Number.isFinite(options.probeTimeoutMs) && options.probeTimeoutMs > 0 ? options.probeTimeoutMs : PROBE_TIMEOUT_MS;
1966
+ const probeSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(probeMs)]) : AbortSignal.timeout(probeMs);
1967
+ // The gate's own version probes reach the wrapper too (cargo is first on PATH): they are filed under the probe token, which the
1968
+ // gate claims and checks like a scan's.
1969
+ const probeLedgerEnv = cargoLedger ? { [SCAN_TOKEN_ENV]: PROBE_LEDGER_TOKEN } : {};
1970
+ if (cargoLedger) claimedTokens.add(PROBE_LEDGER_TOKEN);
1971
+ const r = await deps.spawn(command, PROBES[tool] ?? ["--version"], { cwd, env: { ...deps.env, ...PNPM_NO_HOOKS_ENV, COREPACK_ENABLE_AUTO_PIN: "0", ...probeLedgerEnv }, signal: probeSignal });
1268
1972
  if (!r || r.error || r.status !== 0) outcome = { ok: false, why: r?.error?.message ?? `exit ${r?.status}` };
1269
1973
  else {
1270
1974
  const text = String(r.stdout || r.stderr || "");
@@ -1284,7 +1988,14 @@ export async function runScans(plan, options = {}) {
1284
1988
  const quietLane = options.quietLane && typeof options.quietLane.acquire === "function" ? options.quietLane : null;
1285
1989
  const onResult = typeof options.onResult === "function" ? options.onResult : () => {};
1286
1990
  const total = ordered.length; let done = 0;
1991
+ // Every class A failure latches, whichever branch produced it (Astra, 2026-10-26: two early returns bypassed the latch below, so a
1992
+ // missing advisory database or a failed deny metadata could be followed by a class B compile). Class B reads the latch before it starts.
1287
1993
  async function runOne(s, extraEnv = null) {
1994
+ const row = await runOneBody(s, extraEnv);
1995
+ if (row.status === "fail" && s.class === SCAN_CLASS.A) classAFailed = true;
1996
+ return row;
1997
+ }
1998
+ async function runOneBody(s, extraEnv = null) {
1288
1999
  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" };
1289
2000
  const started = Date.now();
1290
2001
  const row = await scanRow(s, extraEnv);
@@ -1292,33 +2003,93 @@ export async function runScans(plan, options = {}) {
1292
2003
  try { onResult(row, { done, total, seconds: Math.round((Date.now() - started) / 1000) }); } catch { /* reporting never fails a scan */ }
1293
2004
  return row;
1294
2005
  }
1295
- // Class A in order (a failure stops class B); then class B by lane: compile lanes (`jobs`) beside the doc lane, then the test
1296
- // lane (`testJobs`), then the quiet lane one at a time under the broker's quiet lease, so the tests that fail under load run on
1297
- // an otherwise quiet machine, as CI's shards run tests after their build. With jobs = 1 and testJobs = 1 the order is the same.
2006
+ // Class A may run concurrently. Class B runs one scan per component at a time, `classBJobs` components at once (below).
1298
2007
  const aScans = ordered.filter((s) => s.class === SCAN_CLASS.A);
1299
2008
  const bScans = ordered.filter((s) => s.class === SCAN_CLASS.B);
1300
2009
  // Class A scans are independent content checks (and short broker calls: metadata, package list, deny, audit): they run
1301
2010
  // `classAJobs` at a time (default: `jobs`); the rows keep plan order. Class B starts only once every class A row is in.
1302
2011
  const aJobs = Math.max(1, Number(options.classAJobs ?? jobs) || jobs);
1303
2012
  {
1304
- const slots = new Array(aScans.length); const queue = aScans.map((_, i) => i);
1305
- const drain = async () => { while (queue.length) { const i = queue.shift(); slots[i] = await runOne(aScans[i]); } };
1306
- await Promise.all(Array.from({ length: aJobs }, drain));
2013
+ // Class A is everything mechanical and deterministic that does not compile (Adrian, 2026-10-27): the content checks first,
2014
+ // `classAJobs` at a time, then the class A tests (kind "test"), `testJobs` at a time, then the quiet ones alone under the
2015
+ // broker's quiet lease. A failed content check stops the tests: they run on content class A already judged.
2016
+ const slots = new Array(aScans.length);
2017
+ const content = []; const tests = []; const quietTests = [];
2018
+ aScans.forEach((s, i) => (s.kind === "test" ? (s.quiet === true ? quietTests : tests) : content).push(i));
2019
+ const drain = (queue, extraEnv = null) => async () => {
2020
+ while (queue.length) {
2021
+ const i = queue.shift();
2022
+ slots[i] = aScans[i].kind === "test" && classAFailed
2023
+ ? { id: aScans[i].id, class: aScans[i].class, command: aScans[i].command, tool: aScans[i].tool ?? null, toolVersion: null, status: "not-run", detail: "a class A content check failed; the class A tests start only after it passes" }
2024
+ : await runOne(aScans[i], extraEnv);
2025
+ }
2026
+ };
2027
+ await Promise.all(Array.from({ length: aJobs }, drain(content)));
2028
+ await Promise.all(Array.from({ length: testJobs }, drain(tests)));
2029
+ if (quietTests.length) {
2030
+ // One broker quiet lease per quiet suite (the broker caps a lease at 30 min and has no renewal): each suite runs alone,
2031
+ // under its own lease, with a timeout shorter than the lease, so a lease never expires while its suite still runs.
2032
+ for (const i of quietTests) {
2033
+ if (classAFailed || signal?.aborted) {
2034
+ slots[i] = { id: aScans[i].id, class: aScans[i].class, command: aScans[i].command, tool: aScans[i].tool ?? null, toolVersion: null, status: "not-run", detail: signal?.aborted ? "cancelled" : "a class A check failed; the quiet suites start only on a green class A" };
2035
+ continue;
2036
+ }
2037
+ let lease = null;
2038
+ if (quietLane) { try { lease = (await quietLane.acquire({ ttlMs: QUIET_LEASE_MAX_MS })) ?? null; } catch { lease = null; } }
2039
+ if (!lease && options.requireQuietLease) {
2040
+ slots[i] = { id: aScans[i].id, class: aScans[i].class, command: aScans[i].command, tool: aScans[i].tool ?? null, toolVersion: null, status: "fail", code: "QUIET_LEASE_UNAVAILABLE", detail: "the broker's quiet lease could not be held; this conflict-prone suite does not run alongside other jobs on a managed host. Run again when the broker can grant the lease." };
2041
+ classAFailed = true; // a refused class A suite is a red class A: no class B scan starts after it
2042
+ continue;
2043
+ }
2044
+ try { slots[i] = await runOne({ ...aScans[i], timeoutMs: Math.min(scanTimeoutMs(aScans[i], options), QUIET_SCAN_MAX_MS) }, lease?.env ?? null); } finally {
2045
+ if (lease && typeof quietLane.release === "function") { try { await quietLane.release(lease.id); } catch { /* the lease expires on its own */ } }
2046
+ }
2047
+ }
2048
+ }
1307
2049
  results.push(...slots);
1308
2050
  }
1309
2051
  {
1310
2052
  const slots = new Array(bScans.length);
1311
2053
  const lanes = { compile: [], doc: [], tests: [], quiet: [] };
1312
2054
  bScans.forEach((s, i) => lanes[laneOf(s)].push(i));
1313
- const drain = async (queue, extraEnv = null) => { while (queue.length) { const i = queue.shift(); slots[i] = await runOne(bScans[i], extraEnv); } };
1314
- await Promise.all([...Array.from({ length: jobs }, () => drain(lanes.compile)), drain(lanes.doc)]);
1315
- await Promise.all(Array.from({ length: testJobs }, () => drain(lanes.tests)));
2055
+ // RightKit is many components, never one build (Adrian, 2026-10-29; rightkit#1285): class B scans of different components run
2056
+ // concurrently, `classBJobs` at a time (the broker queues beyond its build capacity). A component is the directory a scan runs in
2057
+ // (`cwd`, its own Cargo workspace and output directory); scans of one component stay in plan order, because they share its
2058
+ // output directory and cargo locks it.
2059
+ const bJobs = Math.max(1, Number(options.classBJobs ?? 1) || 1);
2060
+ const drain = async (queue, extraEnv = null) => {
2061
+ const busy = new Set(); const running = new Set();
2062
+ const keyOf = (i) => bScans[i].cwd ?? ".";
2063
+ while (queue.length || running.size) {
2064
+ const at = running.size < bJobs ? queue.findIndex((i) => !busy.has(keyOf(i))) : -1;
2065
+ if (at === -1) { await Promise.race(running); continue; }
2066
+ const [i] = queue.splice(at, 1); const key = keyOf(i); busy.add(key);
2067
+ // A scan that throws becomes a failed row: with several components in flight, a rejection must not abandon the others.
2068
+ const job = runOne(bScans[i], extraEnv)
2069
+ .catch((error) => ({ id: bScans[i].id, class: bScans[i].class, command: bScans[i].command, tool: bScans[i].tool ?? null, toolVersion: null, status: "fail", code: "SCAN_ERROR", detail: String(error?.message ?? error) }))
2070
+ .then((row) => { slots[i] = row; }).finally(() => { busy.delete(key); running.delete(job); });
2071
+ running.add(job);
2072
+ }
2073
+ };
2074
+ await drain(lanes.compile);
2075
+ await drain(lanes.doc);
2076
+ await drain(lanes.tests);
1316
2077
  if (lanes.quiet.length) {
1317
- let lease = null;
1318
- // No lease when the lane cannot run anyway (class A failed or was not supplied): its rows are not-run without one.
1319
- if (quietLane && !signal?.aborted && !classAFailed && !classBBlocked) { try { lease = (await quietLane.acquire()) ?? null; } catch { lease = null; } }
1320
- try { await drain(lanes.quiet, lease?.env ?? null); } finally {
1321
- if (lease && typeof quietLane.release === "function") { try { await quietLane.release(lease.id); } catch { /* the lease expires on its own */ } }
2078
+ // One lease per quiet suite, as in class A above.
2079
+ for (const i of lanes.quiet) {
2080
+ if (classAFailed || classBBlocked || signal?.aborted) {
2081
+ slots[i] = { id: bScans[i].id, class: bScans[i].class, command: bScans[i].command, tool: bScans[i].tool ?? null, toolVersion: null, status: "not-run", detail: signal?.aborted ? "cancelled" : classAFailed ? "class A is red; nothing in class B starts" : "class B is blocked" };
2082
+ continue;
2083
+ }
2084
+ let lease = null;
2085
+ if (quietLane) { try { lease = (await quietLane.acquire({ ttlMs: QUIET_LEASE_MAX_MS })) ?? null; } catch { lease = null; } }
2086
+ if (!lease && options.requireQuietLease) {
2087
+ slots[i] = { id: bScans[i].id, class: bScans[i].class, command: bScans[i].command, tool: bScans[i].tool ?? null, toolVersion: null, status: "fail", code: "QUIET_LEASE_UNAVAILABLE", detail: "the broker's quiet lease could not be held; this conflict-prone suite does not run alongside other jobs on a managed host. Run again when the broker can grant the lease." };
2088
+ continue;
2089
+ }
2090
+ try { slots[i] = await runOne({ ...bScans[i], timeoutMs: Math.min(scanTimeoutMs(bScans[i], options), QUIET_SCAN_MAX_MS) }, lease?.env ?? null); } finally {
2091
+ if (lease && typeof quietLane.release === "function") { try { await quietLane.release(lease.id); } catch { /* the lease expires on its own */ } }
2092
+ }
1322
2093
  }
1323
2094
  }
1324
2095
  results.push(...slots);
@@ -1368,11 +2139,12 @@ export async function runScans(plan, options = {}) {
1368
2139
  if (out && out.missing) { status = "fail"; code = "TOOL_MISSING"; detail = out.missing; }
1369
2140
  else if (out.length) { status = "fail"; detail = out.join("; "); }
1370
2141
  } else {
1371
- const p = await probe(s.tool);
2142
+ const scanDir = path.join(rootDir(s.root), s.cwd ?? "");
2143
+ const p = await probe(s.tool, scanDir);
1372
2144
  row.toolVersion = p.ok ? p.version : null;
1373
2145
  let missingDep = null; let depVersionProblem = null; const depVersions = [];
1374
2146
  for (const dep of s.requires ?? []) {
1375
- const d = await probe(dep);
2147
+ const d = await probe(dep, scanDir);
1376
2148
  if (!d.ok) { missingDep = `${dep} (${d.why})`; break; }
1377
2149
  if (d.pinProblem) { depVersionProblem = d.pinProblem; break; }
1378
2150
  depVersions.push(d.version);
@@ -1383,7 +2155,10 @@ export async function runScans(plan, options = {}) {
1383
2155
  else if (depVersionProblem) { status = "fail"; code = "TOOL_VERSION"; detail = `TOOL_VERSION: ${s.tool} runs ${depVersionProblem}`; }
1384
2156
  else {
1385
2157
  let cacheKey = null;
1386
- if (cache) {
2158
+ // A cargo request that builds the commit snapshot through the broker (options.snapshotBroker) is never served from or written
2159
+ // to the scan cache: each run must carry its own request and verify the broker's own signed evidence.
2160
+ const viaSnapshotBroker = Boolean(deps.snapshotBroker) && s.root === "cargoLive" && (s.tool === "rustright" || s.tool === "cargo");
2161
+ if (cache && !viaSnapshotBroker) {
1387
2162
  try {
1388
2163
  const digest = cache.digest(s);
1389
2164
  // The analyzers a scan runs through (actionlint's shellcheck) judge it too, so their versions key it as well.
@@ -1391,25 +2166,44 @@ export async function runScans(plan, options = {}) {
1391
2166
  if (digest) cacheKey = scanCacheKey({ id: s.id, command: s.command, toolVersion, env: s.env ?? {}, inheritedEnv: inheritedEnvDigest({ ...deps.env, ...s.env }), digest });
1392
2167
  } catch { cacheKey = null; }
1393
2168
  const hit = cacheKey ? cache.read(cacheKey) : null;
1394
- if (hit && hit.key === cacheKey && hit.row?.status === "pass" && hit.row.id === s.id && hit.row.command === s.command) {
2169
+ // A cached test pass counts only with the counts it recorded: without them its coverage is unknown, so it runs again.
2170
+ // ...and only if it passed at least one test: a cached all-skipped "pass" (written before D17) runs again (rightkit#1236).
2171
+ const testEvidence = s.kind !== "test" || testResultsProblem(hit?.row?.results) === null;
2172
+ if (hit && hit.key === cacheKey && hit.row?.status === "pass" && hit.row.id === s.id && hit.row.command === s.command && testEvidence) {
1395
2173
  cacheHits.push(s.id);
1396
- return { ...row, status: "pass", cached: true, detail: `cached ${String(hit.at ?? "").slice(0, 10) || "earlier"}` };
2174
+ return { ...row, status: "pass", cached: true, detail: `cached ${String(hit.at ?? "").slice(0, 10) || "earlier"}`, ...(s.kind === "test" ? { results: hit.row.results } : {}) };
1397
2175
  }
1398
2176
  }
1399
2177
  let advisory = null; let args = s.args;
1400
2178
  if (s.advisoryDb) {
1401
2179
  try { advisory = prepareAdvisory(s); } catch (e) { return { ...row, status: "fail", code: "ADVISORY_DB", detail: e.message }; }
1402
- // cargo-deny --offline must not fetch the advisory database, but its own `cargo metadata` would then be offline too and
2180
+ // cargo-deny --frozen (--offline + --locked: the nested `cargo fetch` it runs may not rewrite Cargo.lock, so the broker admits it as analysis) must not fetch the advisory database, but its own `cargo metadata` would then be offline too and
1403
2181
  // fail on a runner whose cargo cache is cold. The workspace's metadata is resolved first, online and --locked (the
1404
2182
  // lockfile decides every version), and handed to cargo-deny as a file.
1405
2183
  if (s.advisoryDb === "deny") {
1406
2184
  const at = args.indexOf("deny");
1407
2185
  // The whole document is needed (the concurrent spawn keeps only the tail of stdout), so it streams to the file; a
1408
2186
  // spawn that does not know `stdoutFile` returns stdout, which is written below.
2187
+ // cargo-deny --frozen reads crate sources only from the local cargo cache, and `cargo metadata` does not download them
2188
+ // (fullA-19: rightkit-qa 0.9.1 was missing). They are fetched first, online and --locked (bound to the lockfile, so the
2189
+ // broker admits it as analysis). A fetch that fails, is refused or times out fails the row before metadata or deny run:
2190
+ // an offline or unreachable registry is red, never green.
2191
+ const fetchLimit = scanTimeoutMs(s, options);
2192
+ const fetchTimer = new AbortController();
2193
+ const fetchClock = setTimeout(() => fetchTimer.abort(), fetchLimit);
2194
+ let fetched;
2195
+ try { fetched = await deps.spawn(p.command, [...args.slice(0, at), "fetch", "--locked"], { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...s.env }, signal: signal ? AbortSignal.any([signal, fetchTimer.signal]) : fetchTimer.signal, timeout: fetchLimit }); } finally { clearTimeout(fetchClock); }
2196
+ if (!fetched || fetched.error || fetched.status !== 0) {
2197
+ advisory.cleanup?.();
2198
+ if (signal?.aborted) return { ...row, status: "not-run", code: "CANCELLED", detail: "cancelled while running" };
2199
+ if (fetchTimer.signal.aborted || fetched?.error?.code === "ETIMEDOUT") return { ...row, status: "fail", code: "TIMEOUT", detail: `cargo fetch --locked for cargo-deny ran longer than ${Math.round(fetchLimit / 60000)} min and was killed with its process tree` };
2200
+ return { ...row, status: "fail", code: "FETCH", detail: `cargo fetch --locked for cargo-deny failed (crate sources must be local before deny): ${fetched?.error?.message ?? (tail(fetched) || `exit ${fetched?.status}`)}` };
2201
+ }
1409
2202
  const metadataDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-deny-metadata-"));
1410
2203
  const metadataFile = path.join(metadataDir, "metadata.json");
1411
2204
  const metadata = await deps.spawn(p.command, [...args.slice(0, at), ...metadataArgs({ noDeps: false })], { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...s.env }, stdoutFile: metadataFile, ...sig });
1412
- if (!metadata || metadata.error || metadata.status !== 0) { fs.rmSync(metadataDir, { recursive: true, force: true }); advisory.cleanup?.(); return { ...row, status: "fail", code: "EXIT", detail: `cargo metadata for cargo-deny failed: ${metadata?.error?.message ?? (tail(metadata) || `exit ${metadata?.status}`)}` }; }
2205
+ // AUX_METADATA: the auxiliary metadata that feeds deny could not be produced; not a deny finding, so never a deterministic red (rightkit#1257).
2206
+ if (!metadata || metadata.error || metadata.status !== 0) { fs.rmSync(metadataDir, { recursive: true, force: true }); advisory.cleanup?.(); return { ...row, status: "fail", code: "AUX_METADATA", detail: `cargo metadata for cargo-deny failed: ${metadata?.error?.message ?? (tail(metadata) || `exit ${metadata?.status}`)}` }; }
1413
2207
  if (!fs.existsSync(metadataFile)) fs.writeFileSync(metadataFile, String(metadata.stdout ?? ""));
1414
2208
  advisory.metadataPath = metadataFile;
1415
2209
  const overlayCleanup = advisory.cleanup;
@@ -1418,11 +2212,128 @@ export async function runScans(plan, options = {}) {
1418
2212
  args = args.map((a) => (a === "{advisoryDb}" ? advisory.dbDir : a === "{advisoryDenyConfig}" ? advisory.configPath : a === "{advisoryMetadata}" ? advisory.metadataPath : a));
1419
2213
  }
1420
2214
  let r;
1421
- try { r = await deps.spawn(p.command, args.map((a) => (a === "{contentRoot}" ? contentRoot : a)), { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...s.env, ...(extraEnv ?? {}) }, ...sig }); } finally { advisory?.cleanup?.(); }
2215
+ // actionlint 1.7.12 writes each run: script to shellcheck's stdin before starting it (rhysd/actionlint#702): a script over the
2216
+ // pipe buffer (~4 KB on Windows) deadlocks. Oversized scripts are refused here, before actionlint starts, so it cannot hang.
2217
+ if (s.runScriptLimit) {
2218
+ const over = s.runScriptLimit.files.flatMap((file) => { try { return oversizedRunScripts(ctx.read(file), file, s.runScriptLimit.bytes); } catch { return [`${file}: unreadable`]; } });
2219
+ if (over.length) return { ...row, status: "fail", code: "RUN_SCRIPT_TOO_LARGE", detail: `run: scripts over ${s.runScriptLimit.bytes} bytes (actionlint 1.7.12 deadlocks on them, rhysd/actionlint#702; split the step): ${over.join("; ")}` };
2220
+ }
2221
+ // Every scan has a time limit (fails closed, names the scan, kills the process tree): a hung tool is a check that never
2222
+ // fails, i.e. a run that never ends (actionlint on the Dell, 2026-10-27).
2223
+ // A tsc row that names TSC_PROJECT checks its package against the committed sources of its workspace siblings: the project is
2224
+ // generated outside the checked tree and removed after the run, so the check reads nothing a commit lacks and writes nothing.
2225
+ let tscProject = null;
2226
+ if (args.includes(TSC_PROJECT)) {
2227
+ try { tscProject = writeTscProject({ contentRoot, packageDir: path.join(rootDir(s.root), s.cwd ?? ""), workspace: s.tscWorkspace ?? [] }); }
2228
+ catch (e) { advisory?.cleanup?.(); return { ...row, status: "fail", code: "TSC_PROJECT", detail: `${s.id}: the check project could not be built from the committed workspace: ${e.message}` }; }
2229
+ args = args.map((a) => (a === TSC_PROJECT ? tscProject.file : a));
2230
+ }
2231
+ const limitMs = scanTimeoutMs(s, options);
2232
+ const timer = new AbortController();
2233
+ const clock = setTimeout(() => timer.abort(), limitMs);
2234
+ const scanSignal = signal ? AbortSignal.any([signal, timer.signal]) : timer.signal;
2235
+ // A class A test row runs under the launch guard (class-a-launch-guard.mjs): its Node processes may not launch a compile.
2236
+ // Every class A row that executes repository code (tests, lint/typecheck with repository config, generators) runs under the
2237
+ // launch guard: class A runs before any compile, so none of them may start one (joint plan step 4; inventory gap 3).
2238
+ const guardEnv = s.class === SCAN_CLASS.A && (s.kind === "test" || s.executes === true) ? launchGuardEnv({ ...deps.env, ...s.env, ...(extraEnv ?? {}) }) : {};
2239
+ // The broker snapshot carrier (joint plan step 4d): this one cargo request names the commit, the snapshot, the ownership
2240
+ // handle and its own argv and working directory, under a fresh nonce; the ownership records the submit first.
2241
+ const finalArgs = args.map((a) => (a === "{contentRoot}" ? contentRoot : a));
2242
+ let carrier = null;
2243
+ if (viaSnapshotBroker) {
2244
+ const sb = deps.snapshotBroker;
2245
+ const cwd = path.join(rootDir(s.root), s.cwd ?? "");
2246
+ const refDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-snapreq-"));
2247
+ try {
2248
+ const cargoArgv = s.tool === "rustright" ? finalArgs.slice(1) : finalArgs;
2249
+ // The ownership must record the submit, under this gate's handle, before anything is launched: a missing hook or a
2250
+ // mark that did not land refuses the request (the reclaim proof reads broker.submitted).
2251
+ if (typeof sb.markBrokerSubmit !== "function") throw new Error("no build-ownership submit hook: the broker submit cannot be recorded");
2252
+ const marked = sb.markBrokerSubmit();
2253
+ if (marked?.broker?.submitted !== true || marked.handle !== sb.handle) throw new Error(`the build ownership did not record the broker submit under ${sb.handle}`);
2254
+ const made = writeSnapshotRequest({ controlRoot: sb.controlRoot, commit: sb.commit, snapshot: sb.snapshot, declaredInputs: sb.declaredInputs ?? [], handle: sb.handle, primaryCwd: fs.realpathSync.native(cwd), argv: cargoArgv, requestIdFile: path.join(refDir, "request.json") });
2255
+ carrier = { made, refDir, cargoArgv };
2256
+ } catch (e) { fs.rmSync(refDir, { recursive: true, force: true }); clearTimeout(clock); advisory?.cleanup?.(); return { ...row, status: "fail", code: "SNAPSHOT_REQUEST", detail: `the snapshot request for ${s.id} could not be written: ${e.message}` }; }
2257
+ }
2258
+ // From here to the end of its verification the carrier and the request reference are this request's: every exit (cancel,
2259
+ // timeout, a thrown spawn, a refused receipt) removes them.
2260
+ // A wrapped snapshot compile with several Cargo workspaces (options.cargoSentinel, item 7): a scan whose own env binds no
2261
+ // CARGO_TARGET_DIR gets an empty directory of its own as one. Every cargo reached through the wrapper is bound elsewhere; only
2262
+ // a cargo that bypassed it (an absolute path, `rustup run`, a reset PATH) can write here, and the scan then fails below.
2263
+ let sentinel = null;
2264
+ // (A scan that is itself the carried broker request is bound by the broker: no sentinel, so the client's observed environment is the scan's own.)
2265
+ if (options.cargoSentinel && !viaSnapshotBroker && !Object.hasOwn(s.env ?? {}, "CARGO_TARGET_DIR")) {
2266
+ sentinel = path.join(options.cargoSentinel, sha256(s.id).slice(0, 16));
2267
+ fs.rmSync(sentinel, { recursive: true, force: true });
2268
+ fs.mkdirSync(sentinel, { recursive: true });
2269
+ }
2270
+ const sentinelEnv = sentinel ? { CARGO_TARGET_DIR: sentinel, CARGO_BUILD_TARGET_DIR: sentinel } : {};
2271
+ // Every cargo call the wrapper decides during this scan is filed under this token in the gate's ledger.
2272
+ const ledgerToken = cargoLedger ? scanTokenOf(s.id) : null;
2273
+ if (ledgerToken) claimedTokens.add(ledgerToken);
2274
+ const ledgerEnv = ledgerToken ? { [SCAN_TOKEN_ENV]: ledgerToken } : {};
2275
+ try {
2276
+ try { r = await deps.spawn(p.command, finalArgs, { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...sentinelEnv, ...s.env, ...(extraEnv ?? {}), ...guardEnv, ...ledgerEnv, ...(carrier ? carrier.made.env : {}) }, signal: scanSignal, timeout: limitMs }); } finally { clearTimeout(clock); advisory?.cleanup?.(); }
1422
2277
  if (signal?.aborted && (!r || r.error || r.status !== 0)) return { ...row, status: "not-run", code: "CANCELLED", detail: "cancelled while running" };
2278
+ if ((timer.signal.aborted || r?.error?.code === "ETIMEDOUT") && (!r || r.error || r.status !== 0)) return { ...row, status: "fail", code: "TIMEOUT", detail: `${s.id} ran longer than ${Math.round(limitMs / 60000)} min and was killed with its process tree; a scan that does not finish fails` };
1423
2279
  if (!r || r.error) { status = "fail"; code = "SPAWN_ERROR"; detail = r?.error?.message ?? "no result"; }
1424
2280
  else if (r.status !== 0) { status = "fail"; code = "EXIT"; detail = tail(r) || `exit ${r.status}`; }
1425
- else if (cacheKey && !signal?.aborted) cache.write(cacheKey, { id: s.id, class: s.class, command: s.command, tool: s.tool ?? null, toolVersion: row.toolVersion, status: "pass" });
2281
+ // cargo-deny reports some failures only as an [ERROR] line and still exits 0 (under the broker, its nested offline
2282
+ // `cargo fetch` of crate sources is refused: "failed to fetch crates"). A deny row passes only with no [ERROR] line in
2283
+ // its complete output; output cut to the kept tail cannot prove that, so it fails too (RR, reviewer 2026-10-28).
2284
+ if (sentinel) {
2285
+ let written = [];
2286
+ try { written = fs.readdirSync(sentinel); } catch (e) { written = [`(unreadable: ${e.message})`]; }
2287
+ if (written.length) { status = "fail"; code = "RK_CARGO_UNBOUND"; detail = `${s.id}: a cargo that bypassed the gate's wrapper wrote ${written.slice(0, 5).join(", ")} into the unbound sentinel ${sentinel}; with several Cargo workspaces no output directory can be inferred for it, so the scan proves nothing about where its outputs went`; }
2288
+ }
2289
+ // The wrapper's ledger (rows 7.10 and 7.24): a refused, unverified or unfinished cargo call fails this scan even when the code
2290
+ // under test ignored cargo's exit status; on a broker host each call's signed receipt is verified again here, by the gate.
2291
+ if (ledgerToken) {
2292
+ let problems;
2293
+ try {
2294
+ const ledger = readLedger(cargoLedger.dir, ledgerToken);
2295
+ problems = [...ledger.problems, ...(deps.snapshotBroker ? await verifyBrokerLedger(ledger.entries, deps.snapshotBroker, { seen: ledgerSeen }) : [])];
2296
+ } catch (e) { problems = [`the cargo ledger cannot be read (${e.message})`]; }
2297
+ if (problems.length) {
2298
+ const text = `${s.id}: the gate's cargo ledger records ${problems.length} cargo call(s) that did not complete as a verified success: ${problems.slice(0, 5).join("; ")}`;
2299
+ if (status === "pass") { status = "fail"; code = "RK_CARGO_LEDGER"; detail = text; } else detail = `${detail ? `${detail}; ` : ""}${text}`;
2300
+ }
2301
+ }
2302
+ if (s.advisoryDb === "deny" && code !== "RK_CARGO_UNBOUND") {
2303
+ const denyError = `${r.stdout ?? ""}\n${r.stderr ?? ""}`.split(/\r?\n/).find((line) => /\[ERROR\]/.test(line));
2304
+ if (r.truncated) { status = "fail"; code = "DENY_ERROR"; detail = "cargo-deny output exceeded the kept tail, so the absence of an [ERROR] line cannot be shown"; }
2305
+ else if (denyError) { status = "fail"; code = "DENY_ERROR"; detail = `cargo-deny exited 0 but reported an error: ${denyError.trim().slice(0, 300)}`; }
2306
+ }
2307
+ // The broker's signed account of that request: the receipt verifies against this host's pins and its snapshotEvidence names
2308
+ // this commit, this snapshot unchanged from START to exit, this ownership and the output directory. Anything less fails the row.
2309
+ if (carrier) {
2310
+ try {
2311
+ const ref = readRequestRef(path.join(carrier.refDir, "request.json"));
2312
+ const receipt = await deps.snapshotBroker.receiptOf(ref.requestId);
2313
+ 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 });
2314
+ row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
2315
+ } catch (e) {
2316
+ status = "fail"; code = "SNAPSHOT_EVIDENCE"; detail = `${s.id}: the broker's snapshot evidence did not verify (${e.code ?? "error"}: ${e.message})`;
2317
+ }
2318
+ }
2319
+ } finally {
2320
+ if (carrier) { fs.rmSync(carrier.made.file, { force: true }); fs.rmSync(carrier.refDir, { recursive: true, force: true }); }
2321
+ if (tscProject) fs.rmSync(tscProject.dir, { recursive: true, force: true });
2322
+ }
2323
+ if (status === "fail" && options.scanLogDir && r) {
2324
+ 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 */ }
2325
+ }
2326
+ if (s.kind === "test" && r && !r.error && r.status === 0) {
2327
+ // What the suite executed and skipped is evidence, not decoration: a later stage reuses this row only with these counts.
2328
+ const counts = parseTestSummary(`${r.stdout ?? ""}\n${r.stderr ?? ""}`);
2329
+ if (counts) row.results = counts;
2330
+ else if (status === "pass") { status = "fail"; code = "NO_TEST_SUMMARY"; detail = "the test scan passed but printed no recognisable summary (node:test or vitest); its coverage cannot be recorded"; }
2331
+ else row.results = null;
2332
+ // One rule for fresh and cached rows (testResultsProblem): tests ran, none failed, at least one passed.
2333
+ const problem = status === "pass" && row.results ? testResultsProblem(row.results) : null;
2334
+ if (problem) { status = "fail"; code = problem.code; detail = problem.detail; }
2335
+ }
2336
+ if (status === "pass" && cacheKey && !signal?.aborted) cache.write(cacheKey, { id: s.id, class: s.class, command: s.command, tool: s.tool ?? null, toolVersion: row.toolVersion, status: "pass", ...(row.results ? { results: row.results } : {}) });
1426
2337
  }
1427
2338
  }
1428
2339
  } catch (e) { status = "fail"; code = "SCAN_ERROR"; detail = e.message; }
@@ -1430,12 +2341,27 @@ export async function runScans(plan, options = {}) {
1430
2341
  return { ...row, status, ...(code ? { code } : {}), ...(detail ? { detail } : {}) };
1431
2342
  }
1432
2343
 
2344
+ // Cargo calls the gate cannot attribute to a scan (the code under test cleared or replaced the scan token): never skipped.
2345
+ if (cargoLedger) {
2346
+ // The gate's own version probes: anything but a completed call (a refused probe) fails the run too.
2347
+ try {
2348
+ const probeProblems = readLedger(cargoLedger.dir, PROBE_LEDGER_TOKEN).problems;
2349
+ if (probeProblems.length) results.push({ id: "cargo-ledger:tool-probe", class: SCAN_CLASS.B, command: "cargo ledger", tool: null, toolVersion: null, status: "fail", code: "RK_CARGO_LEDGER", detail: `the gate's tool version probes reached the wrapper and did not complete: ${probeProblems.slice(0, 5).join("; ")}` });
2350
+ } catch { /* an unreadable probe ledger is named by the unclaimed check only when it holds records */ }
2351
+ let unclaimed;
2352
+ try { unclaimed = unclaimedLedgers(cargoLedger.dir, claimedTokens); } catch (e) { unclaimed = [`unreadable (${e.message})`]; }
2353
+ if (unclaimed.length) {
2354
+ const calls = [];
2355
+ for (const token of unclaimed) { try { for (const e of readLedger(cargoLedger.dir, token).entries) calls.push(`cargo ${(e.argv ?? []).join(" ")} (${e.status}, from ${e.cwd})`.trim()); } catch { /* named by its token below */ } }
2356
+ results.push({ id: "cargo-ledger", class: SCAN_CLASS.B, command: "cargo ledger", tool: null, toolVersion: null, status: "fail", code: "RK_CARGO_LEDGER", detail: `cargo calls were filed under ${unclaimed.length} scan token(s) no scan owns (${unclaimed.slice(0, 3).join(", ")}), so they cannot be tied to a scan (${SCAN_TOKEN_ENV} was cleared, or the call came from outside every scan): ${calls.slice(0, 5).join("; ") || "unreadable"}` });
2357
+ }
2358
+ }
1433
2359
  const cancelled = signal?.aborted === true;
1434
2360
  return {
1435
2361
  schema: SCAN_RESULT_SCHEMA, ok: !cancelled && results.every((r) => r.status === "pass"), complete: !cancelled && results.every((r) => r.status === "pass"), classes, checker, ...(cancelled ? { cancelled: true } : {}),
1436
2362
  inputDigest: plan.inputDigest ?? null,
1437
2363
  deferred: results.filter((r) => r.status === "deferred").map((r) => r.id),
1438
- scans: results, toolVersions, cacheHits: [...cacheHits].sort(), notes: plan.notes ?? [],
2364
+ scans: results, toolVersions, cacheHits: [...cacheHits].sort(), notes: [...(plan.notes ?? []), ...(cargoLedger ? [UNOBSERVED_NESTED_CARGO_NOTE] : [])],
1439
2365
  scanSetDigest: digestScanSet(results),
1440
2366
  classDigests: { A: digestScanSet(results.filter((r) => r.class === "A")), B: digestScanSet(results.filter((r) => r.class === "B")) },
1441
2367
  };