@kungfu-tech/buildchain 4.0.0 → 4.0.1-alpha.10

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 (228) hide show
  1. package/AGENTS.md +13 -5
  2. package/actions/promote-buildchain-ref/README.md +11 -6
  3. package/actions/release-tail/README.md +8 -0
  4. package/architecture/ci-lane-change-budget.json +187 -0
  5. package/architecture/decisions/0002-next-development-transition.md +101 -0
  6. package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
  7. package/architecture/internal-capabilities.json +219 -7
  8. package/architecture/maintainability-policy.json +246 -29
  9. package/architecture/release-tail-contract-inventory.json +5 -2
  10. package/architecture/v3-core-mechanism-inventory.json +91 -0
  11. package/architecture/v4-adopter-delivery-parity.json +32 -5
  12. package/architecture/v4-capability-state-machine-manifest.json +73 -0
  13. package/architecture/v4-compatibility-facts-parity.json +77 -0
  14. package/architecture/v4-delivery-authority-parity.json +250 -0
  15. package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
  16. package/architecture/v4-floating-consumer-policy.json +81 -0
  17. package/architecture/v4-floating-consumer-policy.md +32 -0
  18. package/architecture/v4-next-development-parity.json +192 -0
  19. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  20. package/architecture/v4-release-train-parity.json +1 -0
  21. package/architecture/v4-runtime-ref-resume-authority.json +64 -0
  22. package/architecture/v4-stage-capsule-qualification.json +2 -2
  23. package/architecture/v4-tail-reseal-parity.json +150 -0
  24. package/bin/buildchain.mjs +32 -3
  25. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  26. package/bin/internal/command-registry.mjs +3 -0
  27. package/bin/internal/compatibility-facts-cli.mjs +101 -0
  28. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  29. package/contracts/dev-delivery-authority-v2.schema.json +662 -0
  30. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  31. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  32. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  33. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  34. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  35. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  36. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  37. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  38. package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
  39. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +59 -0
  40. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  41. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  42. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  43. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  44. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  45. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  46. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  47. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  48. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  49. package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
  50. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
  51. package/contracts/next-development-request-v1.schema.json +66 -0
  52. package/contracts/next-development-transition-v1.schema.json +292 -0
  53. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  54. package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
  55. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +107 -0
  56. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  57. package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
  58. package/contracts/v4-tail-reseal-v1.schema.json +276 -0
  59. package/dist/site/agent-index.json +5 -0
  60. package/dist/site/artifact-schemas.json +10 -0
  61. package/dist/site/buildchain-contract.json +1562 -44
  62. package/dist/site/buildchain-site.json +684 -42
  63. package/dist/site/capability-registry.json +17 -14
  64. package/dist/site/cli-registry.json +205 -3
  65. package/dist/site/controller-registry.json +62 -6
  66. package/dist/site/kfd-claims.json +551 -22
  67. package/dist/site/kfd-upstream-aggregate.json +1 -1
  68. package/dist/site/manual-registry.json +55 -9
  69. package/dist/site/node-api-registry.json +15247 -8690
  70. package/dist/site/page-registry.json +647 -29
  71. package/dist/site/public-surface-audit.json +876 -55
  72. package/dist/site/publication-authority-registry.json +44 -8
  73. package/dist/site/publication-registry.json +4 -4
  74. package/dist/site/release-provenance.json +26 -0
  75. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
  76. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  77. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  78. package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
  79. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  80. package/dist/site/site-manifest.json +37 -13
  81. package/dist/site/workflow-registry.json +200 -22
  82. package/docs/MAP.md +10 -2
  83. package/docs/cli-reference.md +404 -13
  84. package/docs/cli.md +8 -0
  85. package/docs/dev-alpha-candidate-patrol.md +8 -0
  86. package/docs/dev-delivery-qualification-landing-adr.md +251 -0
  87. package/docs/dev-delivery-warrant.md +361 -30
  88. package/docs/lifecycle-protocol.md +41 -0
  89. package/docs/next-development-transition.md +118 -0
  90. package/docs/node-api-reference.md +748 -322
  91. package/docs/release-tail-provider-plane.md +30 -0
  92. package/docs/reusable-build-surface.md +41 -0
  93. package/docs/runtime-train-validation.md +10 -0
  94. package/docs/v4-adopter-delivery.md +133 -0
  95. package/docs/v4-compatibility-facts.md +91 -0
  96. package/docs/v4-publication-rehearsal.md +130 -0
  97. package/docs/v4-runtime-ref-resume-authority.md +69 -0
  98. package/docs/v4-tail-reseal.md +73 -0
  99. package/docs/versioning.md +4 -2
  100. package/package.json +36 -6
  101. package/packages/core/adopter-delivery-passport.js +259 -0
  102. package/packages/core/adopter-delivery-vectors.js +158 -0
  103. package/packages/core/adopter-delivery-vectors.json +77 -0
  104. package/packages/core/artifact-signing.js +61 -0
  105. package/packages/core/buildchain-agent-manuals.js +3 -0
  106. package/packages/core/buildchain-compatibility-authority.js +336 -0
  107. package/packages/core/buildchain-compatibility-fact.js +394 -0
  108. package/packages/core/buildchain-compatibility-facts.json +217 -0
  109. package/packages/core/buildchain-compatibility-proof.js +566 -0
  110. package/packages/core/buildchain-config.js +150 -1
  111. package/packages/core/buildchain-contract.js +96 -143
  112. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  113. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  114. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  115. package/packages/core/buildchain-publication-authority.js +1 -0
  116. package/packages/core/channel-candidate.js +8 -0
  117. package/packages/core/channel-promotion-baseline.js +55 -0
  118. package/packages/core/ci-lane-change-budget.js +247 -0
  119. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  120. package/packages/core/dev-delivery-authority-candidate.js +270 -0
  121. package/packages/core/dev-delivery-authority-evidence.js +146 -0
  122. package/packages/core/dev-delivery-authority-landing.js +461 -0
  123. package/packages/core/dev-delivery-authority-observation.js +48 -0
  124. package/packages/core/dev-delivery-authority-qualification.js +591 -0
  125. package/packages/core/dev-delivery-authority-settlement.js +213 -0
  126. package/packages/core/dev-delivery-authority-state.js +583 -0
  127. package/packages/core/dev-delivery-candidate-identity.js +13 -0
  128. package/packages/core/dev-delivery-contract-surface.js +76 -0
  129. package/packages/core/dev-delivery-execution-failure.js +133 -0
  130. package/packages/core/dev-delivery-execution-transfer.js +592 -0
  131. package/packages/core/dev-delivery-landing-admission-core.js +119 -0
  132. package/packages/core/dev-delivery-landing-readback.js +598 -0
  133. package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
  134. package/packages/core/dev-delivery-landing-testing-port.js +6 -0
  135. package/packages/core/dev-delivery-native-execution.js +110 -0
  136. package/packages/core/dev-delivery-native-proof.js +562 -0
  137. package/packages/core/dev-delivery-process-boundary.js +563 -0
  138. package/packages/core/dev-delivery-proof.js +37 -3
  139. package/packages/core/dev-delivery-provider-attempt.js +127 -0
  140. package/packages/core/dev-delivery-provider-heartbeat.js +382 -0
  141. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  142. package/packages/core/dev-delivery-warrant-qualification.js +145 -0
  143. package/packages/core/dev-delivery-warrant-settlement.js +237 -36
  144. package/packages/core/dev-delivery-warrant-state.js +587 -0
  145. package/packages/core/dev-delivery-warrant.js +328 -370
  146. package/packages/core/github-governance-authority.js +1 -0
  147. package/packages/core/index.js +7 -0
  148. package/packages/core/kungfu-temporal-fact.js +557 -0
  149. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  150. package/packages/core/next-development-candidate-reservation.js +186 -0
  151. package/packages/core/next-development-controller.js +728 -0
  152. package/packages/core/next-development-projection.js +287 -0
  153. package/packages/core/next-development-transition.js +738 -0
  154. package/packages/core/paper-agent-entry.js +11 -5
  155. package/packages/core/paper.js +16 -5
  156. package/packages/core/publication-authority.js +5 -5
  157. package/packages/core/published-delivery-authority.js +266 -0
  158. package/packages/core/release-candidate-recovery.js +66 -11
  159. package/packages/core/release-candidate.js +79 -19
  160. package/packages/core/release-passport.js +271 -38
  161. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  162. package/packages/core/v4-adopter-delivery.js +359 -0
  163. package/packages/core/v4-canonical-contracts.js +5 -0
  164. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  165. package/packages/core/v4-floating-consumer-evidence.js +338 -0
  166. package/packages/core/v4-floating-consumer-policy.js +569 -0
  167. package/packages/core/v4-floating-consumer-release-passport.js +153 -0
  168. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  169. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  170. package/packages/core/v4-publication-rehearsal.js +481 -0
  171. package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
  172. package/packages/core/v4-runtime-selector-persistence.js +228 -0
  173. package/packages/core/v4-tail-reseal-contract.js +26 -0
  174. package/packages/core/v4-tail-reseal-github.js +178 -0
  175. package/packages/core/v4-tail-reseal-receipt.js +225 -0
  176. package/packages/core/v4-tail-reseal.js +594 -0
  177. package/packages/core/workflow-yaml-contract.js +72 -2
  178. package/scripts/audit-publication-control-plane.mjs +31 -31
  179. package/scripts/buildchain-cli-help.mjs +41 -2
  180. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  181. package/scripts/check-inventory.mjs +14 -3
  182. package/scripts/check-v4-floating-consumer-policy-contract.mjs +217 -0
  183. package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
  184. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  185. package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
  186. package/scripts/dev-delivery-authority-provider.mjs +28 -0
  187. package/scripts/dev-delivery-authority.mjs +490 -0
  188. package/scripts/dev-delivery-native-run.mjs +177 -0
  189. package/scripts/dev-delivery-process-boundary.mjs +260 -0
  190. package/scripts/dev-delivery-proof.mjs +67 -2
  191. package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
  192. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  193. package/scripts/dev-delivery-source-proof-reuse.mjs +547 -0
  194. package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
  195. package/scripts/dev-delivery-two-phase.mjs +573 -0
  196. package/scripts/dev-delivery-warrant-options.mjs +266 -0
  197. package/scripts/dev-delivery-warrant-store.mjs +231 -0
  198. package/scripts/dev-delivery-warrant.mjs +232 -194
  199. package/scripts/dev-pr-auto-merge.mjs +37 -48
  200. package/scripts/dev-pr-delivery-warrant.mjs +216 -2
  201. package/scripts/dev-pr-prequeue-guard.mjs +399 -0
  202. package/scripts/dev-qualification-patrol.mjs +9 -9
  203. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  204. package/scripts/ensure-github-release.mjs +3 -3
  205. package/scripts/generate-channel-build-workflow.mjs +3 -0
  206. package/scripts/generate-channel-promotion-workflow.mjs +114 -14
  207. package/scripts/generate-next-development-guidance.mjs +49 -0
  208. package/scripts/generate-release-candidate-passport.mjs +192 -32
  209. package/scripts/generate-site-bundle.mjs +69 -4
  210. package/scripts/init-repo.mjs +26 -4
  211. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  212. package/scripts/next-development-self-dogfood-harness.mjs +460 -0
  213. package/scripts/next-development-self-dogfood.mjs +597 -0
  214. package/scripts/next-development-transition.mjs +47 -0
  215. package/scripts/npm-publish-transaction.mjs +103 -91
  216. package/scripts/promotion-channel-router.mjs +4 -4
  217. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  218. package/scripts/release-tail.mjs +51 -3
  219. package/scripts/release-train-self-dogfood.mjs +94 -92
  220. package/scripts/resume-from-candidate-run.mjs +565 -41
  221. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  222. package/scripts/site-capability-metadata.mjs +43 -0
  223. package/scripts/stable-candidate-qualification.mjs +9 -10
  224. package/scripts/v4-consumer-policy.mjs +231 -0
  225. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
  226. package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
  227. package/scripts/v4-tail-reseal.mjs +416 -0
  228. package/templates/native-dev-delivery.yml +143 -0
@@ -0,0 +1,569 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ createBuildchainContractWorld,
6
+ evaluateBuildchainContractLock,
7
+ } from "./buildchain-contract.js";
8
+ import { parseYamlUses } from "./workflow-yaml-contract.js";
9
+ import {
10
+ V4_FLOATING_CONSUMER_POLICY,
11
+ V4_FLOATING_CONSUMER_RECEIPT,
12
+ v4FloatingConsumerDocumentRoot,
13
+ } from "./v4-floating-consumer-evidence.js";
14
+
15
+ export {
16
+ V4_FLOATING_CONSUMER_CERTIFICATION,
17
+ V4_FLOATING_CONSUMER_POLICY,
18
+ V4_FLOATING_CONSUMER_RECEIPT,
19
+ certifyV4FloatingConsumerPolicyReceipt,
20
+ v4FloatingConsumerDocumentRoot,
21
+ verifyV4FloatingConsumerPolicyCertification,
22
+ verifyV4FloatingConsumerPolicyReceipt,
23
+ } from "./v4-floating-consumer-evidence.js";
24
+
25
+ const EXACT_SHA = /^[0-9a-f]{40}$/u;
26
+ const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
27
+ const BUILDCHAIN_REPOSITORY = "kungfu-systems/buildchain";
28
+ const CHANNELS = Object.freeze({ v4: "stable", "v4-alpha": "alpha" });
29
+ const ALPHA_RECOVERY_BOOTSTRAP = Object.freeze({
30
+ selector: "alpha/v4/v4.0",
31
+ sourcePath: ".github/workflows/buildchain-ref-promotion-recovery.yml",
32
+ workflow: ".github/workflows/.release-candidate-promote.yml",
33
+ });
34
+ const DEFAULT_STABLE_LOCK_PATH = ".buildchain/contract-lock.json";
35
+ const DEFAULT_ALPHA_LOCK_PATH = ".buildchain/alpha-contract-lock.json";
36
+ const TRANSIENT_ACTION =
37
+ /^\.\/\.buildchain\/(?:runtime|workflow-shell|attester-runtime|release-tail-runtime)\//u;
38
+ const SCANNER_PATHS = Object.freeze([
39
+ "architecture/v4-floating-consumer-policy.json",
40
+ "contracts/v4-floating-consumer-policy-receipt-v1.schema.json",
41
+ "packages/core/buildchain-contract.js",
42
+ "packages/core/v4-floating-consumer-policy.js",
43
+ "packages/core/workflow-yaml-contract.js",
44
+ "scripts/v4-consumer-policy.mjs",
45
+ ]);
46
+
47
+ function defaultRuntimeRoot() {
48
+ return typeof import.meta.dirname === "string"
49
+ ? path.resolve(import.meta.dirname, "../..")
50
+ : process.cwd();
51
+ }
52
+
53
+ function stableJson(value) {
54
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
55
+ if (value && typeof value === "object") {
56
+ return `{${Object.keys(value)
57
+ .sort()
58
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
59
+ .join(",")}}`;
60
+ }
61
+ return JSON.stringify(value);
62
+ }
63
+
64
+ function sha256(value) {
65
+ return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
66
+ }
67
+
68
+ function resolveInside(root, relative, label) {
69
+ const resolvedRoot = path.resolve(root);
70
+ const file = path.resolve(resolvedRoot, relative);
71
+ if (!file.startsWith(`${resolvedRoot}${path.sep}`))
72
+ throw new Error(`${label} escapes caller root: ${relative}`);
73
+ return file;
74
+ }
75
+
76
+ export function v4ConsumerPolicyScannerRoot(
77
+ runtimeRoot = defaultRuntimeRoot(),
78
+ ) {
79
+ return sha256(
80
+ stableJson(
81
+ SCANNER_PATHS.map((relative) => ({
82
+ path: relative,
83
+ digest: sha256(fs.readFileSync(path.join(runtimeRoot, relative))),
84
+ })),
85
+ ),
86
+ );
87
+ }
88
+
89
+ export function resolveV4FloatingConsumerPolicyAuthority({
90
+ runtimeRoot = defaultRuntimeRoot(),
91
+ callerRoot = process.cwd(),
92
+ stableLockPath = DEFAULT_STABLE_LOCK_PATH,
93
+ alphaLockPath = DEFAULT_ALPHA_LOCK_PATH,
94
+ } = {}) {
95
+ const policy = readJson(
96
+ runtimeRoot,
97
+ "architecture/v4-floating-consumer-policy.json",
98
+ "v4 floating consumer policy",
99
+ ).value;
100
+ const readLock = (relative, label) => {
101
+ const file = resolveInside(callerRoot, relative, `${label} contract lock`);
102
+ if (!fs.existsSync(file))
103
+ throw new Error(`${label} contract lock is missing: ${relative}`);
104
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
105
+ return { path: relative, root: v4FloatingConsumerDocumentRoot(value) };
106
+ };
107
+ return {
108
+ policy,
109
+ policyRoot: v4FloatingConsumerDocumentRoot(policy),
110
+ scannerRoot: v4ConsumerPolicyScannerRoot(runtimeRoot),
111
+ contractLocks: {
112
+ stable: readLock(stableLockPath, "stable"),
113
+ alpha: readLock(alphaLockPath, "alpha"),
114
+ },
115
+ };
116
+ }
117
+
118
+ function readJson(root, relative, label) {
119
+ const file = path.resolve(root, relative);
120
+ if (
121
+ !file.startsWith(`${path.resolve(root)}${path.sep}`) ||
122
+ !fs.existsSync(file)
123
+ ) {
124
+ throw new Error(`${label} is missing: ${relative}`);
125
+ }
126
+ try {
127
+ return { value: JSON.parse(fs.readFileSync(file, "utf8")), file };
128
+ } catch (error) {
129
+ throw new Error(`${label} is invalid JSON: ${error.message}`);
130
+ }
131
+ }
132
+
133
+ function listWorkflowFiles(root) {
134
+ const directory = path.join(root, ".github", "workflows");
135
+ if (!fs.existsSync(directory)) return [];
136
+ return fs
137
+ .readdirSync(directory, { withFileTypes: true })
138
+ .filter((entry) => entry.isFile() && /\.ya?ml$/u.test(entry.name))
139
+ .map((entry) => `.github/workflows/${entry.name}`)
140
+ .sort();
141
+ }
142
+
143
+ function coordinate(value) {
144
+ const match = String(value || "").match(/^([^/]+\/[^/]+)\/(.+)@([^@]+)$/u);
145
+ return match
146
+ ? { repository: match[1], path: match[2], selector: match[3] }
147
+ : null;
148
+ }
149
+
150
+ function localActionManifest(root, sourcePath, uses) {
151
+ if (!uses.startsWith("./")) return "";
152
+ if (TRANSIENT_ACTION.test(uses)) return "";
153
+ const target = path.resolve(root, uses);
154
+ if (!target.startsWith(`${path.resolve(root)}${path.sep}`)) return "";
155
+ const candidates =
156
+ fs.existsSync(target) && fs.statSync(target).isDirectory()
157
+ ? [path.join(target, "action.yml"), path.join(target, "action.yaml")]
158
+ : [target];
159
+ const manifest = candidates.find((candidate) => fs.existsSync(candidate));
160
+ if (!manifest) {
161
+ throw new Error(`${sourcePath} references missing local action ${uses}`);
162
+ }
163
+ return path.relative(root, manifest).split(path.sep).join("/");
164
+ }
165
+
166
+ function enumerateUses(root) {
167
+ const queue = listWorkflowFiles(root);
168
+ const visited = new Set();
169
+ const records = [];
170
+ while (queue.length) {
171
+ const relative = queue.shift();
172
+ if (visited.has(relative)) continue;
173
+ visited.add(relative);
174
+ const text = fs.readFileSync(path.join(root, relative), "utf8");
175
+ for (const node of parseYamlUses(text)) {
176
+ records.push({
177
+ sourcePath: relative,
178
+ line: node.line,
179
+ scalarKind: node.scalar.kind,
180
+ uses: node.value,
181
+ });
182
+ const manifest = localActionManifest(root, relative, node.value);
183
+ if (manifest && !visited.has(manifest)) queue.push(manifest);
184
+ }
185
+ }
186
+ return {
187
+ files: [...visited].sort().map((relative) => ({
188
+ path: relative,
189
+ digest: sha256(fs.readFileSync(path.join(root, relative))),
190
+ })),
191
+ records,
192
+ };
193
+ }
194
+
195
+ function validateLock(lock, expectedRef, label, failures) {
196
+ if (lock?.contract !== "kungfu-buildchain-contract-lock") {
197
+ failures.push({
198
+ code: `${label}-contract-invalid`,
199
+ message: `${label} must be a Buildchain contract lock`,
200
+ });
201
+ return;
202
+ }
203
+ if (lock.buildchain?.ref !== expectedRef) {
204
+ failures.push({
205
+ code: `${label}-channel-mismatch`,
206
+ message: `${label} must bind ${expectedRef}`,
207
+ });
208
+ }
209
+ if (lock.buildchain?.majorLine !== "v4") {
210
+ failures.push({
211
+ code: `${label}-major-mismatch`,
212
+ message: `${label} must bind major line v4`,
213
+ });
214
+ }
215
+ if (
216
+ !EXACT_SHA.test(String(lock.buildchain?.resolvedSha || "").toLowerCase())
217
+ ) {
218
+ failures.push({
219
+ code: `${label}-sha-invalid`,
220
+ message: `${label} resolvedSha must be an exact commit SHA`,
221
+ });
222
+ }
223
+ for (const field of ["contractDigest", "compatibilityDigest"]) {
224
+ if (!SHA256_ROOT.test(String(lock.buildchain?.[field] || ""))) {
225
+ failures.push({
226
+ code: `${label}-${field}-invalid`,
227
+ message: `${label} ${field} must be a sha256 root`,
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ function selectorFailure(record, selector) {
234
+ const location = `${record.sourcePath}:${record.line}`;
235
+ if (record.scalarKind === "expression" || selector.includes("${{")) {
236
+ return {
237
+ code: "persistent-selector-indirection",
238
+ message: `${location} uses repository/input/environment indirection for a persisted Buildchain selector`,
239
+ };
240
+ }
241
+ if (EXACT_SHA.test(selector.toLowerCase())) {
242
+ return {
243
+ code: "persistent-exact-sha-selector",
244
+ message: `${location} persists an exact Buildchain SHA; use v4 or v4-alpha plus contract locks`,
245
+ };
246
+ }
247
+ return {
248
+ code: "unapproved-v4-selector",
249
+ message: `${location} uses ${selector || "<empty>"}; approved persisted selectors are v4 and v4-alpha`,
250
+ };
251
+ }
252
+
253
+ function normalizeWorkflowPath(value) {
254
+ return `.github/workflows/${String(value || "").replace(/^\/+|^\.github\/workflows\//gu, "")}`;
255
+ }
256
+
257
+ function validateScanIdentity(
258
+ { repository, sourceSha, runtimeSha, workflowSha },
259
+ failures,
260
+ ) {
261
+ for (const [value, code] of [
262
+ [sourceSha, "caller-source-sha-invalid"],
263
+ [runtimeSha, "resolved-runtime-sha-invalid"],
264
+ [workflowSha, "resolved-workflow-sha-invalid"],
265
+ ])
266
+ if (!EXACT_SHA.test(value))
267
+ failures.push({ code, message: `${code} must bind an exact commit` });
268
+ if (!repository || !/^[^/]+\/[^/]+$/u.test(repository))
269
+ failures.push({
270
+ code: "caller-repository-invalid",
271
+ message: "caller repository must be owner/repo",
272
+ });
273
+ }
274
+
275
+ function readContractLock(root, relative, expectedRef, label, failures) {
276
+ try {
277
+ const lock = readJson(root, relative, `${label} contract lock`).value;
278
+ validateLock(lock, expectedRef, `${label}-lock`, failures);
279
+ return lock;
280
+ } catch (error) {
281
+ failures.push({ code: `${label}-lock-missing`, message: error.message });
282
+ }
283
+ }
284
+
285
+ function classifyBuildchainUses(records, failures, { repository } = {}) {
286
+ const uses = [];
287
+ for (const record of records) {
288
+ const parsed = coordinate(record.uses);
289
+ if (!parsed || parsed.repository !== BUILDCHAIN_REPOSITORY) continue;
290
+ const v4Candidate =
291
+ parsed.selector === "v4" ||
292
+ parsed.selector === "v4-alpha" ||
293
+ /^v4(?:[./-]|$)/u.test(parsed.selector) ||
294
+ parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector ||
295
+ EXACT_SHA.test(parsed.selector.toLowerCase()) ||
296
+ parsed.selector.includes("${{");
297
+ if (!v4Candidate) continue;
298
+ const protectedBootstrap =
299
+ repository === BUILDCHAIN_REPOSITORY &&
300
+ normalizeWorkflowPath(parsed.path) ===
301
+ ALPHA_RECOVERY_BOOTSTRAP.workflow &&
302
+ record.sourcePath === ALPHA_RECOVERY_BOOTSTRAP.sourcePath &&
303
+ parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector;
304
+ const channel =
305
+ CHANNELS[parsed.selector] || (protectedBootstrap ? "alpha" : "");
306
+ uses.push({
307
+ ...record,
308
+ ...parsed,
309
+ channel,
310
+ selectorClass: protectedBootstrap
311
+ ? "protected-bootstrap"
312
+ : channel
313
+ ? "floating"
314
+ : "rejected",
315
+ });
316
+ if (!channel) failures.push(selectorFailure(record, parsed.selector));
317
+ }
318
+ return uses;
319
+ }
320
+
321
+ function selectInvocation(uses, workflow, failures, source = "", channel = "") {
322
+ const expectedWorkflow = normalizeWorkflowPath(workflow);
323
+ const invocations = uses.filter(
324
+ (entry) =>
325
+ normalizeWorkflowPath(entry.path) === expectedWorkflow &&
326
+ (!source || entry.sourcePath === source) &&
327
+ (!channel || entry.channel === channel),
328
+ );
329
+ if (invocations.length !== 1) {
330
+ failures.push({
331
+ code: invocations.length
332
+ ? "invoked-workflow-ambiguous"
333
+ : "invoked-workflow-not-found",
334
+ message: `caller source must contain exactly one ${expectedWorkflow} Buildchain v4 invocation`,
335
+ });
336
+ }
337
+ return { expectedWorkflow, selected: invocations[0] };
338
+ }
339
+
340
+ function scanInvocationSource({
341
+ sourceRoot,
342
+ invocationRoot,
343
+ sourceScan,
344
+ sourceUses,
345
+ failures,
346
+ repository,
347
+ }) {
348
+ if (invocationRoot === sourceRoot)
349
+ return { invocationScan: sourceScan, invocationUses: sourceUses };
350
+ try {
351
+ const invocationScan = enumerateUses(invocationRoot);
352
+ return {
353
+ invocationScan,
354
+ invocationUses: classifyBuildchainUses(invocationScan.records, failures, {
355
+ repository,
356
+ }),
357
+ };
358
+ } catch (error) {
359
+ failures.push({
360
+ code: "invocation-source-scan-failed",
361
+ message: error.message,
362
+ });
363
+ return {
364
+ invocationScan: { files: [], records: [] },
365
+ invocationUses: [],
366
+ };
367
+ }
368
+ }
369
+
370
+ function sourceScanRoot({
371
+ sourceRoot,
372
+ invocationRoot,
373
+ sourceScan,
374
+ invocationScan,
375
+ invocationUses,
376
+ }) {
377
+ const material =
378
+ invocationRoot === sourceRoot
379
+ ? sourceScan
380
+ : {
381
+ source: sourceScan,
382
+ invocation: {
383
+ files: invocationScan.files,
384
+ invocations: invocationUses,
385
+ },
386
+ };
387
+ return sha256(stableJson(material));
388
+ }
389
+
390
+ function verifySelectedLock({
391
+ selected,
392
+ stable,
393
+ alpha,
394
+ workflowSha,
395
+ failures,
396
+ }) {
397
+ const selectedLock = selected?.channel === "alpha" ? alpha : stable;
398
+ if (!selected?.channel || !selectedLock) return;
399
+ const authorityRef =
400
+ selected.selectorClass === "protected-bootstrap"
401
+ ? "v4-alpha"
402
+ : selected.selector;
403
+ try {
404
+ const compatibility = evaluateBuildchainContractLock({
405
+ lock: selectedLock,
406
+ current: createBuildchainContractWorld({ root: defaultRuntimeRoot() }),
407
+ runtimeRef: authorityRef,
408
+ runtimeSha: workflowSha,
409
+ runtimeClass: selected.channel,
410
+ workflowShellRef: authorityRef,
411
+ expectedChannel: selected.channel,
412
+ expectedMajor: "v4",
413
+ });
414
+ if (!compatibility.ok) {
415
+ failures.push({
416
+ code: "selected-lock-runtime-incompatible",
417
+ message: `${selected.channel} contract lock rejects resolved workflow shell ${workflowSha}: ${compatibility.reasons?.join("; ") || compatibility.status}`,
418
+ });
419
+ }
420
+ } catch (error) {
421
+ failures.push({
422
+ code: "selected-lock-contract-world-invalid",
423
+ message: `cannot evaluate ${selected.channel} contract lock against the resolved workflow shell: ${error.message}`,
424
+ });
425
+ }
426
+ }
427
+
428
+ export function scanV4FloatingConsumerPolicy({
429
+ root = process.cwd(),
430
+ invocationRoot = root,
431
+ repository = "",
432
+ sourceSha = "",
433
+ invokedWorkflow = "",
434
+ invocationSourcePath = "",
435
+ expectedInvocationChannel = "",
436
+ resolvedWorkflowSha = "",
437
+ resolvedRuntimeSha = "",
438
+ stableLockPath = ".buildchain/contract-lock.json",
439
+ alphaLockPath = ".buildchain/alpha-contract-lock.json",
440
+ policy,
441
+ scannerRoot = "",
442
+ } = {}) {
443
+ const resolvedRoot = path.resolve(root);
444
+ const resolvedInvocationRoot = path.resolve(invocationRoot);
445
+ if (!policy || policy.contract !== V4_FLOATING_CONSUMER_POLICY) {
446
+ throw new Error(`policy must use ${V4_FLOATING_CONSUMER_POLICY}`);
447
+ }
448
+ const failures = [];
449
+ const normalizedSourceSha = String(sourceSha || "").toLowerCase();
450
+ const normalizedRuntimeSha = String(resolvedRuntimeSha || "").toLowerCase();
451
+ const normalizedWorkflowSha = String(
452
+ resolvedWorkflowSha || resolvedRuntimeSha || "",
453
+ ).toLowerCase();
454
+ validateScanIdentity(
455
+ {
456
+ repository,
457
+ sourceSha: normalizedSourceSha,
458
+ runtimeSha: normalizedRuntimeSha,
459
+ workflowSha: normalizedWorkflowSha,
460
+ },
461
+ failures,
462
+ );
463
+ const stable = readContractLock(
464
+ resolvedRoot,
465
+ stableLockPath,
466
+ "v4",
467
+ "stable",
468
+ failures,
469
+ );
470
+ const alpha = readContractLock(
471
+ resolvedRoot,
472
+ alphaLockPath,
473
+ "v4-alpha",
474
+ "alpha",
475
+ failures,
476
+ );
477
+
478
+ let scanned = { files: [], records: [] };
479
+ try {
480
+ scanned = enumerateUses(resolvedRoot);
481
+ } catch (error) {
482
+ failures.push({ code: "source-scan-failed", message: error.message });
483
+ }
484
+ const buildchainUses = classifyBuildchainUses(scanned.records, failures, {
485
+ repository,
486
+ });
487
+ const { invocationScan, invocationUses } = scanInvocationSource({
488
+ sourceRoot: resolvedRoot,
489
+ invocationRoot: resolvedInvocationRoot,
490
+ sourceScan: scanned,
491
+ sourceUses: buildchainUses,
492
+ failures,
493
+ repository,
494
+ });
495
+ const sourcePath = String(invocationSourcePath || "")
496
+ .split("@")[0]
497
+ .replace(`${repository}/`, "");
498
+ const { expectedWorkflow, selected } = selectInvocation(
499
+ invocationUses,
500
+ invokedWorkflow,
501
+ failures,
502
+ sourcePath,
503
+ expectedInvocationChannel,
504
+ );
505
+ verifySelectedLock({
506
+ selected,
507
+ stable,
508
+ alpha,
509
+ workflowSha: normalizedWorkflowSha,
510
+ failures,
511
+ });
512
+
513
+ const policyRoot = sha256(stableJson(policy));
514
+ const resolvedScannerRoot = scannerRoot || policyRoot;
515
+ if (!SHA256_ROOT.test(resolvedScannerRoot)) {
516
+ failures.push({
517
+ code: "scanner-root-invalid",
518
+ message: "scanner root must be a sha256 root",
519
+ });
520
+ }
521
+ const contractLockRoots = {
522
+ stable: stable ? sha256(stableJson(stable)) : "",
523
+ alpha: alpha ? sha256(stableJson(alpha)) : "",
524
+ };
525
+ const sourceScan = { files: scanned.files, invocations: buildchainUses };
526
+ const scanRoot = sourceScanRoot({
527
+ sourceRoot: resolvedRoot,
528
+ invocationRoot: resolvedInvocationRoot,
529
+ sourceScan,
530
+ invocationScan,
531
+ invocationUses,
532
+ });
533
+ const receipt = {
534
+ schema: V4_FLOATING_CONSUMER_RECEIPT,
535
+ status: failures.length ? "failed" : "passed",
536
+ caller: { repository, sourceSha: normalizedSourceSha },
537
+ invocation: {
538
+ workflow: expectedWorkflow,
539
+ sourcePath: selected?.sourcePath || "",
540
+ sourceLine: selected?.line || 0,
541
+ visibleSelector: selected?.selector || "",
542
+ selectorClass: selected?.selectorClass || "",
543
+ channel: selected?.channel || "",
544
+ resolvedWorkflowSha: normalizedWorkflowSha,
545
+ resolvedRuntimeSha: normalizedRuntimeSha,
546
+ },
547
+ contractLocks: {
548
+ stable: { path: stableLockPath, root: contractLockRoots.stable },
549
+ alpha: { path: alphaLockPath, root: contractLockRoots.alpha },
550
+ },
551
+ policy: {
552
+ version: policy.policyVersion,
553
+ root: policyRoot,
554
+ scannerRoot: resolvedScannerRoot,
555
+ sourceScanRoot: scanRoot,
556
+ },
557
+ failures,
558
+ };
559
+ const receiptRoot = sha256(stableJson(receipt));
560
+ return {
561
+ schema: "kungfu-buildchain-v4-floating-consumer-policy-check/v1",
562
+ ok: failures.length === 0,
563
+ failures,
564
+ scannedFiles: scanned.files,
565
+ invocations: invocationUses,
566
+ receipt,
567
+ receiptRoot,
568
+ };
569
+ }