@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
@@ -8,6 +8,7 @@ import { acquireCommittedSource, assertCommittedCheckoutClean, committedSourceDa
8
8
  import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
9
9
  import { runningPolicyBundle } from "./ownership-policy-bundle.mjs";
10
10
  import { canonicalRepositoryIdentity, loadTrustedExternal, resolveKeyLogTrust, sha256, validateOwnershipFloor, verifyRepositoryRegistration } from "./ownership-approval.mjs";
11
+ import { isReleaseSource } from "./release-source-brand.mjs";
11
12
 
12
13
  const digestFile = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
13
14
 
@@ -32,8 +33,22 @@ function createEvidence(repoRoot, {
32
33
  requireCleanCheckout = false,
33
34
  allowPendingAdvisory = false,
34
35
  policyBundleProvider = runningPolicyBundle,
36
+ // A door source split (release-source-identity.mjs `resolveReleaseSource`, item 7 row 7.04): the evidence binds the SELECTED commit's
37
+ // objects, read from the checkout's object database, and never the checkout's HEAD or status. null (and a checkout-mode source) is today's behaviour.
38
+ releaseSource = null,
35
39
  } = {}, readAuthority = readCommittedKeyAuthority, scanCommitted = checkCommittedApp) {
40
+ if (releaseSource !== null && !isReleaseSource(releaseSource)) throw new Error("ownership evidence: the release source was not resolved by resolveReleaseSource");
41
+ const snapshotSource = releaseSource?.mode === "snapshot" ? releaseSource : null;
36
42
  const root = path.resolve(repoRoot);
43
+ // Real paths on both sides: Git's root is resolved (macOS /private/var), callers may pass /var or /tmp.
44
+ const realPath = (p) => { try { return realpathSync.native(p); } catch { return path.resolve(p); } };
45
+ if (snapshotSource && realPath(root) !== realPath(snapshotSource.checkout)) throw new Error(`ownership evidence: in a door source split the git root is the run's checkout ${snapshotSource.checkout}, not ${root}`);
46
+ // The caller names its app root in the snapshot (its cwd); the committed scan places it by its path inside the repository, which is the
47
+ // same relative path in the checkout. Nothing is read from either: the scan reads committed objects.
48
+ if (snapshotSource) {
49
+ const relative = path.relative(realPath(snapshotSource.snapshot), realPath(scanRoot));
50
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) scanRoot = path.join(snapshotSource.checkout, relative);
51
+ }
37
52
  const scope = requestedScope ? [...new Set(requestedScope)].sort() : null;
38
53
  const manifestBytes = readFileSync(manifestPath);
39
54
  const manifest = JSON.parse(manifestBytes.toString("utf8"));
@@ -48,8 +63,14 @@ function createEvidence(repoRoot, {
48
63
  // a useful local backlog scan. It cannot assert a protected source binding or verify
49
64
  // approval signatures, so this result is deliberately advisory.
50
65
  const useCommittedSource = committed && (!enrollmentPending || Boolean(trustState.trust?.rootFingerprint));
51
- if (useCommittedSource && requireCleanCheckout) assertCommittedCheckoutClean(root);
52
- const source = useCommittedSource ? acquireCommittedSource(root, { requiredPaths: [...(trustState.trust?.provenance ?? []).flatMap((record) => record.paths ?? []), ...(trustState.trust?.rootFingerprint && !trustState.trust?.keyAuthority ? [".rightkit/ownership-keys.json"] : [])] }) : null;
66
+ // In a split the evidence is the selected commit's committed source or nothing: the advisory fallback would scan a mutable tree and
67
+ // read HEAD, which are exactly the reads a split replaces. Fail closed.
68
+ if (snapshotSource && !useCommittedSource) throw new Error("ownership evidence in a door source split needs the committed-source scan; this host's trust enrollment cannot provide it (RK_SOURCE_SPLIT)");
69
+ // The clean-checkout condition guards "bytes scanned (committed blobs) = bytes built (working tree)". A split builds a verified snapshot
70
+ // of the commit instead, so the checkout's cleanliness is irrelevant there (GC:24, GC:156 item 1).
71
+ if (useCommittedSource && requireCleanCheckout && !snapshotSource) assertCommittedCheckoutClean(root);
72
+ const selected = snapshotSource ? { commit: snapshotSource.commit, expectedTree: snapshotSource.tree } : {};
73
+ const source = useCommittedSource ? acquireCommittedSource(root, { ...selected, requiredPaths: [...(trustState.trust?.provenance ?? []).flatMap((record) => record.paths ?? []), ...(trustState.trust?.rootFingerprint && !trustState.trust?.keyAuthority ? [".rightkit/ownership-keys.json"] : [])] }) : null;
53
74
  let keyLogEvidence = null;
54
75
  if (trustState.trust?.keyAuthority) {
55
76
  const resolved = readAuthority(trustState.trust);
@@ -59,7 +80,7 @@ function createEvidence(repoRoot, {
59
80
  keyLogEvidence = { keyLogDigest: resolved.digest, keyCheckpoint: resolved.keyCheckpoint, rootFingerprint: resolved.rootFingerprint, keyAuthority: resolved.authority };
60
81
  if (!resolved.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
61
82
  } else if (trustState.trust?.rootFingerprint) {
62
- const keySource = source ?? acquireCommittedSource(root, { requiredPaths: [".rightkit/ownership-keys.json"] });
83
+ const keySource = source ?? acquireCommittedSource(root, { ...selected, requiredPaths: [".rightkit/ownership-keys.json"] });
63
84
  const sourceData = committedSourceDataForInternalUse(keySource);
64
85
  if (!sourceData) throw new Error("protected ownership key log requires authenticated committed source");
65
86
  const entry = sourceData.entries.find((item) => item.rel === ".rightkit/ownership-keys.json");
@@ -70,7 +91,8 @@ function createEvidence(repoRoot, {
70
91
  keyLogEvidence = { keyLogDigest: resolved.keyLogDigest, keyCheckpoint: resolved.checkpoint, rootFingerprint: resolved.trust.rootFingerprint };
71
92
  if (!resolved.trust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
72
93
  }
73
- if (useCommittedSource && requireCleanCheckout) assertCommittedCheckoutClean(root, { expectedCommit: source.commit, expectedTree: source.tree });
94
+ if (useCommittedSource && requireCleanCheckout && !snapshotSource) assertCommittedCheckoutClean(root, { expectedCommit: source.commit, expectedTree: source.tree });
95
+ if (snapshotSource && (source.commit !== snapshotSource.commit || source.tree !== snapshotSource.tree)) throw new Error("ownership evidence: the acquired committed source is not the selected commit's");
74
96
  const commit = useCommittedSource ? source.commit : git(root, ["rev-parse", "HEAD"], gitRunner);
75
97
  if (!commit) throw new Error("ownership evidence: scan source did not produce a HEAD commit");
76
98
  const tree = useCommittedSource ? source.tree : git(root, ["rev-parse", `${commit}^{tree}`], gitRunner);
@@ -92,8 +114,7 @@ function createEvidence(repoRoot, {
92
114
  ? validateOwnershipFloor(trustState.trust, { scannerVersion, catalogVersion, catalogDigest, ...(policyBundleDigest !== undefined ? { policyBundleDigest } : {}) })
93
115
  : { ok: true, problems: [], warnings: ["protected ownership floor is unavailable: no protected trust registry is enrolled on this host (warning only)"] };
94
116
  floor.warnings = [...(floor.warnings ?? []), ...(bundleProblem ? [bundleProblem] : [])];
95
- // Real paths on both sides: Git's root is resolved (macOS /private/var), callers may pass /var or /tmp.
96
- const real = (p) => { try { return realpathSync.native(p); } catch { return path.resolve(p); } };
117
+ const real = realPath;
97
118
  const requestedRoot = path.relative(real(root), real(scanRoot)).replaceAll("\\", "/") || ".";
98
119
  if (requestedRoot.startsWith("../") || requestedRoot === ".." || path.isAbsolute(requestedRoot)) throw new Error("ownership evidence scan root must be inside repository");
99
120
  const evidenceDiagnostics = [];
@@ -199,13 +220,13 @@ function createEvidence(repoRoot, {
199
220
  }
200
221
 
201
222
  /** Create scan evidence bound to committed source, catalog, and signed exceptions. */
202
- export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
203
- return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
223
+ export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
224
+ return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true, releaseSource });
204
225
  }
205
226
 
206
227
  /** Alias spelling makes the immutable-source contract explicit at adapter boundaries. */
207
- export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
208
- return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true });
228
+ export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
229
+ return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, releaseSource });
209
230
  }
210
231
 
211
232
  // Injection seam is deliberately named for tests and is not used by release commands.
@@ -228,12 +249,15 @@ export function runProtectedLocalOwnershipCheck(repoRoot, scanRoot = repoRoot) {
228
249
  }
229
250
 
230
251
  /** Re-scan exact source and compare every sealed binding before release upload. */
231
- export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null) {
232
- return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope), scope);
252
+ export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
253
+ return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope, releaseSource), scope);
233
254
  }
234
255
 
235
256
  export function verifyOwnershipEvidenceForTest(expected, repoRoot, options) {
236
- return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options), options.scope ?? null);
257
+ // Re-scan the way the evidence was made: committed-git-tree evidence (always the case in a door source split) through the
258
+ // committed-source scan, as verifyOwnershipEvidence does in production.
259
+ const rescan = expected?.source === "committed-git-tree" ? createCommittedOwnershipEvidenceForTest : createOwnershipEvidenceForTest;
260
+ return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => rescan(repoRoot, options), options.scope ?? null);
237
261
  }
238
262
 
239
263
  function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent, scope = null) {
@@ -1,41 +1,22 @@
1
- // right-release hooks install: writes the advisory pre-commit ownership gate into an app repository.
1
+ // right-release hooks: the advisory pre-commit ownership gate is retired (docs/GATE-CONTRACT.md: "Commit and push run no gate and no
2
+ // local hook checks"). Nothing here writes a hook any more.
2
3
  //
3
- // right-release hooks install [--app <dir>]
4
+ // right-release hooks install [--app <dir>] (`right-release hooks install` is `rightkit-gate install-hook`: it retires the hook)
4
5
  //
5
- // The hook runs the app's pinned @rightkit/release (node_modules) via `right-release ownership --staged`.
6
- // An existing foreign pre-commit is preserved as pre-commit.rightkit-foreign and chained first.
7
- // The gate is advisory: `git commit --no-verify` bypasses it; broker admission and the release gate
8
- // remain authoritative.
9
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
6
+ // An earlier release wrote `.git/hooks/pre-commit` (or the one in core.hooksPath) running `right-release ownership --staged`, and
7
+ // kept a foreign pre-commit as pre-commit.rightkit-foreign. Retirement removes the hook by its own marker only and puts the foreign
8
+ // pre-commit back. `right-release ownership --staged` stays a manual command; broker admission and the release gate remain
9
+ // authoritative.
10
+ import { lstatSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync } from "node:fs";
10
11
  import path from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
13
  import { spawnSync } from "./background-process.mjs";
14
+ import { HOOK_MARKER, retireHooksIn } from "./hook-retirement.mjs";
13
15
  import { repositoryRootFor } from "./ownership-staged.mjs";
14
16
 
15
- export const HOOK_MARKER = "rightkit-ownership-precommit v1";
16
- const FOREIGN = "pre-commit.rightkit-foreign";
17
- const PINNED_CLI = path.join("node_modules", "@rightkit", "release", "cli", "right-release.mjs");
17
+ export { HOOK_MARKER };
18
18
 
19
- export function renderPreCommitHook() {
20
- return [
21
- "#!/bin/sh",
22
- `# ${HOOK_MARKER}: advisory ownership gate installed by \`right-release hooks install\`.`,
23
- "# Block-tier findings without a valid exception fail here; --no-verify bypasses this gate.",
24
- "# Authoritative enforcement remains in broker admission and the release gate.",
25
- "ROOT=\"$(git rev-parse --show-toplevel)\" || exit 1",
26
- "HOOKS_DIR=\"$(dirname \"$0\")\"",
27
- `if [ -x "$HOOKS_DIR/${FOREIGN}" ]; then`,
28
- ` "$HOOKS_DIR/${FOREIGN}" "$@" || exit $?`,
29
- "fi",
30
- `GATE="$ROOT/${PINNED_CLI.split(path.sep).join("/")}"`,
31
- "if [ ! -f \"$GATE\" ]; then",
32
- " echo \"[rightkit] @rightkit/release is not installed in this repository; add it as a devDependency, then re-run: right-release hooks install\" >&2",
33
- " exit 1",
34
- "fi",
35
- "exec node \"$GATE\" ownership --staged",
36
- "",
37
- ].join("\n");
38
- }
19
+ const FS_OPS = Object.freeze({ readdir: readdirSync, readFile: (file) => readFileSync(file, "utf8"), lstat: lstatSync, unlink: unlinkSync, rename: renameSync, rmdir: rmdirSync });
39
20
 
40
21
  function git(cwd, args) {
41
22
  const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))) });
@@ -52,46 +33,26 @@ export function hooksDirectoryFor(root) {
52
33
  return path.resolve(root, common);
53
34
  }
54
35
 
55
- /**
56
- * Idempotent install. Returns { status: "installed" | "updated" | "unchanged" | "chained", file }.
57
- */
58
- export function installPreCommitHook(repoDir) {
36
+ /** Remove the ownership pre-commit hook an earlier release wrote, restoring the foreign pre-commit it displaced. { status: "retired" | "restored" | "absent", file } */
37
+ export function retirePreCommitHook(repoDir) {
59
38
  const root = repositoryRootFor(repoDir);
60
- const pinned = path.join(root, PINNED_CLI);
61
- if (!existsSync(pinned)) throw new Error(`@rightkit/release is not installed at ${pinned}; add it as a devDependency and run pnpm install first`);
62
39
  const hooksDir = hooksDirectoryFor(root);
40
+ const result = retireHooksIn(hooksDir, FS_OPS, { scripts: false });
63
41
  const file = path.join(hooksDir, "pre-commit");
64
- const foreign = path.join(hooksDir, FOREIGN);
65
- const body = renderPreCommitHook();
66
- mkdirSync(hooksDir, { recursive: true });
67
- let status = "installed";
68
- if (lstatSync(file, { throwIfNoEntry: false })) {
69
- const current = readFileSync(file, "utf8");
70
- if (current === body) return { status: "unchanged", file };
71
- if (current.includes(HOOK_MARKER)) status = "updated";
72
- else {
73
- if (existsSync(path.join(hooksDir, FOREIGN))) throw new Error(`${FOREIGN} already exists; resolve the chained hook manually before installing`);
74
- renameSync(file, foreign);
75
- status = "chained";
76
- }
77
- }
78
- const temporary = `${file}.${process.pid}.tmp`;
79
- writeFileSync(temporary, body, { mode: 0o755 });
80
- chmodSync(temporary, 0o755);
81
- renameSync(temporary, file);
82
- return { status, file };
42
+ return { status: result.restored.length ? "restored" : result.removed.includes("pre-commit") ? "retired" : "absent", file };
83
43
  }
84
44
 
85
45
  export function main(args, { print = (line) => console.log(line) } = {}) {
86
46
  const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
87
47
  if (args[0] !== "install") {
88
- print("right-release hooks install [--app <dir>]");
48
+ print("right-release hooks install [--app <dir>] (retires the ownership pre-commit hook; no hook is written)");
89
49
  return args.includes("--help") ? 0 : 2;
90
50
  }
91
51
  try {
92
- const result = installPreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
93
- print(`[right-release] pre-commit ownership gate ${result.status}: ${result.file}`);
94
- if (result.status === "chained") print(`[right-release] previous pre-commit preserved as ${FOREIGN} and runs first`);
52
+ const result = retirePreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
53
+ print(`[right-release] pre-commit ownership gate retired (${result.status}): ${result.file}`);
54
+ if (result.status === "restored") print("[right-release] the previous pre-commit hook is back in place");
55
+ print("[right-release] commit runs no local hook check; `right-release ownership --staged` is still available by hand");
95
56
  return 0;
96
57
  } catch (error) {
97
58
  print(`[right-release] hooks install FAILED: ${error.message}`);
@@ -322,9 +322,22 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
322
322
  if (bytes && !contentDigests.some((item) => item.path === rel)) contentDigests.push({ path: rel, digest: sha256(bytes) });
323
323
  return bytes;
324
324
  };
325
+ // Directories the repository declares as gate fixtures (ci/gate.config.json `fixtures`: consumer recipes, vendored crates,
326
+ // spikes) are samples and harnesses, never the app's code: the gate builds none of them as units, and neither are they scanned here.
327
+ let fixtures = [];
328
+ try {
329
+ const rel = "ci/gate.config.json";
330
+ const config = inventory.snapshotFiles?.get(`${inventory.pathPrefix ?? ""}${rel}`)?.bytes ?? files.get(rel)?.bytes
331
+ ?? (!inventory.committed && existsSync(path.resolve(appRoot, rel)) ? readFileSync(path.resolve(appRoot, rel)) : undefined);
332
+ if (config) fixtures = (JSON.parse(config.toString("utf8")).fixtures ?? []).map((d) => String(d).replace(/\/+$/, ""));
333
+ } catch { fixtures = []; }
334
+ const underFixture = (rel) => fixtures.some((d) => rel === d || rel.startsWith(`${d}/`));
325
335
  for (const manifest of manifests) {
326
336
  const base = path.posix.basename(manifest.rel), ecosystem = base === "Cargo.toml" ? "cargo" : base === "package.json" ? "npm" : null;
327
337
  if (!ecosystem || !manifest.bytes) continue;
338
+ if (underFixture(manifest.rel)) continue;
339
+ // A Cargo manifest with its own [workspace] table is a workspace root: its lock sits beside it and Cargo never looks higher.
340
+ const workspaceRoot = ecosystem === "cargo" && /^\s*\[workspace\]\s*$/m.test(manifest.bytes.toString("utf8"));
328
341
  const names = ecosystem === "cargo" ? ["Cargo.lock"] : OWNERSHIP_LOCKFILES.slice(1);
329
342
  let dir = path.posix.dirname(manifest.rel), found = false;
330
343
  try {
@@ -337,7 +350,7 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
337
350
  }
338
351
  found = true; break;
339
352
  }
340
- if (dir === ".") break;
353
+ if (dir === "." || workspaceRoot) break;
341
354
  dir = path.posix.dirname(dir);
342
355
  }
343
356
  if (!found) diagnostic(manifest.rel, "lockfile-missing", ecosystem);
@@ -17,10 +17,11 @@ const CATALOG_FILE = "rightkit-ownership.json";
17
17
  const REGISTRY_FILE = "ownership-repositories.json";
18
18
  // Step 4: the release QA requirement floor is protected like the catalog. Dropping a required check changes the digest.
19
19
  export const QA_FLOOR_FILE = "release-qa-floor.json";
20
- // Modules that make or carry enforcement decisions. Their import closure is pinned.
20
+ // Modules that make or carry enforcement decisions. Their import closure is pinned. ownership-hooks.mjs left the list with the
21
+ // advisory pre-commit hook it used to write (GATE-CONTRACT: commit runs no local hook check): it retires that hook and decides nothing.
21
22
  export const POLICY_BUNDLE_ENTRYPOINTS = Object.freeze([
22
23
  "ownership-scan-worker.mjs", "ownership-evidence.mjs", "ownership-verdict.mjs", "ownership-admission.mjs",
23
- "ownership-staged.mjs", "ownership-hooks.mjs", "ownership-key-trust.mjs", "release-qa-receipt.mjs",
24
+ "ownership-staged.mjs", "ownership-key-trust.mjs", "release-qa-receipt.mjs",
24
25
  ]);
25
26
  // Data files are pinned through their own digests, not as source files.
26
27
  const DATA_FILES = new Set([CATALOG_FILE, REGISTRY_FILE, ALLOWLIST_FILE, QA_FLOOR_FILE]);
@@ -1,4 +1,5 @@
1
- // Advisory pre-commit ownership gate: scans the STAGED tree (index content), never the working tree.
1
+ // Ownership gate for the STAGED tree (index content), never the working tree: the manual command `right-release ownership --staged`.
2
+ // No git hook runs it any more (docs/GATE-CONTRACT.md: commit runs no local hook check).
2
3
  // Authoritative enforcement stays in broker admission and the release gate.
3
4
  //
4
5
  // right-release ownership --staged [--app <dir>] [--json]
package/ownership.mjs CHANGED
@@ -382,6 +382,9 @@ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDi
382
382
  const refs = lock.package.filter((p) => p.name === owner).flatMap((p) => p.dependencies ?? []);
383
383
  candidates = candidates.filter((p) => refs.some((ref) => ref === `${d.name} ${p.version}` || ref.startsWith(`${d.name} ${p.version} (`)));
384
384
  }
385
+ // A crate may declare two versions of one package under different names (windows 0.61 and 0.62 lanes): the declared
386
+ // requirement picks the resolved one, by Cargo's default caret rule (or an exact `=` pin).
387
+ if (candidates.length > 1 && typeof d.requirement === "string") candidates = candidates.filter((p) => cargoRequirementMatches(d.requirement, p.version));
385
388
  if (candidates.length !== 1 || typeof candidates[0].version !== "string") throw new Error(`Cargo.lock resolution missing or ambiguous: ${d.name}`);
386
389
  d.resolvedVersion = candidates[0].version;
387
390
  }
@@ -917,3 +920,24 @@ export function checkCommittedApp(appRoot, manifest, options = {}) {
917
920
  export function checkCommittedAppForTest(appRoot, manifest, options = {}) {
918
921
  return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource, options.readAuthorityForTest);
919
922
  }
923
+
924
+ /** Cargo's default requirement (`0.61`, `^1.2`, `=0.3.4`, `>=2.10.1, <3`) against a resolved version; anything else matches. */
925
+ function cargoRequirementMatches(requirement, version) {
926
+ const parse = (v) => String(v).split("+")[0].split("-")[0].split(".").map((n) => Number(n));
927
+ const [x, y = 0, z = 0] = parse(version);
928
+ const cmp = (a, b) => a[0] - b[0] || (a[1] ?? 0) - (b[1] ?? 0) || (a[2] ?? 0) - (b[2] ?? 0);
929
+ return requirement.split(",").map((r) => r.trim()).filter(Boolean).every((r) => {
930
+ const m = /^(=|\^|>=|<=|>|<|~)?\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(r);
931
+ if (!m) return true;
932
+ const op = m[1] ?? "^"; const want = [Number(m[2]), m[3] === undefined ? undefined : Number(m[3]), m[4] === undefined ? undefined : Number(m[4])];
933
+ const v = [x, y, z]; const c = cmp(v, want);
934
+ if (op === "=") return want.every((n, i) => n === undefined || n === v[i]);
935
+ if (op === ">=") return c >= 0; if (op === ">") return c > 0; if (op === "<=") return c <= 0; if (op === "<") return c < 0;
936
+ if (op === "~") return c >= 0 && v[0] === want[0] && (want[1] === undefined || v[1] === want[1]);
937
+ if (c < 0) return false;
938
+ if (want[0] > 0) return v[0] === want[0];
939
+ if (want[1] === undefined) return v[0] === 0;
940
+ if (want[1] > 0) return v[0] === 0 && v[1] === want[1];
941
+ return v[0] === 0 && v[1] === 0 && (want[2] === undefined || v[2] === want[2]);
942
+ });
943
+ }
@@ -8,7 +8,9 @@ import { existsSync } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
11
+ import { ArtifactPathError, SPLIT_TARGET_UNRESOLVED_CODE, isSplitArtifactEnv } from "./release-artifact-path.mjs";
11
12
  import { defaultManagedRoots, isUnderManagedRoot } from "./managed-target-stage.mjs";
13
+ import { NativeHelpersError, nativeHelperNestedCode, validateNativeHelpers } from "./native-helpers.mjs";
12
14
  import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
13
15
  import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
14
16
  import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
@@ -78,13 +80,13 @@ const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config",
78
80
  * release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
79
81
  * the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
80
82
  */
81
- function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs }) {
83
+ function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs, verifySidecars, nativeHelpers, boundTargets }) {
82
84
  const steps = [
83
85
  ...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
84
86
  { phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
85
87
  ...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
86
88
  ];
87
- const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}) };
89
+ const plan = { version: 1, steps, ...(boundTargets?.length ? { boundTargets } : {}), ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
88
90
  return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
89
91
  }
90
92
 
@@ -113,6 +115,9 @@ function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, m
113
115
  function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots, staged }) {
114
116
  const explicit = spec.cargoTargetRoot;
115
117
  if (explicit !== undefined && explicit !== true && explicit !== false && typeof explicit !== "string") fail(`${label}.cargoTargetRoot must be true or an app-relative Cargo.toml path`);
118
+ // A door split (row 7.04c, R-T1/R-T3): the snapshot has no target directory, so every declared target path is the bound target's, absolute, and
119
+ // nothing is copied (the #924 bridge plans zero staged entries): the build output is read and signed in place.
120
+ if (isSplitArtifactEnv(env)) return splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label });
116
121
  const envDir = typeof env.CARGO_TARGET_DIR === "string" && env.CARGO_TARGET_DIR.trim() !== "";
117
122
  if (explicit === false || (!explicit && !envDir)) return (declared) => declared;
118
123
  const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
@@ -140,9 +145,34 @@ function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, lab
140
145
  };
141
146
  }
142
147
 
143
- function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability, stage }) {
148
+ function splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label }) {
149
+ const explicit = spec.cargoTargetRoot;
150
+ const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
151
+ const manifest = path.resolve(appRoot, manifestRel);
152
+ // An app that declares no recipe target and has no src-tauri manifest has nothing to bind; a declared one that is missing is refused below.
153
+ if (explicit !== true && typeof explicit !== "string" && !existsSync(manifest)) return (declared) => declared;
154
+ let targetDir;
155
+ try { targetDir = resolveTargetRoot(manifest); } catch (error) {
156
+ throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: cannot resolve the Cargo target directory of ${manifest} (${error.message}); a split never falls back to a snapshot-relative target/`);
157
+ }
158
+ const relativeToApp = path.relative(appRoot, targetDir);
159
+ if (relativeToApp === "" || (!relativeToApp.startsWith("..") && !path.isAbsolute(relativeToApp))) {
160
+ throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: the Cargo target directory ${targetDir} is inside the snapshot; a split binds it outside (CARGO_TARGET_DIR, row 7.09)`);
161
+ }
162
+ const prefix = path.posix.join(path.posix.dirname(manifestRel).replace(/^\.$/, ""), "target");
163
+ const mapper = (declared) => {
164
+ const normalized = String(declared).replaceAll("\\", "/");
165
+ if (normalized !== prefix && !normalized.startsWith(`${prefix}/`)) return declared;
166
+ if (normalized.split("/").includes("..")) fail(`${label}: ${declared} must not contain ..`);
167
+ return path.resolve(targetDir, normalized.slice(prefix.length + 1));
168
+ };
169
+ mapper.bound = [targetDir];
170
+ return mapper;
171
+ }
172
+
173
+ function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability, stage, boundTargets }) {
144
174
  const args = ["exec", "tauri", "build", "--no-bundle", "--target", triple, ...configArgs(extras, appRoot, cwd), ...buildCargoArgs(extras, jobs)];
145
- return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs });
175
+ return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs, boundTargets });
146
176
  }
147
177
 
148
178
  /** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
@@ -192,14 +222,16 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
192
222
  return {
193
223
  ...rest,
194
224
  signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
195
- prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage }),
225
+ prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage, boundTargets: mapPath.bound }),
196
226
  package: bundleCommand,
197
227
  sign: { ...(target.sign ?? {}), prePackageFiles: [rawExe], files: [installerFile] },
198
228
  recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
199
229
  };
200
230
  }
201
231
 
202
- const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater"]);
232
+ const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater", "stageSidecars", "nativeHelpers"]);
233
+ const STAGE_SIDECARS_KEYS = new Set(["config", "receipt"]);
234
+ const DEFAULT_SIDECAR_RECEIPT = "src-tauri/binaries/stage-sidecar.json";
203
235
  const MAC_TRIPLES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
204
236
  const MAC_CLEANUP_KEYS = new Set(["staleApps", "staleDmgVolumes", "lsclean"]);
205
237
 
@@ -221,7 +253,26 @@ function macCleanupSpec(value) {
221
253
  * `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
222
254
  * it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
223
255
  */
224
- export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
256
+ /**
257
+ * `stageSidecars: true | { config?, receipt? }`: stage the release config's declarative `sidecars` for the build's triple through this
258
+ * package's own `right-release stage-sidecar --target <triple>` as the first prepare step. For `universal-apple-darwin` stage-sidecar
259
+ * builds each slice (aarch64 and x86_64), `lipo -create`s them into `<name>-universal-apple-darwin` (the name Tauri's universal
260
+ * build expects) and refuses a result without both archs; recipe-build then re-checks every externalBin in the receipt
261
+ * (`receipt`, default src-tauri/binaries/stage-sidecar.json) with `lipo -archs` (exactly arm64 + x86_64) before the Tauri build.
262
+ */
263
+ function stageSidecarsSpec(value) {
264
+ if (value === undefined || value === false) return undefined;
265
+ if (value !== true && !isObject(value)) fail("targets.mac.macPackage.stageSidecars must be true or { config?, receipt? }");
266
+ const spec = value === true ? {} : value;
267
+ const unknownKeys = Object.keys(spec).filter((key) => !STAGE_SIDECARS_KEYS.has(key));
268
+ if (unknownKeys.length) fail(`targets.mac.macPackage.stageSidecars has unknown key(s): ${unknownKeys.join(", ")} (allowed: ${[...STAGE_SIDECARS_KEYS].join(", ")})`);
269
+ return {
270
+ ...(spec.config === undefined ? {} : { config: relativeInside(spec.config, "targets.mac.macPackage.stageSidecars.config") }),
271
+ receipt: spec.receipt === undefined ? DEFAULT_SIDECAR_RECEIPT : relativeInside(spec.receipt, "targets.mac.macPackage.stageSidecars.receipt"),
272
+ };
273
+ }
274
+
275
+ export function expandMacPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
225
276
  if (!target?.macPackage) return withoutRecipe(target);
226
277
  const spec = target.macPackage;
227
278
  if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
@@ -247,6 +298,25 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
247
298
  }
248
299
  const override = { bundle: { createUpdaterArtifacts: false, macOS: { signingIdentity: null } } };
249
300
  const extras = buildExtras(spec, "targets.mac.macPackage", { postBuild: true });
301
+ const sidecars = stageSidecarsSpec(spec.stageSidecars);
302
+ if (sidecars) {
303
+ // First prepare step: stage the declared sidecars for this triple (universal: both slices, lipo, <name>-universal-apple-darwin).
304
+ extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
305
+ }
306
+ let helpers;
307
+ if (spec.nativeHelpers !== undefined) {
308
+ try { helpers = validateNativeHelpers(spec.nativeHelpers, { label: "targets.mac.macPackage.nativeHelpers" }); } catch (error) {
309
+ if (error instanceof NativeHelpersError) fail(error.message);
310
+ throw error;
311
+ }
312
+ // A universal app may not carry a thin helper: it would compile, sign and notarize and then fail on half the Macs.
313
+ const thin = triple === "universal-apple-darwin" ? helpers.filter((helper) => !helper.universal) : [];
314
+ if (thin.length) fail(`targets.mac.macPackage.nativeHelpers: ${thin.map((helper) => helper.name).join(", ")} must declare universal: true (this build's triple is universal-apple-darwin)`);
315
+ if (helpers.some((helper) => helper.kind === "app") && (typeof config?.version !== "string" || !/^\d+(\.\d+){0,2}$/.test(config.version))) fail("targets.mac.macPackage.nativeHelpers: a kind \"app\" helper needs the release config version (1-3 numeric parts) for its Info.plist");
316
+ const collisions = Object.keys(nativeHelperNestedCode(helpers)).filter((key) => isObject(target.sign?.nestedCode) && Object.hasOwn(target.sign.nestedCode, key));
317
+ if (collisions.length) fail(`targets.mac.macPackage.nativeHelpers: sign.nestedCode must not redeclare ${collisions.join(", ")}; declare entitlements on the helper entry`);
318
+ if (isObject(target.sign?.entitlements) ? helpers.some((helper) => Object.hasOwn(target.sign.entitlements, helper.embed)) : false) fail("targets.mac.macPackage.nativeHelpers: sign.entitlements must not redeclare a helper's embed path; declare entitlements on the helper entry");
319
+ }
250
320
  const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", ...configArgs(extras, appRoot, cwd), "--config", JSON.stringify(override)];
251
321
  if (spec.features?.length) args.push("--features", spec.features.join(","));
252
322
  args.push(...profileArgs(extras));
@@ -270,7 +340,10 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
270
340
  // An entry inside another staged entry (a file within the .app) is covered by copying the outer one.
271
341
  const macEntries = [...mapPath.staged.values()].filter((entry) => ![...mapPath.staged.values()].some((outer) => outer !== entry && entry.to.startsWith(`${outer.to}/`)));
272
342
  const macStage = macEntries.length ? { post: macEntries.map((entry) => ({ ...entry, fresh: true })), resolve: mapPath.resolve() } : undefined;
273
- const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage;
343
+ const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage || helpers;
344
+ const forceArch = helpers && triple !== "universal-apple-darwin" ? (triple === "aarch64-apple-darwin" ? "arm64" : "x86_64") : undefined;
345
+ const nativeHelpersPlan = helpers ? { app, helpers, ...(config?.version ? { version: config.version } : {}), ...(forceArch ? { forceArch } : {}), configuration: "release" } : undefined;
346
+ const verifySidecars = sidecars && triple === "universal-apple-darwin" ? { receipt: sidecars.receipt, target: triple } : undefined;
274
347
  const { macPackage, recipe: _ignoredMac, ...rest } = target;
275
348
  // The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
276
349
  const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
@@ -283,8 +356,9 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
283
356
  return {
284
357
  ...rest,
285
358
  ...postSign,
286
- prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
287
- sign: { ...(target.sign ?? {}), prePackageFiles },
359
+ prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage, verifySidecars, nativeHelpers: nativeHelpersPlan, boundTargets: mapPath.bound }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
360
+ // Helpers are signed by release.mjs inside-out through sign.nestedCode (deepest path first, hardened runtime, each helper's own entitlements), then the app.
361
+ sign: { ...(target.sign ?? {}), ...(helpers ? { nestedCode: { ...nativeHelperNestedCode(helpers), ...(target.sign?.nestedCode ?? {}) } } : {}), prePackageFiles },
288
362
  notarize: { ...target.notarize, app, appBeforePackage: true },
289
363
  dmg: isObject(target.dmg) ? target.dmg : {},
290
364
  recipe: { kind: "macPackage", universal: triple === "universal-apple-darwin" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.8",
3
+ "version": "0.6.11",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -78,7 +78,8 @@
78
78
  "!*.test.mjs",
79
79
  "!cli/*.test.mjs",
80
80
  "!standalone-clone-evidence.json",
81
- "!refusals.json"
81
+ "!refusals.json",
82
+ "!provenance-real-receipt.fixture.json"
82
83
  ],
83
84
  "sideEffects": false,
84
85
  "publishConfig": {
@@ -91,7 +92,7 @@
91
92
  "directory": "packages/release"
92
93
  },
93
94
  "dependencies": {
94
- "@rightkit/outdated": "0.2.0",
95
+ "@rightkit/outdated": "0.3.0",
95
96
  "smol-toml": "1.9.1"
96
97
  },
97
98
  "scripts": {
@@ -239,13 +239,13 @@ function admitRuntime(bin, rest, options, label) {
239
239
  }
240
240
 
241
241
  /**
242
- * `rightkit-gate install-hook`: writes the committed pre-push hook and sets core.hooksPath, so an app's `prepare` script may run it.
243
- * It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is run by the hook, never
244
- * by a release config; only the installer and the version probe are admitted.
242
+ * `rightkit-gate install-hook`: retires the git hooks an earlier release wrote and registers the agent hook, so an app's `prepare` script may run it.
243
+ * It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is never run by a release
244
+ * config; only the installer and the version probe are admitted.
245
245
  */
246
246
  function admitRightKitGate(args, label) {
247
247
  if (args.length === 1 && (args[0] === "install-hook" || args[0] === "--version")) return admit(`rightkit-gate-${args[0].replace(/^--/, "")}`, "owner");
248
- return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself runs from the pre-push hook`);
248
+ return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself is run through the door, never by a release config`);
249
249
  }
250
250
 
251
251
  function admitRightRelease(args, label) {