@kungfu-tech/buildchain 3.0.8 → 3.0.9-alpha.1

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 (79) hide show
  1. package/actions/promote-buildchain-ref/README.md +8 -7
  2. package/bin/buildchain.mjs +34 -33
  3. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  4. package/contracts/fixtures/kfd-adopter-release-v1/kfd-4-perspective.json +16 -0
  5. package/contracts/fixtures/kfd-adopter-release-v1/kfd-4-replay.json +57 -0
  6. package/contracts/release-cut-v1.schema.json +1 -0
  7. package/contracts/release-train-transition-v1.schema.json +1 -0
  8. package/dist/site/agent-index.json +0 -1
  9. package/dist/site/artifact-schemas.json +0 -2
  10. package/dist/site/badge-endpoint-registry.json +9 -9
  11. package/dist/site/badges/v1/kfd-8/aligned.json +1 -1
  12. package/dist/site/badges/v1/kfd-8/declared.json +1 -1
  13. package/dist/site/badges/v1/kfd-8/downgraded.json +1 -1
  14. package/dist/site/badges/v1/kfd-8/draft.json +1 -1
  15. package/dist/site/badges/v1/kfd-8/failed.json +1 -1
  16. package/dist/site/badges/v1/kfd-8/missing.json +1 -1
  17. package/dist/site/badges/v1/kfd-8/passed.json +1 -1
  18. package/dist/site/badges/v1/kfd-8/planned.json +1 -1
  19. package/dist/site/buildchain-contract.json +38 -34
  20. package/dist/site/buildchain-site.json +59 -39
  21. package/dist/site/capability-registry.json +1 -1
  22. package/dist/site/cli-registry.json +8 -7
  23. package/dist/site/controller-registry.json +13 -5
  24. package/dist/site/kfd-claims.json +35 -8
  25. package/dist/site/kfd-upstream-aggregate.json +10 -10
  26. package/dist/site/manual-registry.json +9 -9
  27. package/dist/site/node-api-registry.json +1143 -320
  28. package/dist/site/page-registry.json +46 -26
  29. package/dist/site/public-surface-audit.json +19 -9
  30. package/dist/site/publication-registry.json +4 -4
  31. package/dist/site/release-passport-check-manifest.json +33 -5
  32. package/dist/site/release-provenance.json +2 -1
  33. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  34. package/dist/site/site-manifest.json +13 -13
  35. package/dist/site/workflow-registry.json +19 -9
  36. package/docs/aws-us-elastic-runner-burst-plane.md +15 -3
  37. package/docs/cli-reference.md +6 -6
  38. package/docs/cli.md +2 -3
  39. package/docs/dev-alpha-candidate-patrol.md +45 -20
  40. package/docs/dev-delivery-warrant.md +12 -2
  41. package/docs/kfd-support.md +7 -7
  42. package/docs/node-api-reference.md +295 -246
  43. package/docs/release-passport.md +25 -12
  44. package/docs/release-train.md +54 -4
  45. package/docs/reusable-build-surface.md +26 -9
  46. package/docs/runtime-train-validation.md +6 -0
  47. package/docs/versioning.md +3 -2
  48. package/package.json +4 -3
  49. package/packages/core/artifact-verification-envelope.js +114 -0
  50. package/packages/core/buildchain-channel-identity.js +119 -0
  51. package/packages/core/buildchain-compatibility-proof.js +32 -0
  52. package/packages/core/buildchain-contract.js +7 -0
  53. package/packages/core/controller-evidence.js +2 -1
  54. package/packages/core/dev-alpha-active-release-train.js +460 -0
  55. package/packages/core/dev-alpha-candidate-selection.js +181 -0
  56. package/packages/core/dev-delivery-warrant.js +15 -4
  57. package/packages/core/index.js +4 -5
  58. package/packages/core/kfd-adopter-manifest.js +390 -0
  59. package/packages/core/kfd-product-gates.js +0 -300
  60. package/packages/core/release-blocker-priority.js +192 -0
  61. package/packages/core/release-passport-contract.js +219 -6
  62. package/packages/core/release-passport.js +139 -242
  63. package/packages/core/release-train.js +220 -0
  64. package/scripts/aws-macos-jit-controller-core.mjs +76 -0
  65. package/scripts/aws-macos-jit-controller.mjs +29 -0
  66. package/scripts/aws-macos-jit-source-rebind.mjs +503 -0
  67. package/scripts/buildchain-channel-router.mjs +18 -2
  68. package/scripts/buildchain-cli-help.mjs +4 -2
  69. package/scripts/buildchain-contract-lock.mjs +11 -1
  70. package/scripts/check-inventory.mjs +4 -4
  71. package/scripts/dev-alpha-candidate-patrol.mjs +196 -201
  72. package/scripts/generate-buildchain-kfd-witnesses.mjs +88 -94
  73. package/scripts/generate-channel-build-workflow.mjs +78 -31
  74. package/scripts/generate-site-bundle.mjs +1 -8
  75. package/scripts/promotion-channel-router.mjs +5 -3
  76. package/scripts/release-train-self-dogfood.mjs +579 -0
  77. package/scripts/runtime-ref-core.mjs +4 -17
  78. package/scripts/site-capability-metadata.mjs +3 -1
  79. package/dist/site/schemas/kfd-support-projection-v1.schema.json +0 -106
@@ -1,121 +1,115 @@
1
1
  #!/usr/bin/env node
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
- import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import path from "node:path";
5
7
  import { pathToFileURL } from "node:url";
6
- import {
7
- createBuildchainKfd1Witness,
8
- createBuildchainKfd2Claims,
9
- createBuildchainKfd3ArtifactWitness,
10
- createBuildchainKfd3PrebuildWitness,
11
- } from "../packages/core/buildchain-kfd-claims.js";
12
- import {
13
- BUILDCHAIN_KFD1_CONTRACT_WORLD_WITNESS_PATH,
14
- BUILDCHAIN_KFD2_CLAIMS_DIR,
15
- BUILDCHAIN_KFD3_ARTIFACT_WITNESS_PATH,
16
- BUILDCHAIN_KFD3_PREBUILD_WITNESS_PATH,
17
- } from "../packages/core/buildchain-layout.js";
8
+ import { addAdopterWitness, initAdopterManifest } from "@kungfu-tech/kfd/adopter-conformance/toolchain";
9
+ import kfdStandards from "@kungfu-tech/kfd/standards.json" with { type: "json" };
10
+ import { installedKfdPackageArtifactRoot } from "../packages/core/artifact-verification-envelope.js";
11
+ import { createKfdAdopterManifestGate, createKfdLegacySupportMatrixProjection, validateKfdAdopterManifestGate, validateKfdLegacySupportMatrixProjection } from "../packages/core/kfd-adopter-manifest.js";
12
+ import { KFD_PRODUCT_GATE_INPUT_CONTRACT, evaluateKfdProductGate, kfdProductGateDigest, validateKfdProductGateResult } from "../packages/core/kfd-product-gates.js";
13
+ import { createBuildchainKfd1Witness, createBuildchainKfd2Claims, createBuildchainKfd3ArtifactWitness, createBuildchainKfd3PrebuildWitness } from "../packages/core/buildchain-kfd-claims.js";
14
+ import { BUILDCHAIN_KFD1_CONTRACT_WORLD_WITNESS_PATH, BUILDCHAIN_KFD2_CLAIMS_DIR, BUILDCHAIN_KFD3_ARTIFACT_WITNESS_PATH, BUILDCHAIN_KFD3_PREBUILD_WITNESS_PATH } from "../packages/core/buildchain-layout.js";
18
15
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
19
-
16
+ const REPOSITORY = "kungfu-systems/buildchain", require = createRequire(import.meta.url), kfdRoot = path.dirname(require.resolve("@kungfu-tech/kfd/package.json"));
17
+ const evidenceSources = { "KFD-1": "dist/site/release-passport-check-manifest.json", "KFD-2": "dist/site/kfd-claims.json", "KFD-3": "dist/site/public-surface-audit.json", "KFD-4": "packages/core/kfd-product-gates.js", "KFD-5": "packages/core/kfd-product-gates.js", "KFD-7": "packages/core/kfd-product-gates.js", "KFD-10": "packages/core/dev-delivery-warrant.js" };
20
18
  function parseArgs(argv = process.argv.slice(2)) {
21
- const args = {
22
- cwd: process.cwd(),
23
- outputDir: ".buildchain/kfd",
24
- sourceSha: process.env.BUILDCHAIN_SOURCE_SHA || "",
25
- };
19
+ const args = { cwd: process.cwd(), outputDir: ".buildchain/kfd", sourceSha: process.env.BUILDCHAIN_SOURCE_SHA || "" };
26
20
  for (let index = 0; index < argv.length; index += 1) {
27
21
  const arg = argv[index];
28
- if (arg === "--cwd") {
29
- args.cwd = argv[++index] || args.cwd;
30
- } else if (arg === "--output-dir") {
31
- args.outputDir = argv[++index] || args.outputDir;
32
- } else if (arg === "--source-sha") {
33
- args.sourceSha = argv[++index] || args.sourceSha;
34
- } else if (arg === "--help") {
35
- args.help = true;
36
- } else {
37
- throw new Error(`unknown argument: ${arg}`);
38
- }
22
+ if (arg === "--cwd") args.cwd = argv[++index] || args.cwd;
23
+ else if (arg === "--output-dir") args.outputDir = argv[++index] || args.outputDir;
24
+ else if (arg === "--source-sha") args.sourceSha = argv[++index] || args.sourceSha;
25
+ else if (arg === "--help") args.help = true;
26
+ else throw new Error(`unknown argument: ${arg}`);
39
27
  }
40
28
  return args;
41
29
  }
42
-
43
30
  function gitSha(cwd) {
44
- try {
45
- return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
46
- } catch {
47
- return "";
48
- }
31
+ try { return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(); } catch { return ""; }
49
32
  }
50
-
51
- function writeJson(filePath, value) {
33
+ function digest(bytes) { return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`; }
34
+ function fileDigest(filePath) { return digest(fs.readFileSync(filePath)); }
35
+ function relative(root, filePath) { return path.relative(root, filePath).replace(/\\/g, "/"); }
36
+ function writeJson(root, relativePath, value) {
37
+ const filePath = path.join(root, relativePath);
52
38
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
39
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
40
+ return { path: relativePath, sha256: fileDigest(filePath), filePath };
54
41
  }
55
-
56
- function toRepoRelative(cwd, filePath) {
57
- return path.relative(cwd, filePath).replace(/\\/g, "/");
58
- }
59
-
60
- export function generateBuildchainKfdWitnesses({
61
- cwd = process.cwd(),
62
- outputDir = ".buildchain/kfd",
63
- sourceSha = "",
64
- emitOutputs = true,
65
- } = {}) {
66
- const root = path.resolve(cwd);
67
- const outDir = path.resolve(root, outputDir);
68
- const resolvedSourceSha = sourceSha || gitSha(root);
42
+ function readJson(filePath, sourceSha = "") { return JSON.parse(fs.readFileSync(filePath, "utf8").replaceAll("{{SOURCE_SHA}}", sourceSha)); }
43
+ export function generateBuildchainKfdWitnesses({ cwd = process.cwd(), outputDir = ".buildchain/kfd", sourceSha = "", emitOutputs = true } = {}) {
44
+ const root = path.resolve(cwd), outDir = path.resolve(root, outputDir), resolvedSourceSha = sourceSha || gitSha(root);
69
45
  const outputPath = (canonicalPath) => path.join(outDir, path.relative(".buildchain/kfd", canonicalPath));
70
- const paths = {
71
- kfd1Witness: outputPath(BUILDCHAIN_KFD1_CONTRACT_WORLD_WITNESS_PATH),
72
- kfd3PrebuildWitness: outputPath(BUILDCHAIN_KFD3_PREBUILD_WITNESS_PATH),
73
- kfd3ArtifactWitness: outputPath(BUILDCHAIN_KFD3_ARTIFACT_WITNESS_PATH),
74
- kfd2ClaimsDir: outputPath(BUILDCHAIN_KFD2_CLAIMS_DIR),
75
- };
76
- writeJson(paths.kfd1Witness, createBuildchainKfd1Witness({ root, sourceSha: resolvedSourceSha }));
77
- writeJson(paths.kfd3PrebuildWitness, createBuildchainKfd3PrebuildWitness({ root, sourceSha: resolvedSourceSha }));
78
- writeJson(paths.kfd3ArtifactWitness, createBuildchainKfd3ArtifactWitness({ root, sourceSha: resolvedSourceSha }));
79
- const witnessFiles = {
80
- "kfd-1-witness": toRepoRelative(root, paths.kfd1Witness),
81
- "kfd-3-prebuild-witness": toRepoRelative(root, paths.kfd3PrebuildWitness),
82
- "kfd-3-artifact-witness": toRepoRelative(root, paths.kfd3ArtifactWitness),
83
- };
46
+ const paths = { kfd1Witness: outputPath(BUILDCHAIN_KFD1_CONTRACT_WORLD_WITNESS_PATH), kfd3PrebuildWitness: outputPath(BUILDCHAIN_KFD3_PREBUILD_WITNESS_PATH), kfd3ArtifactWitness: outputPath(BUILDCHAIN_KFD3_ARTIFACT_WITNESS_PATH), kfd2ClaimsDir: outputPath(BUILDCHAIN_KFD2_CLAIMS_DIR) };
47
+ writeJson(path.dirname(paths.kfd1Witness), path.basename(paths.kfd1Witness), createBuildchainKfd1Witness({ root, sourceSha: resolvedSourceSha }));
48
+ writeJson(path.dirname(paths.kfd3PrebuildWitness), path.basename(paths.kfd3PrebuildWitness), createBuildchainKfd3PrebuildWitness({ root, sourceSha: resolvedSourceSha }));
49
+ writeJson(path.dirname(paths.kfd3ArtifactWitness), path.basename(paths.kfd3ArtifactWitness), createBuildchainKfd3ArtifactWitness({ root, sourceSha: resolvedSourceSha }));
50
+ const witnessFiles = { "kfd-1-witness": relative(root, paths.kfd1Witness), "kfd-3-prebuild-witness": relative(root, paths.kfd3PrebuildWitness), "kfd-3-artifact-witness": relative(root, paths.kfd3ArtifactWitness) };
84
51
  const kfd2ClaimPaths = createBuildchainKfd2Claims({ root, witnessFiles }).map((claim) => {
85
- const slug = String(claim.id || "claim")
86
- .replace(/^claim:/, "")
87
- .replace(/[^0-9A-Za-z._-]+/g, "-")
88
- .replace(/^-+|-+$/g, "") || "claim";
89
- const claimPath = path.join(paths.kfd2ClaimsDir, `${slug}.json`);
90
- writeJson(claimPath, claim);
91
- return claimPath;
52
+ const slug = String(claim.id || "claim").replace(/^claim:/, "").replace(/[^0-9A-Za-z._-]+/g, "-").replace(/^-+|-+$/g, "") || "claim";
53
+ return writeJson(paths.kfd2ClaimsDir, `${slug}.json`, claim).filePath;
92
54
  });
93
- const outputs = {
94
- "kfd-1-witness-jsons": toRepoRelative(root, paths.kfd1Witness),
95
- "kfd-2-claim-jsons": kfd2ClaimPaths.map((claimPath) => toRepoRelative(root, claimPath)).join(","),
96
- "kfd-3-prebuild-witness-jsons": toRepoRelative(root, paths.kfd3PrebuildWitness),
97
- "kfd-3-artifact-witness-jsons": toRepoRelative(root, paths.kfd3ArtifactWitness),
98
- "source-sha": resolvedSourceSha,
99
- "output-dir": toRepoRelative(root, outDir),
100
- };
101
- if (emitOutputs) {
102
- writeGitHubOutputs(outputs);
55
+ const outputs = { "kfd-1-witness-jsons": relative(root, paths.kfd1Witness), "kfd-2-claim-jsons": kfd2ClaimPaths.map((filePath) => relative(root, filePath)).join(","), "kfd-3-prebuild-witness-jsons": relative(root, paths.kfd3PrebuildWitness), "kfd-3-artifact-witness-jsons": relative(root, paths.kfd3ArtifactWitness), "source-sha": resolvedSourceSha, "output-dir": relative(root, outDir) };
56
+ if (emitOutputs) writeGitHubOutputs(outputs);
57
+ return { schemaVersion: 1, contract: "kungfu-buildchain-self-kfd-witness-generation", outputs };
58
+ }
59
+ function productRecords(root, standard, sourceSha) {
60
+ if (standard === "kfd-4") return [
61
+ ["observer-perspective", readJson(path.join(root, "contracts/fixtures/kfd-adopter-release-v1/kfd-4-perspective.json"), sourceSha)],
62
+ ["perspective-replay", readJson(path.join(root, "contracts/fixtures/kfd-adopter-release-v1/kfd-4-replay.json"), sourceSha)],
63
+ ];
64
+ if (standard === "kfd-5") return [["primitive-discovery", readJson(path.join(kfdRoot, "cases/live/software-work-perspective-settlement/cuts/0001-assignment.json"))]];
65
+ const profile = readJson(path.join(kfdRoot, "verifier/fixtures/kfd-7/valid-domain-profile.json"));
66
+ profile.evidenceObligations = profile.evidenceObligations.map((entry) => ({ ...entry, status: "passed", artifactRefs: ["qualification-proof"], residualRisk: "Bound to the retained Buildchain product gate cut; independent certification remains external." }));
67
+ profile.activation = { decision: "activate", evidenceCut: `git://${REPOSITORY}@${sourceSha}`, independentReview: "review://protected-release-review", productWitnesses: ["qualification-proof"], residualRisk: "The product gate is non-qualifying and non-self-certifying." };
68
+ profile.domainProfile = { ...profile.domainProfile, product: "Buildchain", implementation: `git+https://github.com/${REPOSITORY}@${sourceSha}`, qualificationStatus: "qualified" };
69
+ return [["domain-profile", profile]];
70
+ }
71
+ async function generateProductGate(root, outDir, standard, sourceSha, checkedAt) {
72
+ const records = productRecords(root, standard, sourceSha).map(([role, value], index) => ({ role, ...writeJson(outDir, `records/${standard}-${index}.json`, value) }));
73
+ const kinds = standard === "kfd-4" ? [["projection-fsck", "projection-fsck"], ["negative", "negative-fixture"]] : standard === "kfd-5" ? [["negative", "negative-fixture"]] : [["qualification-proof", "qualification-proof"], ["independent-review", "independent-review"], ["negative", "negative-fixture"]];
74
+ const evidence = kinds.map(([id, kind]) => ({ id, kind, ...writeJson(outDir, `evidence/${standard}-${id}.json`, { schemaVersion: 1, contract: "kungfu-buildchain-kfd-adopter-evidence-reference", id, kind, source: { repository: REPOSITORY, sha: sourceSha }, observedAt: checkedAt, status: "retained", nonClaim: "This product-owned reference does not independently qualify or certify KFD adoption." }) }));
75
+ const input = { schemaVersion: 1, contract: KFD_PRODUCT_GATE_INPUT_CONTRACT, standard, standardRevision: kfdStandards.standards[standard].revision, source: { repository: REPOSITORY, sha: sourceSha }, evidenceCut: { generatedAt: checkedAt, expiresAt: new Date(Date.parse(checkedAt) + 86400000).toISOString() }, records: records.map(({ role, path, sha256 }) => ({ role, path, sha256 })), evidence: evidence.map(({ id, kind, path, sha256 }) => ({ id, kind, path, sha256 })), responsibility: { owner: REPOSITORY, evidenceOwner: "Buildchain maintainers", proofOwner: "Buildchain release gate" }, nonClaims: ["A passed product gate does not qualify, certify, activate, or independently approve KFD adoption."] };
76
+ const gate = await evaluateKfdProductGate({ cwd: outDir, input, expectedSourceSha: sourceSha, checkedAt });
77
+ if (gate.status !== "passed" || !validateKfdProductGateResult(gate, { expectedSourceSha: sourceSha, checkedAt }).valid) throw new Error(`${standard} product gate failed: ${JSON.stringify(gate.issues)}`);
78
+ writeJson(outDir, `${standard}/product-gate.json`, gate);
79
+ return gate;
80
+ }
81
+ export async function generateBuildchainKfdAdopterRelease({ cwd = process.cwd(), outputDir = ".buildchain/kfd/adopter-release", sourceSha = "", checkedAt = new Date().toISOString(), emitOutputs = true } = {}) {
82
+ const root = path.resolve(cwd), outDir = path.resolve(root, outputDir);
83
+ if (!/^[0-9a-f]{40}$/.test(sourceSha) || !Number.isFinite(Date.parse(checkedAt))) throw new Error("source SHA and checked-at must be exact");
84
+ for (const sourcePath of Object.values(evidenceSources)) if (!fs.statSync(path.join(root, sourcePath)).isFile()) throw new Error(`required Buildchain evidence source is missing: ${sourcePath}`);
85
+ const packageArtifactRoot = installedKfdPackageArtifactRoot(), gates = [];
86
+ for (const standard of ["kfd-4", "kfd-5", "kfd-7"]) gates.push(await generateProductGate(root, outDir, standard, sourceSha, checkedAt));
87
+ const sourceRoot = kfdProductGateDigest({ repository: REPOSITORY, sourceSha, files: [...new Set(Object.values(evidenceSources))].sort().map((sourcePath) => ({ path: sourcePath, sha256: fileDigest(path.join(root, sourcePath)) })) });
88
+ let manifest = initAdopterManifest({ manifestId: "buildchain-v3-full-cut", adopterId: REPOSITORY, artifactKind: "git-commit", artifactCoordinate: `${REPOSITORY}@${sourceSha}`, artifactRoot: sourceRoot, scope: "Buildchain v3 release and protected delivery authority", packageArtifactRoot, verifiedAt: checkedAt, maxAgeSeconds: 86400 });
89
+ const gateById = new Map(gates.map((gate) => [gate.standard.toUpperCase(), gate]));
90
+ for (const id of ["KFD-1", "KFD-2", "KFD-3", "KFD-4", "KFD-5", "KFD-7"]) {
91
+ const row = manifest.decisions.find((entry) => entry.id === id);
92
+ const sourcePath = evidenceSources[id];
93
+ const sourceEvidence = (kind, rootValue = fileDigest(path.join(root, sourcePath))) => ({ kind, coordinate: `git+https://github.com/${REPOSITORY}@${sourceSha}#${sourcePath}`, root: rootValue, observedAt: checkedAt, kfdPackageRoot: packageArtifactRoot });
94
+ row.state = "candidate"; row.usage = "used"; row.implementationEvidence = [sourceEvidence("implementation")]; row.verificationEvidence = [sourceEvidence("verification", gateById.get(id)?.gateRoot)]; row.gaps = ["Independent decision-specific assessment and certification remain external."];
103
95
  }
104
- return {
105
- schemaVersion: 1,
106
- contract: "kungfu-buildchain-self-kfd-witness-generation",
107
- outputs,
108
- };
96
+ const kfd6 = manifest.decisions.find((entry) => entry.id === "KFD-6");
97
+ kfd6.state = "unsupported"; kfd6.usage = "unused"; kfd6.gaps = ["Buildchain does not claim KFD-6 support in this cut."];
98
+ manifest = addAdopterWitness(manifest, { decisionId: "KFD-10", profileId: "kfd-warrant-evidence", witnessCoordinate: `git+https://github.com/${REPOSITORY}@${sourceSha}#${evidenceSources["KFD-10"]}`, witnessRoot: fileDigest(path.join(root, evidenceSources["KFD-10"])), packageArtifactRoot, verifiedAt: checkedAt, maxAgeSeconds: 86400 });
99
+ const manifestPath = writeJson(outDir, "kfd-adopter-manifest.json", manifest).filePath, manifestGate = createKfdAdopterManifestGate({ manifest, packageArtifactRoot, gateResults: gates, authorityPath: "kfd-adopter-manifest.json", expectedSourceSha: sourceSha, checkedAt });
100
+ if (!validateKfdAdopterManifestGate(manifestGate, { expectedSourceSha: sourceSha, checkedAt }).valid) throw new Error(`generated adopter manifest gate failed: ${JSON.stringify(manifestGate.issues)}`);
101
+ const gatePath = writeJson(outDir, "kfd-adopter-manifest-gate.json", manifestGate).filePath, support = createKfdLegacySupportMatrixProjection({ manifest, manifestGate });
102
+ if (!validateKfdLegacySupportMatrixProjection(support, { manifest, manifestGate }).valid) throw new Error("generated KFD support projection failed");
103
+ const supportPath = writeJson(outDir, "kfd-support.json", support).filePath;
104
+ const outputs = { "kfd-adopter-manifest-json": relative(root, manifestPath), "kfd-adopter-manifest-gate-json": relative(root, gatePath), "kfd-support-matrix-json": relative(root, supportPath), "kfd-product-gate-jsons": gates.map((gate) => relative(root, path.join(outDir, gate.standard, "product-gate.json"))).join(","), "kfd-adopter-manifest-root": manifestGate.authority.manifestRoot, "kfd-adopter-gate-root": manifestGate.gateRoot, "kfd-package-artifact-root": packageArtifactRoot, "source-sha": sourceSha };
105
+ if (emitOutputs) writeGitHubOutputs(outputs);
106
+ return { schemaVersion: 1, contract: "kungfu-buildchain-kfd-adopter-release-generation", status: "passed", qualifying: false, selfCertified: false, outputs };
109
107
  }
110
-
111
108
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
112
109
  try {
113
110
  const args = parseArgs();
114
- if (args.help) {
115
- process.stdout.write("Usage: generate-buildchain-kfd-witnesses [--cwd <repo>] [--output-dir <dir>] [--source-sha <sha>]\n");
116
- process.exit(0);
117
- }
118
- process.stdout.write(`${JSON.stringify(generateBuildchainKfdWitnesses(args), null, 2)}\n`);
111
+ if (args.help) process.stdout.write("Usage: generate-buildchain-kfd-witnesses [--cwd <repo>] [--output-dir <dir>] [--source-sha <sha>]\n");
112
+ else process.stdout.write(`${JSON.stringify(generateBuildchainKfdWitnesses(args), null, 2)}\n`);
119
113
  } catch (error) {
120
114
  console.error(`buildchain self KFD witnesses: ${error.message}`);
121
115
  process.exitCode = 1;
@@ -46,26 +46,26 @@ function routerInputs(inputBlock) {
46
46
 
47
47
  function routerOutputs(outputBlock) {
48
48
  const forwarded = outputBlock
49
- .replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.build.outputs.$1 }}")
49
+ .replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.alpha.outputs.$1 || jobs.stable.outputs.$1 }}")
50
50
  .replaceAll("build-lifecycle", "build-channel-router")
51
- .replace("value: ${{ jobs.build.outputs.controller-plan-artifact }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-artifact }}")
52
- .replace("value: ${{ jobs.build.outputs.controller-plan-json }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-json }}")
53
- .replace("value: ${{ jobs.build.outputs.controller-plan-digest }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-digest }}")
54
- .replace("value: ${{ jobs.build.outputs.controller-receipt-artifact }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-artifact }}")
55
- .replace("value: ${{ jobs.build.outputs.controller-receipt-json }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-json }}")
56
- .replace("value: ${{ jobs.build.outputs.controller-receipt-digest }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-digest }}")
57
- .replace("value: ${{ jobs.build.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}")
51
+ .replace("value: ${{ jobs.alpha.outputs.controller-plan-artifact || jobs.stable.outputs.controller-plan-artifact }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-artifact }}")
52
+ .replace("value: ${{ jobs.alpha.outputs.controller-plan-json || jobs.stable.outputs.controller-plan-json }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-json }}")
53
+ .replace("value: ${{ jobs.alpha.outputs.controller-plan-digest || jobs.stable.outputs.controller-plan-digest }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-digest }}")
54
+ .replace("value: ${{ jobs.alpha.outputs.controller-receipt-artifact || jobs.stable.outputs.controller-receipt-artifact }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-artifact }}")
55
+ .replace("value: ${{ jobs.alpha.outputs.controller-receipt-json || jobs.stable.outputs.controller-receipt-json }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-json }}")
56
+ .replace("value: ${{ jobs.alpha.outputs.controller-receipt-digest || jobs.stable.outputs.controller-receipt-digest }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-digest }}")
57
+ .replace("value: ${{ jobs.alpha.outputs.controller-receipt-status || jobs.stable.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}")
58
58
  .replace(
59
- /( credential-island-macos-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.build\.outputs\.artifact-name \}\}/,
60
- "$1 ${{ jobs.build.outputs.credential-island-macos-artifact }}",
59
+ /( credential-island-macos-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.alpha\.outputs\.artifact-name \|\| jobs\.stable\.outputs\.artifact-name \}\}/,
60
+ "$1 ${{ jobs.alpha.outputs.credential-island-macos-artifact || jobs.stable.outputs.credential-island-macos-artifact }}",
61
61
  )
62
62
  .replace(
63
- /( credential-island-macos-manifest-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.build\.outputs\.manifest-artifact-name \}\}/,
64
- "$1 ${{ jobs.build.outputs.credential-island-macos-manifest-artifact }}",
63
+ /( credential-island-macos-manifest-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.alpha\.outputs\.manifest-artifact-name \|\| jobs\.stable\.outputs\.manifest-artifact-name \}\}/,
64
+ "$1 ${{ jobs.alpha.outputs.credential-island-macos-manifest-artifact || jobs.stable.outputs.credential-island-macos-manifest-artifact }}",
65
65
  );
66
66
  return [
67
67
  " buildchain-channel:",
68
- ' description: "Resolved Buildchain channel: alpha, stable, or override"',
68
+ ' description: "Resolved Buildchain channel: alpha or stable"',
69
69
  " value: ${{ jobs.resolve-channel.outputs.channel }}",
70
70
  " buildchain-channel-selection-source:",
71
71
  ' description: "Evidence source used to select the Buildchain channel"',
@@ -154,7 +154,8 @@ function routerControllerReceiptJob() {
154
154
  needs:
155
155
  - resolve-channel
156
156
  - controller-plan
157
- - build
157
+ - alpha
158
+ - stable
158
159
  if: \${{ always() && needs.controller-plan.result == 'success' }}
159
160
  runs-on: \${{ fromJSON(inputs.control-runner-json) }}
160
161
  outputs:
@@ -183,9 +184,9 @@ function routerControllerReceiptJob() {
183
184
  env:
184
185
  BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
185
186
  BUILDCHAIN_CONTROLLER_STAGES_JSON: >-
186
- [{"id":"resolve-channel","status":"\${{ needs.resolve-channel.result }}"},{"id":"build","status":"\${{ needs.build.result }}"},{"id":"aggregate","status":"\${{ needs.build.result }}"}]
187
- BUILDCHAIN_CONTROLLER_EVIDENCE_JSON: \${{ needs.build.outputs.controller-receipt-digest != '' && format('[{{"kind":"nested-controller-receipt","digest":"{0}"}}]', needs.build.outputs.controller-receipt-digest) || '[]' }}
188
- BUILDCHAIN_CONTROLLER_REASON_CODE: \${{ needs.build.result == 'success' && '' || 'nested-build-incomplete' }}
187
+ [{"id":"resolve-channel","status":"\${{ needs.resolve-channel.result }}"},{"id":"alpha","status":"\${{ needs.alpha.result }}"},{"id":"stable","status":"\${{ needs.stable.result }}"},{"id":"aggregate","status":"\${{ needs.resolve-channel.outputs.channel == 'alpha' && needs.alpha.result || needs.stable.result }}"}]
188
+ BUILDCHAIN_CONTROLLER_EVIDENCE_JSON: \${{ (needs.alpha.outputs.controller-receipt-digest != '' || needs.stable.outputs.controller-receipt-digest != '') && format('[{{"kind":"nested-controller-receipt","digest":"{0}"}}]', needs.alpha.outputs.controller-receipt-digest || needs.stable.outputs.controller-receipt-digest) || '[]' }}
189
+ BUILDCHAIN_CONTROLLER_REASON_CODE: \${{ ((needs.resolve-channel.outputs.channel == 'alpha' && needs.alpha.result == 'success') || (needs.resolve-channel.outputs.channel == 'stable' && needs.stable.result == 'success')) && '' || 'nested-build-incomplete' }}
189
190
  BUILDCHAIN_CONTROLLER_REASON_SUMMARY: Nested build controller did not complete successfully
190
191
  BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT: buildchain-channel-controller-receipt-\${{ github.sha }}
191
192
  BUILDCHAIN_CONTROLLER_RECEIPT_PATH: .buildchain/controller/receipt.json
@@ -215,7 +216,9 @@ function routerAggregateJob() {
215
216
  return ` summarize:
216
217
  name: Summarize build contract
217
218
  needs:
218
- - build
219
+ - resolve-channel
220
+ - alpha
221
+ - stable
219
222
  - controller-receipt
220
223
  if: \${{ always() }}
221
224
  runs-on: \${{ fromJSON(inputs.control-runner-json) }}
@@ -223,12 +226,16 @@ function routerAggregateJob() {
223
226
  - name: Enforce public channel router aggregate
224
227
  shell: bash
225
228
  env:
226
- BUILD_RESULT: \${{ needs.build.result }}
229
+ CHANNEL: \${{ needs.resolve-channel.outputs.channel }}
230
+ ALPHA_RESULT: \${{ needs.alpha.result }}
231
+ STABLE_RESULT: \${{ needs.stable.result }}
227
232
  CONTROLLER_RECEIPT_RESULT: \${{ needs.controller-receipt.result }}
228
233
  run: |
229
234
  set -euo pipefail
230
- if [[ "\${BUILD_RESULT}" != "success" || "\${CONTROLLER_RECEIPT_RESULT}" != "success" ]]; then
231
- echo "::error::Buildchain channel router did not qualify: build=\${BUILD_RESULT} controller-receipt=\${CONTROLLER_RECEIPT_RESULT}"
235
+ selected_result="\${STABLE_RESULT}"
236
+ if [[ "\${CHANNEL}" = "alpha" ]]; then selected_result="\${ALPHA_RESULT}"; fi
237
+ if [[ "\${selected_result}" != "success" || "\${CONTROLLER_RECEIPT_RESULT}" != "success" ]]; then
238
+ echo "::error::Buildchain channel router did not qualify: channel=\${CHANNEL} selected=\${selected_result} controller-receipt=\${CONTROLLER_RECEIPT_RESULT}"
232
239
  exit 1
233
240
  fi
234
241
  echo "Buildchain channel router aggregate passed."`;
@@ -245,6 +252,47 @@ function generateChannelBuildWorkflowBase(source) {
245
252
  return `# Generated by scripts/generate-channel-build-workflow.mjs. Do not edit directly.\nname: Buildchain Channel Build\n\non:\n workflow_call:\n inputs:\n${routerInputs(inputs)}\n secrets:\n${secrets.trimEnd()}\n outputs:\n${routerOutputs(outputs)}\n\npermissions:\n contents: read\n issues: write\n id-token: write\n\njobs:\n resolve-channel:\n name: Resolve Buildchain channel\n runs-on: ubuntu-24.04\n outputs:\n channel: \${{ steps.channel.outputs.channel }}\n buildchain-ref: \${{ steps.channel.outputs.buildchain-ref }}\n contract-lock-path: \${{ steps.lock.outputs.path }}\n selection-source: \${{ steps.channel.outputs.selection-source }}\n reason: \${{ steps.channel.outputs.reason }}\n steps:\n - name: Resolve router source\n id: router\n shell: bash\n env:\n BUILDCHAIN_ROUTER_WORKFLOW_REF: \${{ job.workflow_ref }}\n run: |\n set -euo pipefail\n workflow_ref=\"\${BUILDCHAIN_ROUTER_WORKFLOW_REF}\"\n repository=\"\${workflow_ref%%/.github/workflows/*}\"\n ref=\"\${workflow_ref##*@}\"\n ref=\"\${ref#refs/heads/}\"\n ref=\"\${ref#refs/tags/}\"\n if [[ -z \"\${repository}\" || \"\${repository}\" = \"\${workflow_ref}\" || -z \"\${ref}\" ]]; then\n echo \"Unable to resolve Buildchain router source from job.workflow_ref=\${workflow_ref}\" >&2\n exit 1\n fi\n {\n echo \"repository=\${repository}\"\n echo \"ref=\${ref}\"\n } >> \"\${GITHUB_OUTPUT}\"\n\n - name: Checkout Buildchain router\n uses: actions/checkout@v7.0.0\n with:\n repository: \${{ steps.router.outputs.repository }}\n ref: \${{ steps.router.outputs.ref }}\n path: .buildchain/router\n persist-credentials: false\n\n - name: Resolve channel\n id: channel\n shell: bash\n env:\n BUILDCHAIN_CHANNEL: \${{ inputs.buildchain-channel }}\n BUILDCHAIN_REQUESTED_REF: \${{ inputs.buildchain-ref }}\n BUILDCHAIN_PUBLISH_CHANNEL: \${{ inputs.publish-channel }}\n BUILDCHAIN_EVENT_NAME: \${{ github.event_name }}\n BUILDCHAIN_GIT_REF: \${{ github.ref }}\n BUILDCHAIN_RELEASE_PRERELEASE: \${{ github.event.release.prerelease }}\n BUILDCHAIN_ROUTER_REF: \${{ steps.router.outputs.ref }}\n run: |\n node .buildchain/router/scripts/buildchain-channel-router.mjs \\\n --cwd .buildchain/router \\\n --channel \"\${BUILDCHAIN_CHANNEL}\" \\\n --buildchain-ref \"\${BUILDCHAIN_REQUESTED_REF}\" \\\n --publish-channel \"\${BUILDCHAIN_PUBLISH_CHANNEL}\" \\\n --event-name \"\${BUILDCHAIN_EVENT_NAME}\" \\\n --ref \"\${BUILDCHAIN_GIT_REF}\" \\\n --release-prerelease \"\${BUILDCHAIN_RELEASE_PRERELEASE}\" \\\n --router-ref \"\${BUILDCHAIN_ROUTER_REF}\"\n\n - name: Select consumer contract lock\n id: lock\n shell: bash\n env:\n EXPLICIT_PATH: \${{ inputs.buildchain-contract-lock-path }}\n ALPHA_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}\n STABLE_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}\n CHANNEL: \${{ steps.channel.outputs.channel }}\n run: |\n set -euo pipefail\n path=\"\${EXPLICIT_PATH}\"\n if [[ -z \"\${path}\" ]]; then\n if [[ \"\${CHANNEL}\" = \"alpha\" ]]; then\n path=\"\${ALPHA_PATH}\"\n else\n path=\"\${STABLE_PATH}\"\n fi\n fi\n echo \"path=\${path}\" >> \"\${GITHUB_OUTPUT}\"\n\n build:\n name: Build with resolved channel\n needs: resolve-channel\n uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n issues: write\n id-token: write\n with:\n${forwardedInputs(names)}\n secrets: inherit\n`;
246
253
  }
247
254
 
255
+ function splitBuildJobsByChannel(workflow, names, major) {
256
+ const marker = "\n build:\n";
257
+ const index = workflow.indexOf(marker);
258
+ if (index < 0) throw new Error("generated workflow is missing the build job");
259
+ const inputs = forwardedInputs(names);
260
+ const jobs = `
261
+ alpha:
262
+ name: Build alpha channel
263
+ needs:
264
+ - resolve-channel
265
+ - controller-plan
266
+ if: \${{ needs.resolve-channel.outputs.channel == 'alpha' }}
267
+ uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v${major}-alpha
268
+ permissions:
269
+ actions: read
270
+ contents: read
271
+ issues: write
272
+ id-token: write
273
+ with:
274
+ ${inputs}
275
+ secrets: inherit
276
+
277
+ stable:
278
+ name: Build stable channel
279
+ needs:
280
+ - resolve-channel
281
+ - controller-plan
282
+ if: \${{ needs.resolve-channel.outputs.channel == 'stable' }}
283
+ uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v${major}
284
+ permissions:
285
+ actions: read
286
+ contents: read
287
+ issues: write
288
+ id-token: write
289
+ with:
290
+ ${inputs}
291
+ secrets: inherit
292
+ `;
293
+ return `${workflow.slice(0, index)}${jobs}`;
294
+ }
295
+
248
296
  function bindRouterCheckoutToWorkflowSha(workflow) {
249
297
  return workflow
250
298
  .replace(
@@ -294,7 +342,14 @@ function bindRouterCheckoutToWorkflowSha(workflow) {
294
342
  }
295
343
 
296
344
  export function generateChannelBuildWorkflow(source) {
297
- const generated = bindRouterCheckoutToWorkflowSha(generateChannelBuildWorkflowBase(source))
345
+ const inputBlock = blockBetween(source, " inputs:\n", " secrets:\n");
346
+ const names = inputNames(inputBlock);
347
+ const packageVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || "";
348
+ const major = Number(String(packageVersion).split(".")[0]);
349
+ if (!Number.isInteger(major) || major < 1) throw new Error("package.json version must declare a positive major");
350
+ const generated = bindRouterCheckoutToWorkflowSha(
351
+ splitBuildJobsByChannel(generateChannelBuildWorkflowBase(source), names, major),
352
+ )
298
353
  .replaceAll("runs-on: ubuntu-24.04", "runs-on: ${{ fromJSON(inputs.control-runner-json) }}")
299
354
  .replace(
300
355
  "\npermissions:\n contents: read\n",
@@ -310,15 +365,7 @@ export function generateChannelBuildWorkflow(source) {
310
365
  "",
311
366
  ].join("\n"),
312
367
  )
313
- .replace("\n build:\n", `\n${routerControllerPlanJob()}\n\n build:\n`)
314
- .replace(
315
- " needs: resolve-channel\n uses: ./.github/workflows/.build.yml",
316
- " needs:\n - resolve-channel\n - controller-plan\n uses: ./.github/workflows/.build.yml",
317
- )
318
- .replace(
319
- " uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n",
320
- " uses: ./.github/workflows/.build.yml\n permissions:\n actions: read\n contents: read\n",
321
- );
368
+ .replace("\n alpha:\n", `\n${routerControllerPlanJob()}\n\n alpha:\n`);
322
369
  return `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n\n${routerAggregateJob()}\n`;
323
370
  }
324
371
 
@@ -15,10 +15,7 @@ import {
15
15
  collectKfdUpstreamFacts,
16
16
  } from "../packages/core/kfd.js";
17
17
  import { KFD_AGENT_HUB_ADOPTION_SCHEMA } from "../packages/core/kfd-agent-hub.js";
18
- import {
19
- KFD_PRODUCT_GATE_INPUT_SCHEMA,
20
- KFD_SUPPORT_PROJECTION_SCHEMA,
21
- } from "../packages/core/kfd-product-gates.js";
18
+ import { KFD_PRODUCT_GATE_INPUT_SCHEMA } from "../packages/core/kfd-product-gates.js";
22
19
  import {
23
20
  createReadmeBadgeEndpointRegistry,
24
21
  } from "../packages/core/readme-badges.js";
@@ -933,7 +930,6 @@ function buildSiteBundle() {
933
930
  "schemas/release-tail-provider-bindings-v1.schema.json",
934
931
  "schemas/kfd-agent-hub-adoption.schema.json",
935
932
  "schemas/kfd-product-gate-input-v1.schema.json",
936
- "schemas/kfd-support-projection-v1.schema.json",
937
933
  "kfd-support.json",
938
934
  "artifact-evidence.json",
939
935
  "product-mechanism.json",
@@ -971,7 +967,6 @@ function buildSiteBundle() {
971
967
  "schemas/release-tail-provider-bindings-v1.schema.json",
972
968
  "schemas/kfd-agent-hub-adoption.schema.json",
973
969
  "schemas/kfd-product-gate-input-v1.schema.json",
974
- "schemas/kfd-support-projection-v1.schema.json",
975
970
  "buildchain-contract.json",
976
971
  "kfd-claims.json",
977
972
  "product-mechanism.json",
@@ -1105,7 +1100,6 @@ function buildSiteBundle() {
1105
1100
  "schemas/release-tail-provider-bindings-v1.schema.json",
1106
1101
  "schemas/kfd-agent-hub-adoption.schema.json",
1107
1102
  "schemas/kfd-product-gate-input-v1.schema.json",
1108
- "schemas/kfd-support-projection-v1.schema.json",
1109
1103
  "buildchain-contract.json",
1110
1104
  "kfd-upstream-aggregate.json",
1111
1105
  "kfd-claims.json",
@@ -1289,7 +1283,6 @@ function buildSiteBundle() {
1289
1283
  ),
1290
1284
  "schemas/kfd-agent-hub-adoption.schema.json": KFD_AGENT_HUB_ADOPTION_SCHEMA,
1291
1285
  "schemas/kfd-product-gate-input-v1.schema.json": KFD_PRODUCT_GATE_INPUT_SCHEMA,
1292
- "schemas/kfd-support-projection-v1.schema.json": KFD_SUPPORT_PROJECTION_SCHEMA,
1293
1286
  "badge-endpoint-registry.json": badgeEndpointRegistry,
1294
1287
  "publication-registry.json": publicationRegistry,
1295
1288
  "buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { resolveBuildchainChannel } from "./buildchain-channel-router.mjs";
7
+ import { parseBuildchainRefIdentity } from "../packages/core/buildchain-channel-identity.js";
7
8
 
8
9
  const TARGETS = [
9
10
  { pattern: /^alpha\/v(\d+)\/v\d+\.\d+$/, publicationChannel: "alpha", shellChannel: "alpha" },
@@ -54,9 +55,10 @@ export function resolvePromotionChannel({
54
55
  routerRef,
55
56
  packageVersion,
56
57
  });
57
- const trustedRouterPin = selected.channel === "override"
58
- && sameExactSha(selected.buildchainRef, routerSha);
59
- const overrideUsed = selected.channel === "override" && !trustedRouterPin;
58
+ const runtimeIdentity = parseBuildchainRefIdentity(selected.buildchainRef);
59
+ const opaqueOverride = new Set(["train", "authority", "exact-sha"]).has(runtimeIdentity.kind);
60
+ const trustedRouterPin = opaqueOverride && sameExactSha(selected.buildchainRef, routerSha);
61
+ const overrideUsed = opaqueOverride && !trustedRouterPin;
60
62
  if (!overrideUsed && !trustedRouterPin && selected.channel !== intent.shellChannel) {
61
63
  throw new Error(
62
64
  `promotion target ${intent.targetRef} requires ${intent.shellChannel} shell/runtime, got ${selected.channel}`,