@kungfu-tech/buildchain 2.12.4 → 2.12.5-alpha.2

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 (33) hide show
  1. package/bin/buildchain.mjs +3 -0
  2. package/dist/site/agent-index.json +1 -0
  3. package/dist/site/artifact-schemas.json +1 -0
  4. package/dist/site/buildchain-contract.json +2008 -12
  5. package/dist/site/buildchain-site.json +74 -11
  6. package/dist/site/capability-registry.json +4 -3
  7. package/dist/site/controller-registry.json +1626 -0
  8. package/dist/site/kfd-claims.json +88 -74
  9. package/dist/site/kfd-upstream-aggregate.json +1 -1
  10. package/dist/site/manual-registry.json +18 -2
  11. package/dist/site/node-api-registry.json +20 -7
  12. package/dist/site/page-registry.json +58 -5
  13. package/dist/site/public-surface-audit.json +572 -102
  14. package/dist/site/publication-registry.json +4 -4
  15. package/dist/site/release-provenance.json +2 -0
  16. package/dist/site/site-manifest.json +15 -6
  17. package/dist/site/workflow-registry.json +534 -68
  18. package/docs/MAP.md +2 -1
  19. package/docs/controller-evidence.md +113 -0
  20. package/docs/release-passport.md +6 -0
  21. package/package.json +3 -1
  22. package/packages/core/buildchain-contract.js +62 -1
  23. package/packages/core/buildchain-kfd-claims.js +3 -0
  24. package/packages/core/controller-evidence.js +505 -0
  25. package/packages/core/index.js +17 -0
  26. package/packages/core/public-surface-audit.js +40 -3
  27. package/packages/core/release-candidate.js +26 -0
  28. package/packages/core/release-passport.js +19 -0
  29. package/scripts/check-inventory.mjs +4 -1
  30. package/scripts/controller-evidence.mjs +170 -0
  31. package/scripts/generate-channel-build-workflow.mjs +141 -2
  32. package/scripts/generate-release-candidate-passport.mjs +4 -0
  33. package/scripts/generate-site-bundle.mjs +11 -1
@@ -16,6 +16,7 @@ import {
16
16
  } from "./kfd-gate.js";
17
17
  import { createSurfaceTimestampPolicy } from "./surface-manifest.js";
18
18
  import { validatePublishEvidence as validateTransactionPublishEvidence } from "./publish-transaction.js";
19
+ import { normalizeControllerReceiptReferences } from "./controller-evidence.js";
19
20
 
20
21
  export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
21
22
  export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
@@ -989,6 +990,8 @@ export function createReleasePassport({
989
990
  kfd1 = undefined,
990
991
  kfd2Claims = [],
991
992
  kfd3 = undefined,
993
+ controllerReceipts = [],
994
+ controllerReceiptReferences = [],
992
995
  } = {}) {
993
996
  const normalizedTag = nonEmptyString(tag, "tag");
994
997
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
@@ -1016,6 +1019,18 @@ export function createReleasePassport({
1016
1019
  kfd1Section: normalizedKfd1?.passportSection,
1017
1020
  kfd3Section: normalizedKfd3?.passportSection,
1018
1021
  });
1022
+ const builtSourceSha = optionalString(release.builtSourceSha || release.built_source_sha);
1023
+ const promotionChannelSha = optionalString(release.promotionChannelSha || release.promotion_channel_sha);
1024
+ const treeEquivalent = release.treeEquivalent === true;
1025
+ const normalizedControllerReceipts = normalizeControllerReceiptReferences({
1026
+ receipts: controllerReceipts,
1027
+ references: controllerReceiptReferences,
1028
+ expectedSourceSha: sourceSha,
1029
+ acceptedSourceShas: treeEquivalent && promotionChannelSha === sourceSha && builtSourceSha
1030
+ ? [builtSourceSha]
1031
+ : [],
1032
+ requirePassed: true,
1033
+ });
1019
1034
  const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
1020
1035
  const normalizedPublishSummary = normalizePublishSummary({
1021
1036
  packageSet: normalizedPackageSet,
@@ -1069,6 +1084,7 @@ export function createReleasePassport({
1069
1084
  "artifact-evidence.json",
1070
1085
  "publish evidence",
1071
1086
  "release-state transaction",
1087
+ "controller receipt references",
1072
1088
  ],
1073
1089
  timestampFields: ["generatedAt", "publishedAt", "surfaceTimestampPolicy.generatedAt", "surfaceTimestampPolicy.publishedAt"],
1074
1090
  timestampFieldsParticipateInArtifactDigest: true,
@@ -1138,6 +1154,7 @@ export function createReleasePassport({
1138
1154
  ...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
1139
1155
  ...(normalizedKfd2 ? { "kfd-2": normalizedKfd2 } : {}),
1140
1156
  ...(normalizedKfd3 ? { [normalizedKfd3.key || "kfd-3"]: normalizedKfd3.passportSection } : {}),
1157
+ ...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
1141
1158
  versionImpact: normalizedImpact.versionImpact,
1142
1159
  surfaceImpacts: normalizedImpact.surfaceImpacts,
1143
1160
  artifacts: [
@@ -1218,6 +1235,7 @@ export function collectGitHubReleasePassport({
1218
1235
  kfd3PrebuildWitnessJsons = [],
1219
1236
  kfd3ArtifactWitnessJsons = [],
1220
1237
  kfd3ArtifactVerifyCommand = "",
1238
+ controllerReceiptReferences = [],
1221
1239
  basePassportJson = "",
1222
1240
  requireBaseKfd = false,
1223
1241
  releaseJsonExtra = "",
@@ -1340,6 +1358,7 @@ export function collectGitHubReleasePassport({
1340
1358
  kfd1,
1341
1359
  kfd2Claims: kfd2ClaimMetas.map((meta) => meta.value),
1342
1360
  kfd3,
1361
+ controllerReceiptReferences,
1343
1362
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
1344
1363
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
1345
1364
  workflow,
@@ -234,6 +234,9 @@ if (rootPackage.exports?.["./homebrew"] !== "./packages/core/homebrew.js") {
234
234
  if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildchain-contract.js") {
235
235
  throw new Error("root package must export @kungfu-tech/buildchain/buildchain-contract");
236
236
  }
237
+ if (rootPackage.exports?.["./controller-evidence"] !== "./packages/core/controller-evidence.js") {
238
+ throw new Error("root package must export @kungfu-tech/buildchain/controller-evidence");
239
+ }
237
240
  if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
238
241
  throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
239
242
  }
@@ -1040,7 +1043,7 @@ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-rele
1040
1043
  throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
1041
1044
  }
1042
1045
 
1043
- for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
1046
+ for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
1044
1047
  if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
1045
1048
  throw new Error(`site bundle missing ${siteFile}`);
1046
1049
  }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import {
7
+ createControllerPlan,
8
+ createControllerReceipt,
9
+ validateControllerPlan,
10
+ validateControllerReceipt,
11
+ } from "../packages/core/controller-evidence.js";
12
+
13
+ function argument(name, fallback = "") {
14
+ const index = process.argv.indexOf(`--${name}`);
15
+ return index === -1 ? fallback : process.argv[index + 1] || "";
16
+ }
17
+
18
+ function env(name, fallback = "") {
19
+ return String(process.env[name] ?? fallback).trim();
20
+ }
21
+
22
+ function readJson(filePath, label = filePath) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
25
+ } catch (error) {
26
+ throw new Error(`could not read ${label}: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ function parseJson(value, label, fallback = undefined) {
31
+ if (!String(value || "").trim() && fallback !== undefined) return fallback;
32
+ try {
33
+ return JSON.parse(value);
34
+ } catch (error) {
35
+ throw new Error(`${label} must be valid JSON: ${error.message}`);
36
+ }
37
+ }
38
+
39
+ function writeJson(filePath, value) {
40
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
42
+ }
43
+
44
+ function writeOutputs(outputs) {
45
+ const outputPath = env("GITHUB_OUTPUT");
46
+ if (!outputPath) return;
47
+ fs.appendFileSync(outputPath, Object.entries(outputs).map(([name, value]) => `${name}=${String(value ?? "")}\n`).join(""));
48
+ }
49
+
50
+ function sha256File(filePath) {
51
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
52
+ }
53
+
54
+ function descriptorFromRegistry(registry, controllerId) {
55
+ const descriptor = registry.controllers?.find((entry) => entry.id === controllerId);
56
+ if (!descriptor) throw new Error(`controller registry does not declare ${controllerId}`);
57
+ return descriptor;
58
+ }
59
+
60
+ function normalizeStageStatus(value) {
61
+ const normalized = String(value || "").trim().toLowerCase();
62
+ return {
63
+ success: "passed",
64
+ failure: "failed",
65
+ cancelled: "cancelled",
66
+ skipped: "skipped",
67
+ }[normalized] || normalized;
68
+ }
69
+
70
+ function collectEvidence() {
71
+ const inline = parseJson(env("BUILDCHAIN_CONTROLLER_EVIDENCE_JSON", "[]"), "controller evidence JSON", []);
72
+ const files = parseJson(env("BUILDCHAIN_CONTROLLER_EVIDENCE_FILES_JSON", "[]"), "controller evidence files JSON", []);
73
+ if (!Array.isArray(inline) || !Array.isArray(files)) throw new Error("controller evidence inputs must be arrays");
74
+ return [
75
+ ...inline,
76
+ ...files.map((entry, index) => {
77
+ const filePath = path.resolve(String(entry.path || ""));
78
+ if (!entry.kind || !entry.path) throw new Error(`controller evidence file ${index} requires kind and path`);
79
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) throw new Error(`controller evidence file is missing: ${entry.path}`);
80
+ return {
81
+ kind: String(entry.kind),
82
+ digest: sha256File(filePath),
83
+ ...(entry.artifact ? { artifact: String(entry.artifact) } : {}),
84
+ };
85
+ }),
86
+ ];
87
+ }
88
+
89
+ function planMode() {
90
+ const registryPath = path.resolve(env("BUILDCHAIN_CONTROLLER_REGISTRY", ".buildchain/runtime/dist/site/controller-registry.json"));
91
+ const registry = readJson(registryPath, "controller registry");
92
+ const descriptor = descriptorFromRegistry(registry, env("BUILDCHAIN_CONTROLLER_ID"));
93
+ const plan = createControllerPlan({
94
+ descriptor,
95
+ source: {
96
+ repository: env("BUILDCHAIN_CONTROLLER_SOURCE_REPOSITORY"),
97
+ sha: env("BUILDCHAIN_CONTROLLER_SOURCE_SHA"),
98
+ },
99
+ runtime: {
100
+ ref: env("BUILDCHAIN_CONTROLLER_RUNTIME_REF"),
101
+ sha: env("BUILDCHAIN_CONTROLLER_RUNTIME_SHA"),
102
+ contractDigest: env("BUILDCHAIN_CONTROLLER_CONTRACT_DIGEST"),
103
+ },
104
+ inputs: parseJson(env("BUILDCHAIN_CONTROLLER_INPUTS_JSON", "{}"), "controller inputs JSON", {}),
105
+ });
106
+ const outputPath = path.resolve(env("BUILDCHAIN_CONTROLLER_PLAN_PATH", ".buildchain/controller/plan.json"));
107
+ writeJson(outputPath, plan);
108
+ writeOutputs({
109
+ "controller-plan-path": outputPath,
110
+ "controller-plan-json": JSON.stringify(plan),
111
+ "controller-plan-digest": plan.digest,
112
+ });
113
+ return plan;
114
+ }
115
+
116
+ function receiptMode() {
117
+ const planPath = path.resolve(env("BUILDCHAIN_CONTROLLER_PLAN_PATH", ".buildchain/controller/plan.json"));
118
+ const stages = parseJson(env("BUILDCHAIN_CONTROLLER_STAGES_JSON", "[]"), "controller stages JSON", []);
119
+ if (!Array.isArray(stages)) throw new Error("controller stages JSON must be an array");
120
+ const receipt = createControllerReceipt({
121
+ plan: readJson(planPath, "controller plan"),
122
+ stages: stages.map((stage) => ({ ...stage, status: normalizeStageStatus(stage.status) })),
123
+ evidence: collectEvidence(),
124
+ reason: env("BUILDCHAIN_CONTROLLER_REASON_CODE")
125
+ ? {
126
+ code: env("BUILDCHAIN_CONTROLLER_REASON_CODE"),
127
+ summary: env("BUILDCHAIN_CONTROLLER_REASON_SUMMARY", "controller did not pass"),
128
+ }
129
+ : undefined,
130
+ artifact: env("BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT"),
131
+ });
132
+ const outputPath = path.resolve(env("BUILDCHAIN_CONTROLLER_RECEIPT_PATH", ".buildchain/controller/receipt.json"));
133
+ writeJson(outputPath, receipt);
134
+ writeOutputs({
135
+ "controller-receipt-path": outputPath,
136
+ "controller-receipt-json": JSON.stringify(receipt),
137
+ "controller-receipt-digest": receipt.digest,
138
+ "controller-receipt-status": receipt.status,
139
+ "controller-receipt-qualifying": String(receipt.qualifying),
140
+ });
141
+ return receipt;
142
+ }
143
+
144
+ function validateMode() {
145
+ const kind = env("BUILDCHAIN_CONTROLLER_EVIDENCE_KIND", argument("kind", "receipt"));
146
+ const filePath = path.resolve(env("BUILDCHAIN_CONTROLLER_EVIDENCE_PATH", argument("file")));
147
+ const value = readJson(filePath, `controller ${kind}`);
148
+ const validation = kind === "plan" ? validateControllerPlan(value) : validateControllerReceipt(value);
149
+ if (!validation.ok || (process.argv.includes("--require-qualifying") && !validation.qualifying)) {
150
+ throw new Error(`controller ${kind} validation failed: ${validation.issues.join("; ") || "not qualifying"}`);
151
+ }
152
+ return validation;
153
+ }
154
+
155
+ export function controllerEvidenceCli() {
156
+ const mode = argument("mode", env("BUILDCHAIN_CONTROLLER_MODE", "plan"));
157
+ if (mode === "plan") return planMode();
158
+ if (mode === "receipt") return receiptMode();
159
+ if (mode === "validate") return validateMode();
160
+ throw new Error(`unsupported controller evidence mode: ${mode}`);
161
+ }
162
+
163
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
164
+ try {
165
+ controllerEvidenceCli();
166
+ } catch (error) {
167
+ console.error(`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`);
168
+ process.exitCode = 1;
169
+ }
170
+ }
@@ -45,7 +45,16 @@ function routerInputs(inputBlock) {
45
45
  }
46
46
 
47
47
  function routerOutputs(outputBlock) {
48
- const forwarded = outputBlock.replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.build.outputs.$1 }}");
48
+ const forwarded = outputBlock
49
+ .replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.build.outputs.$1 }}")
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 }}");
49
58
  return [
50
59
  " buildchain-channel:",
51
60
  ' description: "Resolved Buildchain channel: alpha, stable, or override"',
@@ -72,7 +81,127 @@ function forwardedInputs(names) {
72
81
  .join("\n");
73
82
  }
74
83
 
75
- export function generateChannelBuildWorkflow(source) {
84
+ function routerControllerPlanJob() {
85
+ return ` controller-plan:
86
+ name: Plan channel router controller evidence
87
+ needs: resolve-channel
88
+ runs-on: ubuntu-24.04
89
+ outputs:
90
+ controller-plan-artifact: \${{ steps.names.outputs.controller-plan-artifact }}
91
+ controller-plan-json: \${{ steps.plan.outputs.controller-plan-json }}
92
+ controller-plan-digest: \${{ steps.plan.outputs.controller-plan-digest }}
93
+ runtime-sha: \${{ steps.identities.outputs.runtime-sha }}
94
+ steps:
95
+ - name: Checkout selected Buildchain runtime
96
+ uses: actions/checkout@v7.0.0
97
+ with:
98
+ repository: \${{ inputs.buildchain-repository }}
99
+ ref: \${{ needs.resolve-channel.outputs.buildchain-ref }}
100
+ path: .buildchain/runtime
101
+ persist-credentials: false
102
+
103
+ - name: Resolve selected runtime identity
104
+ id: identities
105
+ shell: bash
106
+ run: |
107
+ runtime_sha="$(git -C .buildchain/runtime rev-parse HEAD)"
108
+ contract_digest="$(node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(".buildchain/runtime/dist/site/buildchain-contract.json","utf8")); process.stdout.write(value.contractDigest)')"
109
+ {
110
+ echo "runtime-sha=\${runtime_sha}"
111
+ echo "contract-digest=\${contract_digest}"
112
+ } >> "$GITHUB_OUTPUT"
113
+
114
+ - name: Create channel router controller plan
115
+ id: plan
116
+ run: node .buildchain/runtime/scripts/controller-evidence.mjs --mode plan
117
+ env:
118
+ BUILDCHAIN_CONTROLLER_ID: build-channel-router
119
+ BUILDCHAIN_CONTROLLER_SOURCE_REPOSITORY: \${{ github.repository }}
120
+ BUILDCHAIN_CONTROLLER_SOURCE_SHA: \${{ github.sha }}
121
+ BUILDCHAIN_CONTROLLER_RUNTIME_REF: \${{ needs.resolve-channel.outputs.buildchain-ref }}
122
+ BUILDCHAIN_CONTROLLER_RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
123
+ BUILDCHAIN_CONTROLLER_CONTRACT_DIGEST: \${{ steps.identities.outputs.contract-digest }}
124
+ BUILDCHAIN_CONTROLLER_INPUTS_JSON: \${{ toJSON(inputs) }}
125
+ BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
126
+
127
+ - name: Resolve channel router controller plan artifact
128
+ id: names
129
+ run: echo "controller-plan-artifact=buildchain-channel-controller-plan-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
130
+ env:
131
+ SOURCE_SHA: \${{ github.sha }}
132
+
133
+ - name: Upload channel router controller plan
134
+ uses: actions/upload-artifact@v7.0.1
135
+ with:
136
+ name: \${{ steps.names.outputs.controller-plan-artifact }}
137
+ path: .buildchain/controller/plan.json
138
+ if-no-files-found: error`;
139
+ }
140
+
141
+ function routerControllerReceiptJob() {
142
+ return ` controller-receipt:
143
+ name: Finalize channel router controller evidence
144
+ needs:
145
+ - resolve-channel
146
+ - controller-plan
147
+ - build
148
+ if: \${{ always() && needs.controller-plan.result == 'success' }}
149
+ runs-on: ubuntu-24.04
150
+ outputs:
151
+ controller-receipt-artifact: \${{ steps.names.outputs.controller-receipt-artifact }}
152
+ controller-receipt-json: \${{ steps.receipt.outputs.controller-receipt-json }}
153
+ controller-receipt-digest: \${{ steps.receipt.outputs.controller-receipt-digest }}
154
+ controller-receipt-status: \${{ steps.receipt.outputs.controller-receipt-status }}
155
+ steps:
156
+ - name: Checkout selected Buildchain runtime
157
+ uses: actions/checkout@v7.0.0
158
+ with:
159
+ repository: \${{ inputs.buildchain-repository }}
160
+ ref: \${{ needs.controller-plan.outputs.runtime-sha }}
161
+ path: .buildchain/runtime
162
+ persist-credentials: false
163
+
164
+ - name: Download channel router controller plan
165
+ uses: actions/download-artifact@v7.0.0
166
+ with:
167
+ name: \${{ needs.controller-plan.outputs.controller-plan-artifact }}
168
+ path: .buildchain/controller
169
+
170
+ - name: Create channel router controller receipt
171
+ id: receipt
172
+ run: node .buildchain/runtime/scripts/controller-evidence.mjs --mode receipt
173
+ env:
174
+ BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
175
+ BUILDCHAIN_CONTROLLER_STAGES_JSON: >-
176
+ [{"id":"resolve-channel","status":"\${{ needs.resolve-channel.result }}"},{"id":"build","status":"\${{ needs.build.result }}"},{"id":"aggregate","status":"\${{ needs.build.result }}"}]
177
+ BUILDCHAIN_CONTROLLER_EVIDENCE_JSON: \${{ needs.build.outputs.controller-receipt-digest != '' && format('[{{"kind":"nested-controller-receipt","digest":"{0}"}}]', needs.build.outputs.controller-receipt-digest) || '[]' }}
178
+ BUILDCHAIN_CONTROLLER_REASON_CODE: \${{ needs.build.result == 'success' && '' || 'nested-build-incomplete' }}
179
+ BUILDCHAIN_CONTROLLER_REASON_SUMMARY: Nested build controller did not complete successfully
180
+ BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT: buildchain-channel-controller-receipt-\${{ github.sha }}
181
+ BUILDCHAIN_CONTROLLER_RECEIPT_PATH: .buildchain/controller/receipt.json
182
+
183
+ - name: Resolve channel router controller receipt artifact
184
+ id: names
185
+ run: echo "controller-receipt-artifact=buildchain-channel-controller-receipt-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
186
+ env:
187
+ SOURCE_SHA: \${{ github.sha }}
188
+
189
+ - name: Upload channel router controller receipt
190
+ if: \${{ always() && steps.receipt.outcome == 'success' }}
191
+ uses: actions/upload-artifact@v7.0.1
192
+ with:
193
+ name: \${{ steps.names.outputs.controller-receipt-artifact }}
194
+ path: .buildchain/controller/receipt.json
195
+ if-no-files-found: error
196
+
197
+ - name: Enforce qualifying channel router controller receipt
198
+ if: \${{ steps.receipt.outputs.controller-receipt-qualifying != 'true' }}
199
+ run: |
200
+ echo "::error::channel router controller receipt is not qualifying: \${{ steps.receipt.outputs.controller-receipt-status }}"
201
+ exit 1`;
202
+ }
203
+
204
+ function generateChannelBuildWorkflowBase(source) {
76
205
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
77
206
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
78
207
  const outputs = blockBetween(source, " outputs:\n", "\npermissions:\n");
@@ -83,6 +212,16 @@ export function generateChannelBuildWorkflow(source) {
83
212
  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`;
84
213
  }
85
214
 
215
+ export function generateChannelBuildWorkflow(source) {
216
+ const generated = generateChannelBuildWorkflowBase(source)
217
+ .replace("\n build:\n", `\n${routerControllerPlanJob()}\n\n build:\n`)
218
+ .replace(
219
+ " needs: resolve-channel\n uses: ./.github/workflows/.build.yml",
220
+ " needs:\n - resolve-channel\n - controller-plan\n uses: ./.github/workflows/.build.yml",
221
+ );
222
+ return `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n`;
223
+ }
224
+
86
225
  function main() {
87
226
  const source = fs.readFileSync(sourcePath, "utf8");
88
227
  const generated = generateChannelBuildWorkflow(source);
@@ -27,6 +27,8 @@ export function generateReleaseCandidatePassportCli() {
27
27
  );
28
28
  const gateAggregateJson = env("BUILDCHAIN_GATE_PROFILE_AGGREGATE_JSON");
29
29
  const gateAggregate = gateAggregateJson ? JSON.parse(gateAggregateJson) : undefined;
30
+ const controllerReceiptJson = env("BUILDCHAIN_CONTROLLER_RECEIPT_JSON");
31
+ const controllerReceipts = controllerReceiptJson ? [JSON.parse(controllerReceiptJson)] : [];
30
32
  const passport = createReleaseCandidatePassport({
31
33
  repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
32
34
  pullRequest: {
@@ -49,6 +51,7 @@ export function generateReleaseCandidatePassportCli() {
49
51
  workflowShellRef: env("BUILDCHAIN_WORKFLOW_SHELL_REF", buildSummary.runtime?.workflowShellRef || ""),
50
52
  },
51
53
  gateAggregate,
54
+ controllerReceipts,
52
55
  workflow: {
53
56
  name: env("GITHUB_WORKFLOW"),
54
57
  runId: env("GITHUB_RUN_ID", buildSummary.git?.runId || ""),
@@ -77,6 +80,7 @@ export function generateReleaseCandidatePassportCli() {
77
80
  candidateHash: passport.candidateHash,
78
81
  platformCount: passport.platformMatrix.length,
79
82
  gateProfileEvidence: passport.gateProfileEvidence,
83
+ controllerReceipts: passport.controllerReceipts || [],
80
84
  }),
81
85
  });
82
86
  return passport;
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { createRequire } from "node:module";
7
7
  import { createBuildchainContractWorld } from "../packages/core/buildchain-contract.js";
8
+ import { createControllerRegistry } from "../packages/core/controller-evidence.js";
8
9
  import {
9
10
  BUILDCHAIN_AGENT_MANUALS,
10
11
  createBuildchainKfdClaimRegistry,
@@ -316,6 +317,7 @@ const manualMetaById = new Map(Object.entries({
316
317
  "product-mechanism": { capabilityGroup: "getting-started", audience: ["agent", "maintainer"], maturity: "stable", order: 30 },
317
318
  cli: { capabilityGroup: "api-cli-reference", audience: ["agent", "developer"], maturity: "stable", order: 40 },
318
319
  "release-passport": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 100 },
320
+ "controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
319
321
  "binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
320
322
  "publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
321
323
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
@@ -478,6 +480,7 @@ function nodeApiMeta(exportName) {
478
480
  "./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
479
481
  "./release-line-bootstrap": { group: "governance-versioning", summary: "Semver release-line bootstrap planning and version-state APIs." },
480
482
  "./buildchain-contract": { group: "governance-versioning", summary: "Runtime contract world and compatibility digest APIs for floating-ref drift checks." },
483
+ "./controller-evidence": { group: "reusable-build", summary: "Project-independent controller descriptors, source/runtime-bound plans, receipts, aggregates, and validation APIs." },
481
484
  "./surface-manifest": { group: "site-and-propagation", summary: "Surface manifest timestamp and reproducibility policy APIs." },
482
485
  "./issue-reporting": { group: "observability-diagnostics", summary: "Buildchain-owned issue reporting API for workflow friction feedback." },
483
486
  "./buildchain-layout": { group: "kfd-trust", summary: "Versioned Buildchain repository-layout discovery contract plus canonical .buildchain path resolution and migration APIs." },
@@ -753,6 +756,7 @@ function buildSiteBundle() {
753
756
  "docs/stable-candidate-patrol.md",
754
757
  "docs/release-governance.md",
755
758
  "docs/release-passport.md",
759
+ "docs/controller-evidence.md",
756
760
  "docs/publish-transaction.md",
757
761
  "docs/homebrew.md",
758
762
  "docs/site-bundle-contract.md",
@@ -862,6 +866,7 @@ function buildSiteBundle() {
862
866
  status: "active",
863
867
  })),
864
868
  };
869
+ const controllerRegistry = createControllerRegistry({ workflows: workflowRegistry.workflows });
865
870
  const publicSurfaceAudit = collectPublicSurfaceReverseAudit({
866
871
  root,
867
872
  cliRegistry,
@@ -949,6 +954,7 @@ function buildSiteBundle() {
949
954
  "manual-registry.json",
950
955
  "node-api-registry.json",
951
956
  "workflow-registry.json",
957
+ "controller-registry.json",
952
958
  "public-surface-audit.json",
953
959
  "release-model.json",
954
960
  "artifact-schemas.json",
@@ -1029,6 +1035,7 @@ function buildSiteBundle() {
1029
1035
  "manual-registry.json",
1030
1036
  "node-api-registry.json",
1031
1037
  "workflow-registry.json",
1038
+ "controller-registry.json",
1032
1039
  "public-surface-audit.json",
1033
1040
  "release-model.json",
1034
1041
  "artifact-schemas.json",
@@ -1055,6 +1062,7 @@ function buildSiteBundle() {
1055
1062
  "manual-registry.json",
1056
1063
  "node-api-registry.json",
1057
1064
  "workflow-registry.json",
1065
+ "controller-registry.json",
1058
1066
  "public-surface-audit.json",
1059
1067
  "artifact-schemas.json",
1060
1068
  "buildchain-contract.json",
@@ -1191,6 +1199,7 @@ function buildSiteBundle() {
1191
1199
  "capability-grouped navigation registry for docs, CLI, Node API, workflows, actions, and KFD claims",
1192
1200
  "release model facts",
1193
1201
  "workflow and action registries",
1202
+ "controller evidence descriptors and input classification",
1194
1203
  "CLI command registry",
1195
1204
  "public surface reverse audit",
1196
1205
  "manual and Node API registries",
@@ -1221,12 +1230,13 @@ function buildSiteBundle() {
1221
1230
  "manual-registry.json": manualRegistry,
1222
1231
  "node-api-registry.json": nodeApiRegistry,
1223
1232
  "workflow-registry.json": workflowRegistry,
1233
+ "controller-registry.json": controllerRegistry,
1224
1234
  "public-surface-audit.json": publicSurfaceAudit,
1225
1235
  "release-model.json": releaseModel,
1226
1236
  "artifact-schemas.json": artifactSchemas,
1227
1237
  "badge-endpoint-registry.json": badgeEndpointRegistry,
1228
1238
  "publication-registry.json": publicationRegistry,
1229
- "buildchain-contract.json": createBuildchainContractWorld({ root }),
1239
+ "buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
1230
1240
  "kfd-upstream-aggregate.json": collectKfdUpstreamFacts({ cwd: root, includeOwn: false }),
1231
1241
  "kfd-claims.json": createBuildchainKfdClaimRegistry({ root }),
1232
1242
  "product-mechanism.json": productMechanism,