@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.55

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 (159) hide show
  1. package/README.md +9 -6
  2. package/actions/release-tail/README.md +8 -0
  3. package/actions/v4-release-candidate-promote/README.md +3 -0
  4. package/architecture/agent-change-map.md +1015 -0
  5. package/architecture/ci-lane-change-budget.json +256 -0
  6. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  7. package/architecture/internal-capabilities.json +220 -15
  8. package/architecture/maintainability-debt.json +3229 -0
  9. package/architecture/maintainability-policy.json +489 -132
  10. package/architecture/release-tail-contract-inventory.json +5 -2
  11. package/architecture/v3-core-mechanism-inventory.json +4 -3
  12. package/architecture/v3-v4-capability-closure.md +59 -0
  13. package/architecture/v3-v4-live-capability-inventory.json +102404 -0
  14. package/architecture/v4-adopter-delivery-parity.json +32 -5
  15. package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
  16. package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
  17. package/architecture/v4-floating-consumer-policy.json +6 -0
  18. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  19. package/architecture/v4-runtime-semantic-closure.json +710 -0
  20. package/bin/buildchain.mjs +7 -1
  21. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  22. package/bin/internal/command-registry.mjs +1 -0
  23. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  24. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  25. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  26. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  27. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  28. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  29. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  30. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  31. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  32. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  33. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  34. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  35. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  36. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  37. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  38. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  39. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  40. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  41. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  42. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  43. package/dist/site/agent-index.json +4 -0
  44. package/dist/site/artifact-schemas.json +8 -0
  45. package/dist/site/buildchain-contract.json +125 -34
  46. package/dist/site/buildchain-site.json +308 -33
  47. package/dist/site/capability-registry.json +13 -12
  48. package/dist/site/cli-registry.json +57 -2
  49. package/dist/site/controller-registry.json +41 -5
  50. package/dist/site/kfd-claims.json +438 -24
  51. package/dist/site/kfd-upstream-aggregate.json +1 -1
  52. package/dist/site/manual-registry.json +22 -7
  53. package/dist/site/node-api-registry.json +12843 -9314
  54. package/dist/site/page-registry.json +288 -21
  55. package/dist/site/public-surface-audit.json +281 -21
  56. package/dist/site/publication-authority-registry.json +66 -8
  57. package/dist/site/publication-registry.json +4 -4
  58. package/dist/site/release-provenance.json +20 -0
  59. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  60. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  61. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  62. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  63. package/dist/site/site-manifest.json +19 -11
  64. package/dist/site/workflow-registry.json +195 -17
  65. package/docs/MAP.md +4 -2
  66. package/docs/cli-reference.md +70 -2
  67. package/docs/cli.md +8 -0
  68. package/docs/dev-delivery-warrant.md +34 -10
  69. package/docs/node-api-reference.md +524 -279
  70. package/docs/publish-transaction.md +11 -4
  71. package/docs/release-candidate.md +7 -0
  72. package/docs/release-tail-provider-plane.md +30 -0
  73. package/docs/v4-adopter-delivery.md +152 -0
  74. package/docs/v4-declarative-release-tail-migration.md +43 -0
  75. package/docs/v4-publication-rehearsal.md +130 -0
  76. package/package.json +24 -4
  77. package/packages/core/adopter-delivery-passport.js +295 -0
  78. package/packages/core/adopter-delivery-vectors.js +158 -0
  79. package/packages/core/adopter-delivery-vectors.json +77 -0
  80. package/packages/core/buildchain-agent-manuals.js +1 -0
  81. package/packages/core/buildchain-config.js +84 -0
  82. package/packages/core/buildchain-contract.js +31 -1
  83. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  84. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  85. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  86. package/packages/core/buildchain-publication-authority.js +2 -0
  87. package/packages/core/ci-lane-change-budget.js +247 -0
  88. package/packages/core/dev-delivery-native-proof.js +43 -27
  89. package/packages/core/dev-delivery-warrant-settlement.js +239 -3
  90. package/packages/core/dev-delivery-warrant-state.js +46 -10
  91. package/packages/core/dev-delivery-warrant.js +29 -7
  92. package/packages/core/github-governance-authority.js +5 -4
  93. package/packages/core/index.js +6 -0
  94. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  95. package/packages/core/paper-scaffold-content.js +401 -0
  96. package/packages/core/paper.js +12 -378
  97. package/packages/core/publication-control-plane-audit.js +135 -111
  98. package/packages/core/publication-rehearsal-projection.js +104 -0
  99. package/packages/core/publication-rehearsal-runtime.js +190 -0
  100. package/packages/core/published-delivery-authority.js +266 -0
  101. package/packages/core/release-candidate-recovery.js +71 -15
  102. package/packages/core/release-candidate.js +2 -2
  103. package/packages/core/release-passport.js +95 -170
  104. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  105. package/packages/core/v4-adopter-delivery.js +359 -0
  106. package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
  107. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  108. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  109. package/packages/core/v4-floating-consumer-policy.js +139 -35
  110. package/packages/core/v4-floating-consumer-release-passport.js +154 -8
  111. package/packages/core/v4-publication-qualification.js +222 -0
  112. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  113. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  114. package/packages/core/v4-publication-rehearsal.js +481 -0
  115. package/packages/core/workflow-yaml-contract.js +24 -2
  116. package/scripts/artifact-signing-controller-core.mjs +1 -1
  117. package/scripts/audit-publication-control-plane.mjs +17 -17
  118. package/scripts/buildchain-cli-help.mjs +13 -0
  119. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  120. package/scripts/check-internal-architecture.mjs +23 -3
  121. package/scripts/check-inventory.mjs +24 -6
  122. package/scripts/check-maintainability.mjs +145 -4
  123. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  124. package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
  125. package/scripts/check-v4-floating-consumer-policy-contract.mjs +30 -4
  126. package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
  127. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  128. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  129. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  130. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  131. package/scripts/dev-delivery-warrant.mjs +50 -6
  132. package/scripts/dev-pr-auto-merge.mjs +25 -11
  133. package/scripts/dev-pr-delivery-warrant.mjs +45 -0
  134. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  135. package/scripts/dev-qualification-patrol.mjs +9 -9
  136. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  137. package/scripts/generate-agent-change-map.mjs +118 -0
  138. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  139. package/scripts/generate-release-candidate-passport.mjs +177 -0
  140. package/scripts/generate-site-bundle.mjs +36 -0
  141. package/scripts/maintainability-domain-metrics.mjs +237 -0
  142. package/scripts/maintainability-governance.mjs +473 -0
  143. package/scripts/maintainability-metrics.mjs +28 -6
  144. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  145. package/scripts/promotion-channel-router.mjs +4 -4
  146. package/scripts/promotion-routing-evidence.mjs +35 -6
  147. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  148. package/scripts/release-candidate-resolver.mjs +44 -0
  149. package/scripts/release-line-policy.mjs +25 -15
  150. package/scripts/release-tail.mjs +68 -3
  151. package/scripts/resume-from-candidate-run.mjs +595 -65
  152. package/scripts/run-lifecycle-core.mjs +341 -376
  153. package/scripts/site-capability-metadata.mjs +21 -0
  154. package/scripts/stable-candidate-qualification.mjs +9 -10
  155. package/scripts/v3-v4-capability-catalog.mjs +439 -0
  156. package/scripts/v4-consumer-policy.mjs +4 -0
  157. package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
  158. package/scripts/v4-declarative-promotion-admission.mjs +42 -0
  159. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
@@ -0,0 +1,394 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const GIT_OID = /^[0-9a-f]{40}$/u;
8
+ const CONTENT_ROOT = /^sha256:[0-9a-f]{64}$/u;
9
+ const FAMILY_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
10
+ const FAMILY_MARKER =
11
+ /<!-- kungfu-family-queue-lease:v1 ([A-Za-z0-9_-]+) -->/gu;
12
+ const FAMILY_LEASE_SCHEMA = "kungfu.project-cut.family-queue-lease/v1";
13
+
14
+ export class ProjectCutAdmissionError extends Error {
15
+ constructor(reasonCode, message) {
16
+ super(message);
17
+ this.name = "ProjectCutAdmissionError";
18
+ this.reasonCode = reasonCode;
19
+ }
20
+ }
21
+
22
+ function stableValue(value) {
23
+ if (Array.isArray(value)) return value.map(stableValue);
24
+ if (value && typeof value === "object") {
25
+ return Object.fromEntries(
26
+ Object.entries(value)
27
+ .filter(([, entry]) => entry !== undefined)
28
+ .sort(([left], [right]) => left.localeCompare(right))
29
+ .map(([key, entry]) => [key, stableValue(entry)]),
30
+ );
31
+ }
32
+ return value;
33
+ }
34
+
35
+ function canonicalBytes(value) {
36
+ return Buffer.from(`${JSON.stringify(stableValue(value))}\n`, "utf8");
37
+ }
38
+
39
+ function contentRoot(value) {
40
+ return `sha256:${crypto.createHash("sha256").update(canonicalBytes(value)).digest("hex")}`;
41
+ }
42
+
43
+ function exactRoot(value, label) {
44
+ const normalized = String(value || "")
45
+ .trim()
46
+ .toLowerCase();
47
+ if (!CONTENT_ROOT.test(normalized))
48
+ throw new Error(`${label} must be an exact SHA-256 root`);
49
+ return normalized;
50
+ }
51
+
52
+ function familyId(value, label) {
53
+ const normalized = String(value || "").trim();
54
+ if (!FAMILY_ID.test(normalized)) throw new Error(`${label} is invalid`);
55
+ return normalized;
56
+ }
57
+
58
+ function git(
59
+ cwd,
60
+ args,
61
+ {
62
+ env = {},
63
+ input = undefined,
64
+ encoding = "utf8",
65
+ reason = "git-failed",
66
+ } = {},
67
+ ) {
68
+ const result = spawnSync("git", args, {
69
+ cwd,
70
+ env: { ...process.env, ...env },
71
+ input,
72
+ encoding,
73
+ maxBuffer: 64 * 1024 * 1024,
74
+ });
75
+ if (result.status !== 0) {
76
+ const detail = String(result.stderr || result.stdout || "").trim();
77
+ throw new ProjectCutAdmissionError(
78
+ reason,
79
+ `git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`,
80
+ );
81
+ }
82
+ return encoding === null ? result.stdout : String(result.stdout || "").trim();
83
+ }
84
+
85
+ function resolveCommit(cwd, revision, label) {
86
+ const oid = git(cwd, ["rev-parse", "--verify", `${revision}^{commit}`], {
87
+ reason: `${label}-unresolved`,
88
+ });
89
+ if (!GIT_OID.test(oid))
90
+ throw new ProjectCutAdmissionError(
91
+ `${label}-unresolved`,
92
+ `${label} is not an exact commit`,
93
+ );
94
+ return oid;
95
+ }
96
+
97
+ function linearSourceCommits(cwd, fork, head) {
98
+ const commits = git(
99
+ cwd,
100
+ ["rev-list", "--reverse", "--topo-order", `${fork}..${head}`],
101
+ {
102
+ reason: "source-history-unreadable",
103
+ },
104
+ )
105
+ .split(/\s+/u)
106
+ .filter(Boolean);
107
+ if (commits.length === 0)
108
+ throw new ProjectCutAdmissionError(
109
+ "empty-source-range",
110
+ "source range is empty",
111
+ );
112
+ let expectedParent = fork;
113
+ for (const commit of commits) {
114
+ const [observed, ...parents] = git(
115
+ cwd,
116
+ ["rev-list", "--parents", "-n", "1", commit],
117
+ {
118
+ reason: "source-history-unreadable",
119
+ },
120
+ ).split(/\s+/u);
121
+ if (
122
+ observed !== commit ||
123
+ parents.length !== 1 ||
124
+ parents[0] !== expectedParent
125
+ ) {
126
+ throw new ProjectCutAdmissionError(
127
+ "nonlinear-source-range",
128
+ "source range is not one immutable linear patch series",
129
+ );
130
+ }
131
+ expectedParent = commit;
132
+ }
133
+ return commits;
134
+ }
135
+
136
+ function patchId(cwd, patch) {
137
+ const output = git(cwd, ["patch-id", "--stable"], {
138
+ input: patch,
139
+ encoding: "utf8",
140
+ reason: "patch-id-failed",
141
+ });
142
+ const id = output.split(/\s+/u)[0] || "";
143
+ if (!GIT_OID.test(id))
144
+ throw new ProjectCutAdmissionError(
145
+ "patch-id-failed",
146
+ "patch has no stable identity",
147
+ );
148
+ return id;
149
+ }
150
+
151
+ function virtualCommitOid(cwd, tree, base, head) {
152
+ const body = [
153
+ `tree ${tree}`,
154
+ `parent ${base}`,
155
+ "author Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
156
+ "committer Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
157
+ "",
158
+ `Replay ${head} onto ${base}`,
159
+ "",
160
+ ].join("\n");
161
+ return git(cwd, ["hash-object", "-t", "commit", "--stdin"], {
162
+ input: body,
163
+ reason: "candidate-commit-hash-failed",
164
+ });
165
+ }
166
+
167
+ function replayProjectCut(cwd, base, head) {
168
+ const fork = git(cwd, ["merge-base", base, head], {
169
+ reason: "no-common-fork",
170
+ });
171
+ if (!GIT_OID.test(fork))
172
+ throw new ProjectCutAdmissionError(
173
+ "no-common-fork",
174
+ "base and head have no exact fork",
175
+ );
176
+ const commits = linearSourceCommits(cwd, fork, head);
177
+ const temporary = fs.mkdtempSync(
178
+ path.join(os.tmpdir(), "buildchain-project-cut-"),
179
+ );
180
+ const indexPath = path.join(temporary, "index");
181
+ const indexEnvironment = { GIT_INDEX_FILE: indexPath };
182
+ try {
183
+ git(cwd, ["read-tree", base], {
184
+ env: indexEnvironment,
185
+ reason: "base-tree-unreadable",
186
+ });
187
+ const sourcePatch = git(
188
+ cwd,
189
+ ["diff", "--binary", "--full-index", "--no-ext-diff", fork, head],
190
+ {
191
+ encoding: null,
192
+ reason: "source-patch-unreadable",
193
+ },
194
+ );
195
+ const emptySourcePatch = sourcePatch.length === 0;
196
+ if (emptySourcePatch && commits.length !== 1)
197
+ throw new ProjectCutAdmissionError("empty-source-composition", "an empty source composition must be exactly one linear commit");
198
+ if (!emptySourcePatch) git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
199
+ env: indexEnvironment, input: sourcePatch, encoding: "utf8", reason: "project-cut-conflict",
200
+ });
201
+ const candidateTreeOid = git(cwd, ["write-tree"], {
202
+ env: indexEnvironment,
203
+ reason: "candidate-tree-unreadable",
204
+ });
205
+ const replayPatch = git(
206
+ cwd,
207
+ ["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", base],
208
+ {
209
+ env: indexEnvironment,
210
+ encoding: null,
211
+ reason: "replay-patch-unreadable",
212
+ },
213
+ );
214
+ const sourceNames = git(
215
+ cwd,
216
+ ["diff", "--name-status", "--no-renames", fork, head],
217
+ {
218
+ reason: "source-paths-unreadable",
219
+ },
220
+ );
221
+ const replayNames = git(
222
+ cwd,
223
+ ["diff", "--cached", "--name-status", "--no-renames", base],
224
+ {
225
+ env: indexEnvironment,
226
+ reason: "replay-paths-unreadable",
227
+ },
228
+ );
229
+ const compositionDrifted = emptySourcePatch ? replayPatch.length !== 0 ||
230
+ sourceNames !== "" || replayNames !== "" : patchId(cwd, sourcePatch) !==
231
+ patchId(cwd, replayPatch) || sourceNames !== replayNames;
232
+ if (compositionDrifted) {
233
+ throw new ProjectCutAdmissionError(
234
+ "composition-drift",
235
+ "latest-base replay changes the immutable source composition",
236
+ );
237
+ }
238
+ return {
239
+ baseCommitOid: base,
240
+ headCommitOid: head,
241
+ forkCommitOid: fork,
242
+ candidateCommitOid: virtualCommitOid(cwd, candidateTreeOid, base, head),
243
+ candidateTreeOid,
244
+ replayedCommitCount: commits.length,
245
+ };
246
+ } finally {
247
+ fs.rmSync(temporary, { recursive: true, force: true });
248
+ }
249
+ }
250
+
251
+ function encodeMarker(payload) {
252
+ return canonicalBytes(payload)
253
+ .toString("base64")
254
+ .replace(/\+/gu, "-")
255
+ .replace(/\//gu, "_")
256
+ .replace(/=+$/gu, "");
257
+ }
258
+
259
+ function decodeMarker(token) {
260
+ const padded = `${token}${"=".repeat((4 - (token.length % 4)) % 4)}`
261
+ .replace(/-/gu, "+")
262
+ .replace(/_/gu, "/");
263
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
264
+ }
265
+
266
+ function familyLease(input, replay) {
267
+ const binding = {
268
+ schema: FAMILY_LEASE_SCHEMA,
269
+ state: "active",
270
+ initiativeId: familyId(input.initiativeId, "initiativeId"),
271
+ assignmentId: familyId(input.assignmentId, "assignmentId"),
272
+ deliveryClass: familyId(input.deliveryClass, "deliveryClass"),
273
+ admissionProofRoot: exactRoot(
274
+ input.admissionProofRoot,
275
+ "admissionProofRoot",
276
+ ),
277
+ queueAttempt: String(input.queueAttempt || "").trim(),
278
+ pullRequestHead: replay.headCommitOid,
279
+ baseCommitOid: replay.baseCommitOid,
280
+ forkCommitOid: replay.forkCommitOid,
281
+ candidateTreeOid: replay.candidateTreeOid,
282
+ };
283
+ const expectedAttempt = `${replay.headCommitOid}@${replay.baseCommitOid}`;
284
+ if (binding.queueAttempt !== expectedAttempt) {
285
+ throw new ProjectCutAdmissionError(
286
+ "queue-attempt-drift",
287
+ `queueAttempt must equal ${expectedAttempt}`,
288
+ );
289
+ }
290
+ const bindingRoot = contentRoot(binding);
291
+ const statusContext = `Queue family lease/${bindingRoot.slice(7, 23)}`;
292
+ const payload = { ...binding, bindingRoot, statusContext };
293
+ const leaseRoot = contentRoot(payload);
294
+ const sealed = { ...payload, leaseRoot };
295
+ return {
296
+ marker: `<!-- kungfu-family-queue-lease:v1 ${encodeMarker(sealed)} -->`,
297
+ leaseRoot,
298
+ statusContext,
299
+ pullRequestHead: replay.headCommitOid,
300
+ };
301
+ }
302
+
303
+ export function parseFamilyQueueLeaseMarker(text) {
304
+ const matches = [...String(text || "").matchAll(FAMILY_MARKER)];
305
+ if (matches.length !== 1) return null;
306
+ try {
307
+ const payload = decodeMarker(matches[0][1]);
308
+ const { leaseRoot, bindingRoot, statusContext, ...binding } = payload;
309
+ if (payload.schema !== FAMILY_LEASE_SCHEMA || payload.state !== "active")
310
+ return null;
311
+ if (contentRoot(binding) !== bindingRoot) return null;
312
+ if (statusContext !== `Queue family lease/${bindingRoot.slice(7, 23)}`)
313
+ return null;
314
+ if (contentRoot({ ...binding, bindingRoot, statusContext }) !== leaseRoot)
315
+ return null;
316
+ if (encodeMarker(payload) !== matches[0][1]) return null;
317
+ exactRoot(leaseRoot, "leaseRoot");
318
+ exactRoot(payload.admissionProofRoot, "admissionProofRoot");
319
+ for (const field of [
320
+ "pullRequestHead",
321
+ "baseCommitOid",
322
+ "forkCommitOid",
323
+ "candidateTreeOid",
324
+ ]) {
325
+ if (!GIT_OID.test(String(payload[field] || ""))) return null;
326
+ }
327
+ return payload;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ export function releaseFamilyQueueLease(markerText, input = {}) {
334
+ const lease = parseFamilyQueueLeaseMarker(markerText);
335
+ if (!lease) throw new Error("family queue lease marker is invalid");
336
+ const expectedHead = String(input.expectedPullRequestHead || "")
337
+ .trim()
338
+ .toLowerCase();
339
+ if (!GIT_OID.test(expectedHead) || expectedHead !== lease.pullRequestHead) {
340
+ throw new Error("family queue lease pull request head drifted");
341
+ }
342
+ const terminalReason = String(input.terminalReason || "").trim();
343
+ if (!["controller-failed", "merged"].includes(terminalReason))
344
+ throw new Error("terminalReason is invalid");
345
+ const evidenceRoot = input.evidenceRoot
346
+ ? exactRoot(input.evidenceRoot, "evidenceRoot")
347
+ : null;
348
+ if (terminalReason === "merged" && !evidenceRoot)
349
+ throw new Error("merged family lease requires evidenceRoot");
350
+ const receipt = {
351
+ schema: "project.cut.family-queue-release/v1",
352
+ state: "released",
353
+ leaseRoot: lease.leaseRoot,
354
+ pullRequestHead: lease.pullRequestHead,
355
+ terminalReason,
356
+ evidenceRoot,
357
+ };
358
+ return { ...receipt, releaseRoot: contentRoot(receipt) };
359
+ }
360
+
361
+ export function qualifyProjectCut(input = {}) {
362
+ const cwd = path.resolve(input.cwd || process.cwd());
363
+ const base = resolveCommit(cwd, input.base, "base");
364
+ const head = resolveCommit(cwd, input.head, "head");
365
+ const replay = replayProjectCut(cwd, base, head);
366
+ const familyInputs = [
367
+ input.initiativeId,
368
+ input.assignmentId,
369
+ input.deliveryClass,
370
+ input.queueAttempt,
371
+ input.admissionProofRoot,
372
+ ];
373
+ const familyCount = familyInputs.filter((value) =>
374
+ String(value || "").trim(),
375
+ ).length;
376
+ if (familyCount !== 0 && familyCount !== familyInputs.length) {
377
+ throw new ProjectCutAdmissionError(
378
+ "partial-family-binding",
379
+ "family admission inputs must be all present or all absent",
380
+ );
381
+ }
382
+ return {
383
+ schema: "project.cut.merge-queue-admission/v1",
384
+ ok: true,
385
+ decision: "qualified",
386
+ retryable: false,
387
+ compositionChanged: false,
388
+ reasonCodes: [],
389
+ ...replay,
390
+ ...(familyCount === familyInputs.length
391
+ ? { familyLease: familyLease(input, replay) }
392
+ : {}),
393
+ };
394
+ }
@@ -66,8 +66,8 @@ export function resolvePromotionChannel({
66
66
  }
67
67
  const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
68
68
  return {
69
- targetRef: intent.targetRef,
70
- publicationChannel: intent.publicationChannel,
69
+ targetRef: intent.targetRef, publicationChannel: intent.publicationChannel,
70
+ routerRef: normalized(routerRef), routerSha: normalized(routerSha).toLowerCase(),
71
71
  channel: intent.shellChannel,
72
72
  major: selected.major,
73
73
  shellRef,
@@ -99,8 +99,8 @@ function readPackageVersion(cwd) {
99
99
 
100
100
  function writeOutputs(file, result) {
101
101
  const outputs = {
102
- "target-ref": result.targetRef,
103
- "publication-channel": result.publicationChannel,
102
+ "target-ref": result.targetRef, "publication-channel": result.publicationChannel,
103
+ "router-ref": result.routerRef, "router-sha": result.routerSha,
104
104
  channel: result.channel,
105
105
  major: String(result.major),
106
106
  "shell-ref": result.shellRef,
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
4
  import { pathToFileURL } from "node:url";
6
-
7
5
  function createPromotionRoutingEvidence({
8
6
  env = process.env,
9
7
  cwd = process.cwd(),
@@ -55,13 +53,45 @@ function createPromotionRoutingEvidence({
55
53
  }
56
54
  return { routing, routingPath, candidatePath };
57
55
  }
58
-
56
+ function bundlePromotionControllerEvidence({
57
+ env = process.env,
58
+ cwd = process.cwd(),
59
+ } = {}) {
60
+ const finalizationNeeded = env.FINALIZATION_NEEDED === "true";
61
+ const files = [
62
+ [env.RELEASE_CANDIDATE_PASSPORT, "release-candidate-passport.json"],
63
+ [env.PUBLISH_EVIDENCE, "publish-evidence.json"],
64
+ ...(!finalizationNeeded
65
+ ? [[env.RELEASE_PASSPORT, "release-passport.json"]]
66
+ : []),
67
+ ...(env.PUBLICATION_COMMIT_ENABLED === "true" && !finalizationNeeded
68
+ ? [[env.PUBLICATION_COMMIT_EVIDENCE, "publication-commit-evidence.json"]]
69
+ : []),
70
+ ...(!finalizationNeeded ? [
71
+ [env.RELEASE_ACTIVATION_RECEIPTS, "release-activation-receipt-set.json"],
72
+ [env.RELEASED_PRODUCT_EVIDENCE, "released-product-evidence.json"],
73
+ [env.RELEASE_PROPAGATION_CAPTURE, "release-propagation-capture.json"],
74
+ ].filter(([source]) => source) : []),
75
+ ];
76
+ const outputDir = path.join(cwd, ".buildchain/controller/promotion-evidence");
77
+ fs.mkdirSync(outputDir, { recursive: true });
78
+ for (const [source, name] of files) {
79
+ if (!source || !fs.existsSync(path.resolve(cwd, source)))
80
+ throw new Error(`promotion controller evidence is missing: ${source || name}`);
81
+ fs.copyFileSync(path.resolve(cwd, source), path.join(outputDir, name));
82
+ }
83
+ return { files: files.map(([, name]) => name), outputDir };
84
+ }
59
85
  if (
60
86
  process.argv[1] &&
61
87
  import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
62
88
  ) {
63
89
  try {
64
- createPromotionRoutingEvidence();
90
+ if (process.argv[2] === "bundle-controller-evidence") {
91
+ bundlePromotionControllerEvidence();
92
+ } else {
93
+ createPromotionRoutingEvidence();
94
+ }
65
95
  } catch (error) {
66
96
  console.error(
67
97
  `promotion routing evidence failed: ${error instanceof Error ? error.message : String(error)}`,
@@ -69,5 +99,4 @@ if (
69
99
  process.exitCode = 1;
70
100
  }
71
101
  }
72
-
73
- export { createPromotionRoutingEvidence };
102
+ export { bundlePromotionControllerEvidence, createPromotionRoutingEvidence };
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+
3
+ export function resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId = "" } = {}) {
4
+ if (Number(passport?.pullRequest?.number || 0)) return undefined;
5
+ if (passport?.target?.channel !== "anchor" || passport?.target?.ref !== "publish-gate/anchor") {
6
+ throw new Error("Release Candidate Passport has no PR identity");
7
+ }
8
+ let releaseManifest;
9
+ try {
10
+ releaseManifest = JSON.parse(buildSummary?.publishSource?.releaseManifest || "");
11
+ } catch {
12
+ throw new Error("anchored candidate release manifest is invalid");
13
+ }
14
+ const request = releaseManifest?.anchorRequest;
15
+ if (
16
+ releaseManifest?.sourceRef !== "publish-gate/anchor" ||
17
+ releaseManifest?.sourceSha !== passport.source?.headSha ||
18
+ request?.schema !== 1 ||
19
+ request?.contract !== "kungfu-buildchain-explicit-publish-anchor-request/v1" ||
20
+ !request.assignmentId ||
21
+ !/^native:[0-9a-f-]+$/u.test(String(request.attemptId || "")) ||
22
+ request.transactionId !== transactionId ||
23
+ request.source?.sha !== passport.source?.headSha ||
24
+ request.source?.tree !== passport.source?.treeHash ||
25
+ !/^\d+$/u.test(String(request.supersededCandidate?.workflowRunId || "")) ||
26
+ !/^[0-9a-f]{40}$/u.test(String(request.supersededCandidate?.sha || "")) ||
27
+ !/^sha256:[0-9a-f]{64}$/u.test(String(request.supersededCandidate?.root || "")) ||
28
+ request.runtime?.sha !== passport.buildchain?.sha
29
+ ) {
30
+ throw new Error("anchored candidate request does not bind the sealed recovery identity");
31
+ }
32
+ return request;
33
+ }
34
+
35
+ function normalizeRun(run, repository) {
36
+ return {
37
+ id: String(run.id), repository,
38
+ headRepository: run.head_repository?.full_name || "",
39
+ status: run.status, conclusion: run.conclusion,
40
+ event: run.event, path: run.path, name: run.name,
41
+ headSha: run.head_sha || "", headBranch: run.head_branch || "",
42
+ pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
43
+ };
44
+ }
45
+
46
+ export function normalizeCandidateRun(run, repository) {
47
+ return normalizeRun(run, repository);
48
+ }
49
+
50
+ export function normalizeAnchorProvenance(anchorProvenance, repository) {
51
+ if (!anchorProvenance) return undefined;
52
+ return {
53
+ request: anchorProvenance.request,
54
+ run: normalizeRun(anchorProvenance.run, repository),
55
+ workflow: {
56
+ path: anchorProvenance.workflow.path,
57
+ name: anchorProvenance.workflow.name,
58
+ state: anchorProvenance.workflow.state,
59
+ },
60
+ passport: anchorProvenance.passport,
61
+ buildSummary: anchorProvenance.buildSummary,
62
+ };
63
+ }
64
+
65
+ export async function recoverCandidateProvenance({
66
+ passport, buildSummary, transactionId, repoInfo, artifactName,
67
+ expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
68
+ fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
69
+ downloadArtifact, readOnlyJson,
70
+ }) {
71
+ const request = resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId });
72
+ if (!request) {
73
+ return { anchorProvenance: undefined, provenancePassport: passport };
74
+ }
75
+ const runId = String(request.supersededCandidate.workflowRunId);
76
+ const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
77
+ const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
78
+ const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
79
+ const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
80
+ if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("superseded candidate run has more than 100 artifacts; complete pagination is required before recovery");
81
+ const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
82
+ const provenanceRoot = path.join(bundleRoot, "anchored-provenance");
83
+ const passportDownload = await downloadArtifact({ artifact: selected.passport, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
84
+ const summaryDownload = await downloadArtifact({ artifact: selected.summary, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
85
+ const recoveredPassport = readOnlyJson(passportDownload.files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "superseded release-candidate-passport.json");
86
+ const recoveredBuildSummary = readOnlyJson(summaryDownload.files.filter((file) => path.basename(file.path) === "build-summary.json"), "superseded build-summary.json");
87
+ if (
88
+ String(run.id) !== runId ||
89
+ recoveredPassport.workflow?.runId !== runId ||
90
+ recoveredPassport.workflow?.name !== expectedWorkflowName ||
91
+ String(workflow.path || run.path || "").split("@")[0].replace(/^\.github\/workflows\//u, "") !== expectedWorkflowFile.replace(/^\.github\/workflows\//u, "") ||
92
+ recoveredPassport.target?.channel !== channel ||
93
+ recoveredPassport.source?.headSha !== request.supersededCandidate.sha ||
94
+ recoveredPassport.source?.treeHash !== request.source.tree ||
95
+ `sha256:${recoveredPassport.candidateHash}` !== request.supersededCandidate.root
96
+ ) throw new Error("superseded candidate provenance does not match the anchored request");
97
+ const anchorProvenance = { request, run, workflow, passport: recoveredPassport, buildSummary: recoveredBuildSummary };
98
+ return { anchorProvenance, provenancePassport: recoveredPassport };
99
+ }
@@ -9,6 +9,8 @@ import { pipeline } from "node:stream/promises";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { createResolvedPublicationSealedBundle } from "./publication-candidate-sealer.mjs";
11
11
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
12
+ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
13
+ import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
12
14
 
13
15
  const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
14
16
 
@@ -434,6 +436,42 @@ function findDownloadedFile(root, filename) {
434
436
  return "";
435
437
  }
436
438
 
439
+ function resolveV4PublicationEvidence(passportDir, passport, outputPath) {
440
+ const stageCapsulesPath = findDownloadedFile(passportDir, "release-candidate-stage-capsules.json");
441
+ const publicationQualificationPath = findDownloadedFile(passportDir, "release-candidate-publication-qualification.json");
442
+ if (!passport.consumerPolicy?.receiptRoot) {
443
+ return {
444
+ paths: {
445
+ stageCapsules: stageCapsulesPath ? outputPath(stageCapsulesPath) : "",
446
+ publicationQualification: publicationQualificationPath ? outputPath(publicationQualificationPath) : "",
447
+ },
448
+ publicationQualificationRoot: "",
449
+ };
450
+ }
451
+ if (!stageCapsulesPath || !publicationQualificationPath) {
452
+ throw new Error("v4 release candidate is missing Stage Capsules or publication qualification receipt");
453
+ }
454
+ const stageCapsules = JSON.parse(fs.readFileSync(stageCapsulesPath, "utf8"));
455
+ const qualification = JSON.parse(fs.readFileSync(publicationQualificationPath, "utf8"));
456
+ validateV4PublicationQualificationReceipt(qualification, {
457
+ repository: passport.repository,
458
+ candidateRoot: `sha256:${passport.candidateHash}`,
459
+ sourceSha: passport.source.headSha,
460
+ sourceRoot: v4ContentRoot("candidate-identity", passport.source),
461
+ artifactRoot: v4PublicationQualificationRoot(stageCapsules.capsules.map(({ publicationArtifact }) => publicationArtifact)),
462
+ policyDigest: passport.consumerPolicy.receiptRoot,
463
+ });
464
+ if (stageCapsules.publicationQualificationRoot !== qualification.receiptRoot) throw new Error("Stage Capsules do not bind the publication qualification receipt");
465
+ if (stageCapsules.capsules.some(({ capsule }) => capsule.identity.qualificationRoot !== qualification.receiptRoot)) throw new Error("a Stage Capsule does not bind the publication qualification receipt");
466
+ return {
467
+ paths: {
468
+ stageCapsules: outputPath(stageCapsulesPath),
469
+ publicationQualification: outputPath(publicationQualificationPath),
470
+ },
471
+ publicationQualificationRoot: qualification.receiptRoot,
472
+ };
473
+ }
474
+
437
475
  export async function resolveReleaseCandidateArtifacts({
438
476
  repository,
439
477
  targetRef,
@@ -667,6 +705,7 @@ export async function resolveReleaseCandidateArtifacts({
667
705
  throw new Error("downloaded release-candidate artifacts did not contain release-candidate-passport.json and build-summary.json");
668
706
  }
669
707
  const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
708
+ const v4Publication = resolveV4PublicationEvidence(passportDir, passport, outputPath);
670
709
  const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
671
710
  const githubArtifactAttestationPolicyPaths = findDownloadedFiles(
672
711
  payloadDir,
@@ -722,6 +761,7 @@ export async function resolveReleaseCandidateArtifacts({
722
761
  paths: {
723
762
  passport: outputPath(passportPath),
724
763
  buildSummary: outputPath(buildSummaryPath),
764
+ ...v4Publication.paths,
725
765
  payloads: outputPath(payloadDir),
726
766
  platformManifests: platformManifestPaths.map(outputPath),
727
767
  githubArtifactAttestationPolicies: githubArtifactAttestationPolicyPaths.map(outputPath),
@@ -738,6 +778,7 @@ export async function resolveReleaseCandidateArtifacts({
738
778
  githubArtifactAttestationPolicyCount: githubArtifactAttestationPolicyPaths.length,
739
779
  npmTarballCount: npmTarballPaths.length,
740
780
  publishRequiredArtifacts: generatedRequiredArtifacts,
781
+ publicationQualificationRoot: v4Publication.publicationQualificationRoot,
741
782
  };
742
783
  }
743
784
 
@@ -766,6 +807,9 @@ export async function resolveReleaseCandidateArtifactsCli() {
766
807
  "promote-only-release-candidate": String(result.enabled === true),
767
808
  "release-candidate-passport-path": result.paths?.passport || "",
768
809
  "release-candidate-build-summary-path": result.paths?.buildSummary || "",
810
+ "release-candidate-stage-capsules-path": result.paths?.stageCapsules || "",
811
+ "release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
812
+ "release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
769
813
  "release-candidate-version": result.version || "",
770
814
  "release-candidate-source-sha": result.artifacts?.sourceSha || "",
771
815
  "release-candidate-artifact": result.artifacts?.passport || "",