@rightkit/release 0.6.24 → 0.6.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +11 -35
  2. package/android-dev.mjs +2 -1
  3. package/app-check.mjs +1 -2
  4. package/app-closure.mjs +3 -2
  5. package/assembled-app.mjs +11 -3
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +6 -0
  8. package/bin/rightkit-gate.mjs +83 -26
  9. package/build-helper-trace.mjs +264 -0
  10. package/build-helpers-command.mjs +184 -0
  11. package/build-launch.mjs +11 -75
  12. package/build-release.mjs +10 -11
  13. package/cargo-profile-contract.mjs +19 -29
  14. package/cargo-target-shim.mjs +31 -0
  15. package/cli/right-release.mjs +5 -7
  16. package/create-mac-updater.mjs +4 -0
  17. package/cross-targets.mjs +94 -0
  18. package/deterministic-scans.mjs +310 -20
  19. package/development.mjs +8 -8
  20. package/freshness-census.mjs +1 -1
  21. package/gate-stages.mjs +1 -1
  22. package/install-assembled-app.mjs +1 -1
  23. package/install-tauri-authenticode.mjs +3 -3
  24. package/local-publish.mjs +27 -5
  25. package/mac-package-recipe.mjs +55 -2
  26. package/managed-roots.mjs +12 -4
  27. package/package.json +1 -5
  28. package/pipeline-admission.mjs +1 -1
  29. package/pipeline-receipts.mjs +302 -302
  30. package/provenance-verify.mjs +3 -1
  31. package/qa-build-features.mjs +11 -2
  32. package/release-qa-audit.mjs +9 -27
  33. package/release-qa-command.mjs +23 -64
  34. package/release-qa-fixtures.mjs +9 -5
  35. package/release-qa-floor.json +0 -1
  36. package/release-qa-policy.mjs +9 -10
  37. package/release-qa-receipt.mjs +6 -44
  38. package/release-source-identity.mjs +1 -12
  39. package/release-switch-registry.json +4 -1
  40. package/rightkit-registry-resolver.mjs +35 -3
  41. package/rightkit-versions.json +2 -2
  42. package/sign-windows.mjs +1 -1
  43. package/snapshot-broker.mjs +33 -8
  44. package/snapshot-metadata.mjs +32 -0
  45. package/snapshot-request.mjs +6 -3
  46. package/stage-sidecar.mjs +28 -11
  47. package/suite-signing-policy.mjs +8 -58
  48. package/templates/cargo-profiles.toml +6 -25
  49. package/test-command.mjs +40 -140
  50. package/updater-shipped-key.mjs +26 -0
  51. package/web-build.mjs +29 -4
  52. package/qa-journeys-prebuilt-run.mjs +0 -18
  53. package/qa-journeys-prebuilt.mjs +0 -148
  54. package/qa-layout.mjs +0 -229
  55. package/qa-snapshot-run.mjs +0 -53
  56. package/qa-snapshot.mjs +0 -164
  57. package/sign-qa-bundle.mjs +0 -210
  58. package/suite-signing.mjs +0 -259
@@ -320,6 +320,9 @@ export async function resolveRightKitPins({
320
320
  const observedAt = new Date(now()).toISOString();
321
321
  const packuments = new Map();
322
322
  const packument = (name) => { if (!packuments.has(name)) packuments.set(name, client.getJson(npmUrl(name), { accept: "application/vnd.npm.install-v1+json" })); return packuments.get(name); };
323
+ // The freshness bar needs publish dates: the abbreviated packument has no `time`, so the full one is read for the settle.
324
+ const fullPackuments = new Map();
325
+ const fullPackument = (name) => { if (!fullPackuments.has(name)) fullPackuments.set(name, client.getJson(npmUrl(name), { accept: "application/json" })); return fullPackuments.get(name); };
323
326
  const indexes = new Map();
324
327
  const index = (name) => { if (!indexes.has(name)) indexes.set(name, client.getJson(cargoIndexUrl(name), { text: true })); return indexes.get(name); };
325
328
 
@@ -379,17 +382,24 @@ export async function resolveRightKitPins({
379
382
  };
380
383
  await pool(names, async (entry) => {
381
384
  try {
382
- let value;
385
+ let value; let body;
383
386
  if (entry.ecosystem === "npm") {
384
- value = (await packument(entry.name)).body?.["dist-tags"]?.latest;
387
+ body = (await fullPackument(entry.name)).body;
388
+ value = body?.["dist-tags"]?.latest;
385
389
  } else {
386
390
  const response = await client.getJson(`${REGISTRIES.cargoApi}${entry.name}`);
391
+ body = response.body;
387
392
  value = response.body?.crate?.max_stable_version;
388
393
  }
389
394
  if (typeof value !== "string" || !EXACT_SEMVER.test(value)) throw new RightKitRegistryError("registry-outage", `${entry.name} registry answer carries no usable latest version`, { recoverable: true });
390
395
  const previous = cache.read("latest", entry.ecosystem, entry.name);
391
396
  const mark = previous?.highWater?.version ?? previous?.latest;
392
- const row = { ecosystem: entry.ecosystem, name: entry.name, latest: value, observedAt, via: "live" };
397
+ // The freshness bar is the settled latest (published at least SETTLE_MS ago), never above the live latest.
398
+ const settled = settledLatest(entry.ecosystem, body);
399
+ // Fail closed: a registry answer without publish dates cannot prove a version is settled (never fall back to the live latest).
400
+ if (settled === null) throw new RightKitRegistryError("registry-outage", `${entry.name} registry answer carries no publish dates, so the 24-hour settle cannot be judged`, { recoverable: true });
401
+ const bar = settled && EXACT_SEMVER.test(settled) && compareVersions(settled, value) < 0 ? settled : value;
402
+ const row = { ecosystem: entry.ecosystem, name: entry.name, latest: bar, ...(bar !== value ? { liveLatest: value } : {}), observedAt, via: "live" };
393
403
  let highWater = { version: value, observedAt };
394
404
  if (typeof mark === "string" && EXACT_SEMVER.test(mark) && compareVersions(mark, value) > 0) {
395
405
  if (entry.ecosystem === "npm" && await stillPublished(entry, mark)) {
@@ -586,6 +596,28 @@ export function verifiedVersionMaps(snapshot) {
586
596
  return maps;
587
597
  }
588
598
 
599
+ /**
600
+ * Adrian, 2026-11-02: a pin is held to the newest version that has been published for at least 24 hours (`SETTLE_MS`), so a RightKit publish
601
+ * during an app's run never turns its pins stale. The registry's live latest is still recorded (`liveLatest`). Versions are the registry's own:
602
+ * crates.io `versions[]` ({num, created_at, yanked}), npm `time` + `versions` (deprecated skipped). When no stable version is that old, the
603
+ * oldest stable version is the floor (nothing newer is required yet). Returns null when the registry answer carries no dates.
604
+ */
605
+ export const SETTLE_MS = 24 * 60 * 60 * 1000;
606
+ export function settledLatest(ecosystem, body, { now = Date.now(), settleMs = SETTLE_MS } = {}) {
607
+ let rows;
608
+ if (ecosystem === "npm") {
609
+ const times = body?.time ?? {}; const versions = body?.versions ?? {};
610
+ rows = Object.keys(versions).filter((v) => !versions[v]?.deprecated).map((v) => ({ v, at: Date.parse(times[v]) }));
611
+ } else {
612
+ rows = (body?.versions ?? []).filter((r) => r && !r.yanked).map((r) => ({ v: r.num, at: Date.parse(r.created_at) }));
613
+ }
614
+ rows = rows.filter((r) => typeof r.v === "string" && EXACT_SEMVER.test(r.v) && !r.v.includes("-") && Number.isFinite(r.at));
615
+ if (!rows.length) return null;
616
+ const byVersion = (a, b) => compareVersions(a.v, b.v);
617
+ const settled = rows.filter((r) => now - r.at >= settleMs).sort(byVersion);
618
+ return settled.length ? settled.at(-1).v : rows.sort(byVersion)[0].v;
619
+ }
620
+
589
621
  // ---------------------------------------------------------------------------------------------- freshness + holds
590
622
 
591
623
  function matchesPattern(name, pattern) {
@@ -49,7 +49,7 @@
49
49
  "@rightkit/qa": "0.6.2",
50
50
  "@rightkit/qa-chrome": "0.1.12",
51
51
  "@rightkit/react-shell": "0.7.2",
52
- "@rightkit/release": "0.6.24",
52
+ "@rightkit/release": "0.6.25",
53
53
  "@rightkit/settings-ui": "0.7.0",
54
54
  "@rightkit/social-ui": "0.1.0",
55
55
  "@rightkit/system-ui": "0.1.0",
@@ -607,7 +607,7 @@
607
607
  "rightkit-playback": "0.3.0",
608
608
  "rightkit-policy-rules": "0.1.0",
609
609
  "rightkit-privileged-helper": "0.1.0",
610
- "rightkit-qa": "0.10.1",
610
+ "rightkit-qa": "0.10.2",
611
611
  "rightkit-qa-driver": "0.7.0",
612
612
  "rightkit-record": "0.3.3",
613
613
  "rightkit-reframe": "0.1.0",
package/sign-windows.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Windows Authenticode signing through Azure Artifact Signing. Two entries: the CLI (`node sign-windows.mjs [--dry-run|--verify-only] [--receipt <file>] <file>...`,
3
- // unchanged) and the functions `signWindowsFiles` / `verifyWindowsFiles` that the gate's in-process signing steps reuse (suite-signing.mjs), so there is
3
+ // unchanged) and the functions `signWindowsFiles` / `verifyWindowsFiles` that the gate's signing steps reuse, so there is
4
4
  // one signer and one verifier. The identity is the signer SUBJECT (WINDOWS_SIGNER_SUBJECT), never a thumbprint.
5
5
  import { createHash } from "node:crypto";
6
6
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -49,9 +49,25 @@ export const CALLING_GATE_ENV = "RUSTRIGHT_CALLING_GATE";
49
49
  /** The managed `ws` roots a broker pins its generation targets under (BROKER-CONTRACT output pinning). */
50
50
  export const brokerOutputRoots = ({ dataRoot }) => [path.join(dataRoot, "rightkit-build-control", "ws")];
51
51
 
52
- /** The broker's receipt of one request, read through the RustRight client (`rustright status <id>`), synchronously. */
53
- export function receiptSync({ client, requestId, env, spawn = spawnSync, platform = process.platform }) {
54
- return receiptFromReply(spawnBrokerClient(spawn, client, ["status", requestId], statusOptions(env), { platform }), requestId);
52
+ /**
53
+ * The longest a read waits for a job that is still live although its client already exited (A7). The client can return before the broker
54
+ * has started the job (a race seen at 16 lanes), so one STATUS read of a live job is not a verdict: the read then follows the job with
55
+ * `rustright attach <id>` (the client's WATCH: it blocks until the job ends and rides out a broker restart; never a poll) and reads STATUS again.
56
+ */
57
+ export const LIVE_WAIT_MS = 300_000;
58
+ const liveWaitOptions = (env, ms) => ({ ...statusOptions(env), timeout: ms });
59
+ const isLiveRead = (e) => e?.diagnosis?.kind === "live";
60
+ const stillLive = (e, waitedMs) => Object.assign(new SnapshotRequestError(e.code, `${e.message}; the read also waited up to ${Math.round(waitedMs / 1000)} s with \`rustright attach\` (WATCH) for the terminal state and the job was still live`), { diagnosis: e.diagnosis });
61
+
62
+ /** The broker's receipt of one request, read through the RustRight client (`rustright status <id>`), synchronously; a live job is waited for once, bounded (LIVE_WAIT_MS). */
63
+ export function receiptSync({ client, requestId, env, spawn = spawnSync, platform = process.platform, liveWaitMs = LIVE_WAIT_MS }) {
64
+ const read = () => receiptFromReply(spawnBrokerClient(spawn, client, ["status", requestId], statusOptions(env), { platform }), requestId);
65
+ try { return read(); } catch (e) {
66
+ if (!isLiveRead(e) || !(liveWaitMs > 0)) throw e;
67
+ // The attach result is not the verdict (its stream is not a receipt): whatever it ends with, the terminal state is read back by STATUS.
68
+ spawnBrokerClient(spawn, client, ["attach", requestId], liveWaitOptions(env, liveWaitMs), { platform });
69
+ try { return read(); } catch (again) { throw isLiveRead(again) ? stillLive(again, liveWaitMs) : again; }
70
+ }
55
71
  }
56
72
  const statusOptions = (env) => ({ encoding: "utf8", env, timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
57
73
 
@@ -63,7 +79,8 @@ function receiptFromReply(r, requestId) {
63
79
  // A refused or unavailable reply may arrive with a nonzero exit (the client exits 125 on a REJECTED result): its typed body is still diagnosed (GB-84).
64
80
  try { doc = JSON.parse(String(r.stdout)); } catch { throw r.status !== 0 ? failedRun() : new SnapshotRequestError("receipt", `rustright status ${requestId} printed no JSON`); }
65
81
  // The status envelope is `{type:"STATUS", id, state, ..., receipt}`: `receipt` is null while the job is live and full once it is
66
- // terminal. Polling is not allowed, so a null receipt after the cargo process exited is a clear error, never a retry.
82
+ // terminal. A null receipt after the cargo process exited is not a verdict at once: the caller waits for the terminal state once (receiptSync:
83
+ // `rustright attach`, bounded, never a poll), and a job still live after that is a clear error, never a retry.
67
84
  // GB-53: an over-cap outcome is a typed truncation, never a missing-field mystery.
68
85
  const over = overCapProblem(doc);
69
86
  if (over) throw new SnapshotRequestError(over.code, `rustright status ${requestId}: ${over.message}`);
@@ -386,7 +403,7 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
386
403
  } catch (e) { return refuse("RK_CARGO_BROKER_CARRIER", e.message); }
387
404
  try { record({ ...base, status: "pending", nonce: made.carrier.nonce, requestRef: path.join(refDir, "request.json"), ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd }); } catch (e) { removeCarrier(made.file); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
388
405
  // GB-52: the call's operation deadline (the caller's own cap is never replaced) and an outer guard past the client's own cancel wait.
389
- const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
406
+ const childEnv = { RUST_BACKTRACE: clientEnv.RUST_BACKTRACE ?? "1", ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
390
407
  const guard = clientGuardMs(childEnv);
391
408
  const capture = stdio === "capture";
392
409
  // Broker work may exist from the next statement on: the ownership records this request's sub-handle first (a mark that does not land sends nothing).
@@ -410,6 +427,7 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
410
427
  } catch (e) {
411
428
  outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
412
429
  }
430
+ outcome = { ...outcome, clientStatus: r.status ?? null, clientSignal: r.signal ?? null }; // A7: the client's own exit, kept with every outcome
413
431
  const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
414
432
  // GB-54: a request the broker REFUSED (admission paused, a suspended or unknown job, a cutover drain) never started: the client's typed REJECTED result is kept as the outcome's
415
433
  // code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
@@ -595,7 +613,7 @@ function* carryBrokerAdapterSteps({ adapter, words, cwd, env, section, record, e
595
613
  } catch (e) { return refuse("RK_ADAPTER_BROKER_CARRIER", e.message); }
596
614
  try { record({ ...base, status: "pending", nonce: made.carrier.nonce, requestRef: path.join(refDir, "request.json"), ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd }); } catch (e) { removeCarrier(made.file); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
597
615
  // GB-52: the call's operation deadline (the caller's own cap is never replaced) and an outer guard past the client's own cancel wait.
598
- const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
616
+ const childEnv = { RUST_BACKTRACE: clientEnv.RUST_BACKTRACE ?? "1", ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
599
617
  const guard = clientGuardMs(childEnv);
600
618
  const capture = stdio === "capture";
601
619
  // Broker work may exist from the next statement on: the ownership records this request's sub-handle first (a mark that does not land sends nothing).
@@ -625,14 +643,21 @@ function* carryBrokerAdapterSteps({ adapter, words, cwd, env, section, record, e
625
643
  // directory in the snapshot), which must be inside that root"). It is the carrier's primaryCwd, never the toplevel for a step run from a subfolder
626
644
  // (an app whose root is `app/`), and never a directory outside the primary checkout or one that is not this call's counterpart.
627
645
  if (canonicalCounterpart(ref.workspace) !== canonicalCounterpart(checkoutCwd) || !inside(canonicalCounterpart(ref.workspace), section.checkout)) throw new SnapshotRequestError("workspace", `the request's workspace ${ref.workspace} is not the primary checkout counterpart ${checkoutCwd} of the step's directory ${realCwd} (primary toplevel ${section.checkout})`);
628
- const statusReply = yield { args: ["status", ref.requestId], options: statusOptions(clientEnv) };
629
- const receipt = receiptFromReply(statusReply, ref.requestId);
646
+ let statusReply = yield { args: ["status", ref.requestId], options: statusOptions(clientEnv) };
647
+ // A job still live although its client exited (A7): follow it once with `rustright attach` (WATCH, bounded), then read STATUS again; never a poll.
648
+ let liveWaited = false;
649
+ try { receiptFromReply(statusReply, ref.requestId); } catch (probe) {
650
+ if (isLiveRead(probe)) { yield { args: ["attach", ref.requestId], options: liveWaitOptions(clientEnv, LIVE_WAIT_MS) }; statusReply = yield { args: ["status", ref.requestId], options: statusOptions(clientEnv) }; liveWaited = true; }
651
+ }
652
+ let receipt;
653
+ try { receipt = receiptFromReply(statusReply, ref.requestId); } catch (again) { throw liveWaited && isLiveRead(again) ? stillLive(again, LIVE_WAIT_MS) : again; }
630
654
  const { payload, evidence, attribution, toolchain, nestedChildren } = verifySnapshotReceipt(receipt, { pins: section.pins, jobId: ref.requestId, carrier: made.carrier, argv: words, workspace: ref.workspace, adapter, adapterEnv, checkout: section.checkout, outputRoots: section.outputRoots, platform: section.platform });
631
655
  nativeManaged = signedNativeOutputs(receipt, payload);
632
656
  outcome = { status: "verified", requestId: ref.requestId, workspace: ref.workspace, outputDir: evidence.outputDir, nestedChildren, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
633
657
  } catch (e) {
634
658
  outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
635
659
  }
660
+ outcome = { ...outcome, clientStatus: r.status ?? null, clientSignal: r.signal ?? null }; // A7: the client's own exit, kept with every outcome
636
661
  const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
637
662
  // GB-54: a request the broker REFUSED (admission paused, a suspended or unknown job, a cutover drain) never started: the client's typed REJECTED result is kept as the outcome's
638
663
  // code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
@@ -121,3 +121,35 @@ export function snapshotCargoSpawn({ spawn, carrySpawn = spawn, cargoCommand, sn
121
121
  } catch (e) { return refused(e.code ?? "RK_SNAPSHOT_BROKER", e.message); }
122
122
  };
123
123
  }
124
+
125
+ /**
126
+ * GATE-CONTRACT GC-A11 (Adrian, 2026-11-02: "Class a never in the broker. Any cargo things that are cheap and non compiling are in class a,
127
+ * can run outside the broker."): the same read-only shapes as above, run DIRECTLY with the host's real toolchain cargo, never carried to the
128
+ * broker. `directPath` is the PATH with RustRight shims and outer gate wrappers taken off (managed-cargo.mjs directCargoPath), so `cargo`
129
+ * resolves to the pinned toolchain, not the broker's shim. Each call writes only to its own scratch target under `tmpRoot` (cargo would
130
+ * otherwise write target/.rustc_info.json beside the manifest and the snapshot would no longer be the commit's tree); the scratch is removed
131
+ * when the call returns. Every other cargo call inside the snapshot is refused (a compile belongs to the broker); calls outside the snapshot
132
+ * and every non-cargo command pass through to `spawn` unchanged. `spawn` may be synchronous or return a promise.
133
+ */
134
+ export function directReadSpawn({ spawn, cargoCommand, snapshotRoot, directPath, pathKey = "PATH", tmpRoot, env, err = () => {} }) {
135
+ const prefix = cargoCommand.slice(1);
136
+ const refused = (code, message) => { err(`rightkit-gate: ${code}: ${message}`); return { status: BROKER_REFUSAL_STATUS, signal: null, error: null, stdout: "", stderr: `rightkit-gate: ${code}: ${message}\n` }; };
137
+ return (cmd, args, opts = {}) => {
138
+ const cargo = cmd === cargoCommand[0] && prefix.every((w, i) => args[i] === w);
139
+ if (!cargo || typeof opts.cwd !== "string" || !within(real(opts.cwd), real(snapshotRoot))) return spawn(cmd, args, opts);
140
+ const words = args.slice(prefix.length);
141
+ const shape = carriedScanRefusal(words);
142
+ if (shape !== null) return refused(CARRIED_METADATA_REFUSED, `\`cargo ${words.join(" ")}\` would run inside the managed snapshot ${snapshotRoot} and is not one of the read-only shapes Class A runs directly (GC-A11): ${shape}`);
143
+ let scratch;
144
+ try { fs.mkdirSync(tmpRoot, { recursive: true }); scratch = fs.mkdtempSync(path.join(tmpRoot, "rk-class-a-cargo-")); }
145
+ catch (e) { return refused("RK_CLASS_A_SCRATCH", `cannot create a scratch target under ${tmpRoot}: ${e.message}`); }
146
+ const childEnv = { ...env, ...(opts.env ?? {}), [pathKey]: directPath, CARGO_TARGET_DIR: scratch };
147
+ const cleanup = () => { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best effort: a scratch dir under the gate's tmp root */ } };
148
+ let r;
149
+ try { r = spawn("cargo", words, { ...opts, env: childEnv }); }
150
+ catch (e) { cleanup(); throw e; }
151
+ if (r && typeof r.then === "function") return r.finally(cleanup);
152
+ cleanup();
153
+ return r;
154
+ };
155
+ }
@@ -23,7 +23,7 @@ import path from "node:path";
23
23
  import { REQUEST_OVER_CAP_CODE, requestOverCap } from "./broker-wire.mjs";
24
24
  import { ownerFileOf, writeCarrierOwner } from "./carrier-census.mjs";
25
25
  import { BROKER_COMPILE_COMMANDS, CARGO_COMMAND_ALIASES, cargoCommandIndex, cargoSubcommandOf, expandCargoAliases, isCompileCommand, isPackageObservation } from "./cargo-shapes.mjs";
26
- import { canonicalJson, CAUSE_EVIDENCE_SCHEMA, CAUSE_UNEXPLAINED, CAUSE_UNKNOWN, ProvenanceError, requireExplainedCauses, verifyBr19Evidence, verifyCauseEvidence, verifyProvenance, verifyReceiptCoverage, verifyResultEvidence } from "./provenance-verify.mjs";
26
+ import { canonicalJson, CAUSE_EVIDENCE_SCHEMA, CAUSE_UNEXPLAINED, CAUSE_UNKNOWN, ProvenanceError, verifyBr19Evidence, verifyCauseEvidence, verifyProvenance, verifyReceiptCoverage, verifyResultEvidence } from "./provenance-verify.mjs";
27
27
 
28
28
  export const SNAPSHOT_REQUEST_SCHEMA = "rustright.snapshot-request/1";
29
29
  // One spelling per variable, the current one (rustright-cli shims.rs reads RUSTRIGHT_* first, then the legacy RIGHTKIT_* spelling, which task
@@ -646,8 +646,11 @@ export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, wor
646
646
  // legitimately carries no census; when one carries cause evidence anyway it must still be well formed, but nothing is judged for it (nothing was rebuilt).
647
647
  const signedFreshness = stage.freshness ?? null;
648
648
  let causeEvidence = null;
649
- if (compile) causeEvidence = requireExplainedCauses(signedFreshness);
650
- else if (isObject(signedFreshness) && signedFreshness.causeEvidence !== undefined) causeEvidence = verifyCauseEvidence(signedFreshness);
649
+ // Adrian, 2026-11-02 (the broker owner's chat, checked with pulse `said`): "Refusal, remove but I want a daily job checking the warmth etc and
650
+ // reporting issues." An unexplained or unknown rebuild no longer fails a stage: the cause evidence is still read, checked well formed and
651
+ // recorded (the daily warmth check reports it), never judged here. The broker stopped refusing it too (rustright b00220f).
652
+ // A compile must still carry well-formed signed cause evidence (verifyCauseEvidence refuses a missing or incoherent one); its verdict is not judged.
653
+ if (compile || (isObject(signedFreshness) && signedFreshness.causeEvidence !== undefined)) causeEvidence = verifyCauseEvidence(signedFreshness);
651
654
  // Fields the signature does not cover must still be present with their types (the consumer reads them). Every carried command:
652
655
  // exit (an integer, or null with a signal) and signal, timingsMs{admission, queue, bind, cargo, afterExitBind, diagnostics} (each a
653
656
  // number or null) and diagnosticsPath (a path, or null). Success needs exit 0 and no signal, as signed. `verdict` and `refreshed`
package/stage-sidecar.mjs CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  bundledFileName, planMediaSidecar, sourceOfferText, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
47
47
  } from "./media-sidecar.mjs";
48
48
  import { ContentDigestError, contentIdentity, digestFor } from "./content-digest.mjs";
49
+ import { oneOutput } from "./cross-targets.mjs";
49
50
 
50
51
  export const STAGE_SIDECAR_CONTRACT = "rightkit-stage-sidecar-v1";
51
52
  export const STAGE_SIDECAR_RECEIPT_NAME = "stage-sidecar.json";
@@ -107,7 +108,7 @@ const NATIVE_OTHER_ROUTE = Object.freeze({ ffmpeg: "has its own kind: use { kind
107
108
  /** A catalog version token (1.24.4, chromium/8021): only compared with the rows, never used as a path. */
108
109
  const NATIVE_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+/-]{0,63}$/;
109
110
  const KEYS = Object.freeze({
110
- "cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
111
+ "cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "devFeatures", "manifestPath", "sha256", "dest", "clean", "platforms"],
111
112
  ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "resourceDir", "sources", "probe", "platforms"],
112
113
  onnxruntime: ["kind", "version", "catalog", "resourceDir", "as", "licenseDir", "cacheDir", "companions", "platforms"],
113
114
  "native-library": ["kind", "library", "version", "catalog", "resourceDir", "licenseDir", "cacheDir", "as", "files", "companions", "source", "platforms"],
@@ -190,6 +191,8 @@ export function resolveBuildMode(env = process.env) {
190
191
 
191
192
  /** A profile is a Cargo profile name or exactly { dev, release } of names. Returns the pair or the single name. */
192
193
  function parseProfile(value, label, errors) {
194
+ // One build output (GATE-CONTRACT GC-A15): a sidecar builds only the release profile; any other profile, or a { dev, release } pair, is refused.
195
+ if (value !== "release") { errors.push(`${label} must be "release" (no other Cargo profile is allowed, GC-A15); omit it, release is the default`); return "release"; }
193
196
  if (typeof value === "string") {
194
197
  if (!PROFILE.test(value)) errors.push(`${label} must be a Cargo profile name`);
195
198
  return value;
@@ -329,8 +332,9 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
329
332
  if (!isObject(wrapper)) fail("STAGE_SIDECAR_CONFIG_INVALID", "sidecars must be an array (or { sidecars, stageDir?, receiptPath?, profiles? })");
330
333
  for (const key of Object.keys(wrapper)) if (!["sidecars", "stageDir", "receiptPath", "profiles", "wipeStageDir"].includes(key)) errors.push(`unknown key ${JSON.stringify(key)}`);
331
334
  if (wrapper.wipeStageDir !== undefined && typeof wrapper.wipeStageDir !== "boolean") errors.push("wipeStageDir must be a boolean");
332
- const defaultProfile = wrapper.profiles === undefined ? undefined : parseProfile(wrapper.profiles, "profiles", errors);
333
- if (typeof defaultProfile === "string") errors.push("profiles must be { dev, release }");
335
+ // The { dev, release } profile pair is retired (GC-A15): every sidecar builds release; a `profiles` key is refused.
336
+ if (wrapper.profiles !== undefined) errors.push("profiles is not allowed: every sidecar builds the release profile (GC-A15); remove the key");
337
+ const defaultProfile = undefined;
334
338
  if (!Array.isArray(wrapper.sidecars) || !wrapper.sidecars.length) errors.push("sidecars must be a non-empty array");
335
339
  const stageDir = relativePath(wrapper.stageDir ?? DEFAULT_STAGE_DIR, "stageDir", errors);
336
340
  const receiptPath = wrapper.receiptPath === undefined
@@ -374,8 +378,13 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
374
378
  if (typeof name !== "string" || !NAME.test(name) || /\.exe$/i.test(name) || name !== path.basename(name)) errors.push(`${where}: name must be a bare sidecar name (no path, no .exe, no target triple)`);
375
379
  const declared = item.profile !== undefined ? parseProfile(item.profile, `${where}: profile`, errors) : (defaultProfile ?? "release");
376
380
  const profile = isObject(declared) ? declared[mode] : declared;
377
- const features = item.features ?? [];
378
- if (!Array.isArray(features) || features.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_/-]+$/.test(f))) errors.push(`${where}: features must be a list of feature names (package/feature allowed)`);
381
+ // `devFeatures` are added only for the dev build (RIGHTKIT_BUILD_MODE=dev, set by the dev lane): the app's QA feature, e.g. qa-native.
382
+ // The Cargo profile is release either way (GC-A15); the release tool refuses those features in a release (release-switch-registry.json).
383
+ const devFeatures = item.devFeatures ?? [];
384
+ const badList = (list) => !Array.isArray(list) || list.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_/-]+$/.test(f));
385
+ if (badList(item.features ?? [])) errors.push(`${where}: features must be a list of feature names (package/feature allowed)`);
386
+ if (badList(devFeatures)) errors.push(`${where}: devFeatures must be a list of feature names (package/feature allowed)`);
387
+ const features = [...(item.features ?? []), ...(mode === "dev" && Array.isArray(devFeatures) ? devFeatures : [])];
379
388
  const manifestPath = item.manifestPath === undefined ? undefined : relativePath(item.manifestPath, `${where}: manifestPath`, errors);
380
389
  sha(item.sha256, "sha256");
381
390
  const dest = destOf(item.dest);
@@ -961,7 +970,14 @@ function stageCargoBin(entry, ctx) {
961
970
  fs.chmodSync(temporary, 0o755);
962
971
  fs.renameSync(temporary, dest);
963
972
  carryMtime(ctx, dest, slices.map((slice) => slice.source));
964
- return [{ kind: "cargo-bin", name: entry.name, path: rel(ctx, dest), sha256, sizeBytes: fs.statSync(dest).size, changed, sourceSha256: sha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: !entry.dest, slices: slices.map((slice) => ({ triple: slice.triple, sha256: slice.sourceSha256 })) }];
973
+ // rightkit#1443.2: Tauri's universal build compiles each arch and resolves an externalBin per THIN triple, so each slice is also staged as
974
+ // `<name>-<thin triple>` beside the universal file (the pre-gate R2 build staged all three). Only for an externalBin (no `dest`).
975
+ const thin = entry.dest ? [] : slices.map((slice) => {
976
+ const thinDest = path.resolve(ctx.appRoot, ctx.stageDir, stagedName(entry.name, parseTargetTriple(slice.triple)));
977
+ const copied = copyInto(ctx, slice.source, thinDest, { executable: true });
978
+ return { kind: "cargo-bin", name: entry.name, path: rel(ctx, thinDest), ...copied, sourceSha256: slice.sourceSha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: false, sliceOf: rel(ctx, dest), triple: slice.triple };
979
+ });
980
+ return [{ kind: "cargo-bin", name: entry.name, path: rel(ctx, dest), sha256, sizeBytes: fs.statSync(dest).size, changed, sourceSha256: sha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: !entry.dest, slices: slices.map((slice) => ({ triple: slice.triple, sha256: slice.sourceSha256 })) }, ...thin];
965
981
  }
966
982
 
967
983
  function stageCargoSlice(entry, ctx, triple, { dest }) {
@@ -974,9 +990,10 @@ function stageCargoSlice(entry, ctx, triple, { dest }) {
974
990
  /** Build one slice through `rustright cargo` and locate the executable. Returns { source, sourceSha256 }. */
975
991
  function stageCargoBuild(entry, ctx, triple) {
976
992
  const { run, appRoot, env, fs } = ctx;
977
- const args = ["cargo", "build", "--locked", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : []),
978
- "-p", entry.package, "--bin", entry.bin, "--target", triple, "--profile", entry.profile,
979
- ...(entry.features.length ? ["--features", entry.features.join(",")] : []), "--message-format=json-render-diagnostics"];
993
+ // One output directory (oneOutput, cross-targets.mjs): a sidecar always builds with the release profile, whatever profile its entry names.
994
+ const args = oneOutput(["cargo", "build", "--locked", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : []),
995
+ "-p", entry.package, "--bin", entry.bin, "--target", triple,
996
+ ...(entry.features.length ? ["--features", entry.features.join(",")] : []), "--message-format=json-render-diagnostics"]);
980
997
  ctx.log(`stage-sidecar: rightkit ${args.join(" ")}`);
981
998
  // From a snapshot, Cargo would resolve `target/` (or a relative build.target-dir) against the snapshot: its output directory is
982
999
  // resolved in the checkout instead and passed as an absolute CARGO_TARGET_DIR, so the warm outputs stay the checkout's and the
@@ -996,7 +1013,7 @@ function stageCargoBuild(entry, ctx, triple) {
996
1013
  if (carried && (!source || !fs.existsSync(source))) {
997
1014
  const targetDir = typeof built.outputDir === "string" && path.isAbsolute(built.outputDir) ? built.outputDir : undefined;
998
1015
  if (!targetDir) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and the receipt names no output directory`, { bin: entry.bin });
999
- const candidates = builtBinaryCandidates({ targetDir, targetDirs: [], triple, profile: entry.profile, bin: entry.bin, windows: ctx.windows });
1016
+ const candidates = builtBinaryCandidates({ targetDir, targetDirs: [], triple, profile: "release", bin: entry.bin, windows: ctx.windows });
1000
1017
  source = candidates.find((file) => fs.existsSync(file));
1001
1018
  if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build did not produce ${entry.bin} (looked in ${candidates.join(", ")})`, { bin: entry.bin, candidates });
1002
1019
  } else if (!source || !fs.existsSync(source)) {
@@ -1008,7 +1025,7 @@ function stageCargoBuild(entry, ctx, triple) {
1008
1025
  // authoritative; an explicit CARGO_TARGET_DIR in this environment is tried too, resolved against the app root.
1009
1026
  const envDir = typeof buildEnv.CARGO_TARGET_DIR === "string" && buildEnv.CARGO_TARGET_DIR.trim() ? path.resolve(appRoot, buildEnv.CARGO_TARGET_DIR) : undefined;
1010
1027
  if (!targetDir && !envDir) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and its target directory cannot be read`, { bin: entry.bin });
1011
- const candidates = builtBinaryCandidates({ targetDir, targetDirs: envDir ? [envDir] : [], triple, profile: entry.profile, bin: entry.bin, windows: ctx.windows });
1028
+ const candidates = builtBinaryCandidates({ targetDir, targetDirs: envDir ? [envDir] : [], triple, profile: "release", bin: entry.bin, windows: ctx.windows });
1012
1029
  source = candidates.find((file) => fs.existsSync(file));
1013
1030
  if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build did not produce ${entry.bin} (looked in ${candidates.join(", ")})`, { bin: entry.bin, candidates });
1014
1031
  }
@@ -1,12 +1,11 @@
1
1
  // The signing policy of a gate-run test or dev build (rightkit#1303, GATE-CONTRACT "One signing identity per product, every build"). Pure: it reads
2
- // nothing but what its caller hands it, so the gate's route decision (rightkit-gate.mjs routePlan, from the commit's own bytes), the `sign` step
3
- // (suite-signing.mjs, from the snapshot) and the tests all judge the same text.
2
+ // nothing but what its caller hands it, so the gate's route decision (rightkit-gate.mjs routePlan, from the commit's own bytes) and the tests all judge the same text.
4
3
  //
5
4
  // - The identity is derived, never declared: the app's tauri.conf.json `identifier` is the one id. Mac install.bundleId must equal it, and a
6
5
  // dev or test Tauri `--config` that overrides it is refused. The team is DEFAULT_TEAM, the signer subject WINDOWS_SIGNER_SUBJECT.
7
6
  // - A suite that builds a product (tauri build/bundle, or a cargo build of the tauri package or a sidecar package) must be named by a later `sign`
8
- // step; a Windows dev `install.kind: "tauri"` is refused (`tauri-authenticode` is the signed route); `sign-qa-bundle` and RIGHTKIT_DEV_SIGNING
9
- // (ad hoc) are refused.
7
+ // step; a Windows dev `install.kind: "tauri"` is refused (`tauri-authenticode` is the signed route); RIGHTKIT_DEV_SIGNING
8
+ // (ad hoc) is refused.
10
9
  // - These refusals apply on Mac and Dell only (process.platform darwin or win32). Linux builds sign nothing and hosted CI is exempt.
11
10
  import path from "node:path";
12
11
  import { createRequire } from "node:module";
@@ -14,14 +13,6 @@ import { DEFAULT_TEAM, WINDOWS_SIGNER_SUBJECT } from "./signing-identity.mjs";
14
13
 
15
14
  export const SIGN_TEAM = DEFAULT_TEAM;
16
15
  export const SIGN_SUBJECT = WINDOWS_SIGNER_SUBJECT;
17
- /** The environment variable a successful `sign` step sets for the suites after it: the signed copy of the first file. */
18
- export const QA_APP_BINARY_ENV = "RIGHTKIT_QA_APP_BINARY";
19
- /** The environment variable a successful `sign` step sets beside it: the path of the signing evidence file (signed files, sha256 before and after, identity). */
20
- export const QA_SIGNING_EVIDENCE_ENV = "RIGHTKIT_QA_SIGNING_EVIDENCE";
21
- /** GB-42: set beside the two above: a JSON object mapping each file a `sign` step signed (its `path`, relative to the output layout) to its signed copy, so a
22
- * consumer is handed every declared sidecar's signed bytes, not only the main binary's. */
23
- export const QA_SIGNED_FILES_ENV = "RIGHTKIT_QA_SIGNED_FILES";
24
- export const SIGN_STEP_KEYS = Object.freeze(["name", "sign"]);
25
16
  export const WINDOWS_TAURI_KIND = "tauri";
26
17
  export const WINDOWS_SIGNED_KIND = "tauri-authenticode";
27
18
 
@@ -44,45 +35,8 @@ export class SuiteSigningError extends Error {
44
35
  }
45
36
  const refuse = (code, message, details) => { throw new SuiteSigningError(code, message, details); };
46
37
  const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
47
- const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
48
38
  const SIGNING_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
49
39
 
50
- /** A relative path with no `..`, no drive, no root: the form a sign file path takes inside a receipt's output directory. Returns it with `/`. */
51
- export function containedRelative(value, label) {
52
- if (typeof value !== "string" || !value.trim()) refuse("SIGN_STEP_PATH", `${label} must be a non-empty relative path`);
53
- const normalized = value.replaceAll("\\", "/");
54
- if (path.isAbsolute(value) || path.win32.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.startsWith("/") || normalized.split("/").includes("..") || normalized.split("/").includes("")) {
55
- refuse("SIGN_STEP_PATH", `${label} must be a relative path inside the earlier suite's output directory (no absolute path, no .., no empty segment): ${JSON.stringify(value)}`);
56
- }
57
- return normalized;
58
- }
59
-
60
- /**
61
- * Validate one `sign` spec: `{ files: [{ from, path }] }`. `from` names an EARLIER suite of the same list (`earlier` is the set of names declared
62
- * before this step), `path` is relative to that suite's carried-receipt output directory. Returns the spec with normalized paths.
63
- */
64
- export function validateSignSpec(sign, label, earlier) {
65
- if (!isObject(sign)) refuse("SIGN_STEP_SHAPE", `${label} must be an object { files: [{ from, path }] }`);
66
- const unknown = Object.keys(sign).filter((key) => key !== "files");
67
- if (unknown.length) refuse("SIGN_STEP_SHAPE", `${label} has unknown key(s): ${unknown.join(", ")} (allowed: files). The identity is derived from the product, never declared here`);
68
- if (!Array.isArray(sign.files) || sign.files.length === 0) refuse("SIGN_STEP_SHAPE", `${label}.files must be a non-empty list of { from, path }`);
69
- const seen = new Set();
70
- const files = sign.files.map((item, index) => {
71
- const at = `${label}.files[${index}]`;
72
- if (!isObject(item)) refuse("SIGN_STEP_SHAPE", `${at} must be { from, path }`);
73
- const extra = Object.keys(item).filter((key) => key !== "from" && key !== "path");
74
- if (extra.length) refuse("SIGN_STEP_SHAPE", `${at} has unknown key(s): ${extra.join(", ")} (allowed: from, path)`);
75
- if (typeof item.from !== "string" || !NAME.test(item.from)) refuse("SIGN_STEP_FROM", `${at}.from must name an earlier suite`);
76
- if (!earlier.has(item.from)) refuse("SIGN_STEP_FROM", `${at}.from ${JSON.stringify(item.from)} is not a suite declared before this step (${[...earlier].join(", ") || "none"}); a sign step signs what an earlier suite built`);
77
- const rel = containedRelative(item.path, `${at}.path`);
78
- const key = `${item.from}\0${rel}`;
79
- if (seen.has(key)) refuse("SIGN_STEP_SHAPE", `${at} repeats ${item.from}:${rel}`);
80
- seen.add(key);
81
- return { from: item.from, path: rel };
82
- });
83
- return { files };
84
- }
85
-
86
40
  // ---- what a step is -------------------------------------------------------------------------------------------------------------
87
41
 
88
42
  const base = (program) => path.basename(String(program ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "").toLowerCase();
@@ -391,13 +345,11 @@ function signPathNames(file, layout, bin) {
391
345
  }
392
346
 
393
347
  /**
394
- * GB-42 (GATE 42, E19; stable identity before launch): why an earlier sign step never makes `cargo run` or an integration test that uses
395
- * CARGO_BIN_EXE_ safe. The signer signs a COPY outside the build's output directory, and neither Cargo nor a compile-time literal is redirected to it
396
- * (RIGHTKIT_QA_APP_BINARY changes nothing for them), so such a step starts the raw, unsigned bytes in the target directory even though the sign
397
- * declaration names the right paths. The supported route is: compile only (`cargo test --no-run`, a build), sign, then a consumer that is handed the
398
- * signed path (the QA runner, which reads RIGHTKIT_QA_APP_BINARY and RIGHTKIT_QA_SIGNED_FILES) and runs prebuilt bytes.
348
+ * GB-42 (GATE 42, E19; stable identity before launch): why `cargo run` or an integration test that uses CARGO_BIN_EXE_ is never safe in a build
349
+ * stage. Cargo resolves the product bin inside the build's output directory, so such a step starts the raw, unsigned bytes in the target directory.
350
+ * Product bins run only from the installed, signed build (GATE-CONTRACT GC-A07: QA journeys run after the build against the installed dev copy).
399
351
  */
400
- export const RAW_TARGET_LAUNCH = "launches a product bin Cargo resolves inside the build's target directory (cargo run, or an integration test using CARGO_BIN_EXE_): the signer signs a copy elsewhere and nothing redirects Cargo or a compile-time literal to it, so an earlier sign step does not bind the bytes that run. Build compile-only (cargo test --no-run), sign, then run a consumer that is handed the signed path (RIGHTKIT_QA_APP_BINARY, RIGHTKIT_QA_SIGNED_FILES)";
352
+ export const RAW_TARGET_LAUNCH = "launches a product bin Cargo resolves inside the build's target directory (cargo run, or an integration test using CARGO_BIN_EXE_): the signer signs a copy elsewhere and nothing redirects Cargo or a compile-time literal to it, so the bytes that run are never the signed ones. Product bins run only from the installed, signed build (GATE-CONTRACT GC-A07)";
401
353
 
402
354
  /**
403
355
  * Launch binding (rightkit#1303): the earlier sign steps that satisfy a launching suite must name, in `sign.files`, each executable it launches. `launch` is
@@ -435,11 +387,9 @@ export function isProductBuildStep(step, targets = { packages: new Set(), manife
435
387
  return found.product || found.launches || found.unknown.length > 0;
436
388
  }
437
389
 
438
- /** Why a step asks for ad hoc signing (the QA bundle signer, or the ad hoc opt-in), or null. */
390
+ /** Why a step asks for ad hoc signing (the ad hoc opt-in), or null. */
439
391
  export function adHocReason(step) {
440
392
  const words = stepWords(step);
441
- const bad = words.find((word) => /sign-qa-bundle/.test(word));
442
- if (bad !== undefined) return `${JSON.stringify(bad)} runs the ad hoc QA bundle signer (sign-qa-bundle)`;
443
393
  const named = words.find((word) => /RIGHTKIT_DEV_SIGNING/.test(word));
444
394
  if (named !== undefined) return `${JSON.stringify(named)} names RIGHTKIT_DEV_SIGNING`;
445
395
  if (isObject(step?.env) && Object.keys(step.env).some((key) => key === "RIGHTKIT_DEV_SIGNING")) return "its env sets RIGHTKIT_DEV_SIGNING (ad hoc signing)";
@@ -1,28 +1,9 @@
1
- # RightKit standard Cargo profiles for every app workspace (Adrian, 2026-10-16; Astra-reviewed option C).
2
- # One development profile: Cargo's built-in `dev`, used by build, test, check, clippy and `right-release dev`.
3
- # `release-iterate` is retired. Panic behaviour is the same in every app: unwind.
4
- # Copy these tables verbatim into the app's workspace-root Cargo.toml; the RightKit ownership scanner
5
- # flags any deviation. Keep the app's existing [profile.release] optimization keys, but set panic = "unwind".
6
-
7
- [profile.dev]
8
- opt-level = 1
9
- debug = "line-tables-only"
10
- strip = "none"
11
- debug-assertions = true
12
- overflow-checks = true
13
- lto = "off"
14
- panic = "unwind"
15
- incremental = true
16
- codegen-units = 256
17
- rpath = false
18
-
19
- # Dependencies: non-incremental so sccache can cache them across apps.
20
- [profile.dev.package."*"]
21
- incremental = false
22
- codegen-units = 16
23
-
24
- # No test-profile overrides: test inherits dev.
25
- # The retired release-iterate profile must not appear: delete it and its package overrides.
1
+ # RightKit standard Cargo profile for every app workspace (Adrian, 2026-10-16; GATE-CONTRACT GC-A01, GC-A07).
2
+ # One build output and one warm cache per app per host: every gate compile (check, clippy, rustdoc, the test compile and
3
+ # both installers, dev and release) uses [profile.release] for the host's target (cross-targets.mjs oneOutput). The dev
4
+ # installer is the release profile plus `qa-native`. An app carries no [profile.dev], [profile.test] or other custom
5
+ # profile: the RightKit ownership scanner refuses them. `release-iterate` is retired. Panic behaviour is the same in every
6
+ # app: unwind. Keep the app's existing [profile.release] optimization keys, but set panic = "unwind".
26
7
 
27
8
  [profile.release]
28
9
  panic = "unwind"