@kungfu-tech/buildchain 2.12.9-alpha.0 → 2.13.0-alpha.0

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 (37) hide show
  1. package/actions/run-lifecycle/README.md +5 -0
  2. package/bin/buildchain.mjs +62 -0
  3. package/dist/site/buildchain-contract.json +33 -23
  4. package/dist/site/buildchain-site.json +66 -30
  5. package/dist/site/capability-registry.json +3 -3
  6. package/dist/site/cli-registry.json +24 -0
  7. package/dist/site/controller-registry.json +11 -3
  8. package/dist/site/kfd-claims.json +64 -13
  9. package/dist/site/kfd-upstream-aggregate.json +1 -1
  10. package/dist/site/manual-registry.json +7 -7
  11. package/dist/site/node-api-registry.json +19 -6
  12. package/dist/site/page-registry.json +55 -19
  13. package/dist/site/public-surface-audit.json +47 -10
  14. package/dist/site/publication-registry.json +4 -4
  15. package/dist/site/release-provenance.json +1 -0
  16. package/dist/site/site-manifest.json +11 -11
  17. package/dist/site/workflow-registry.json +7 -4
  18. package/docs/MAP.md +1 -0
  19. package/docs/artifact-verification-envelope.md +113 -0
  20. package/docs/cli.md +22 -0
  21. package/docs/release-candidate.md +5 -0
  22. package/docs/release-governance.md +7 -0
  23. package/docs/release-propagation.md +3 -0
  24. package/docs/reusable-build-surface.md +13 -0
  25. package/docs/versioning.md +1 -0
  26. package/package.json +2 -1
  27. package/packages/core/README.md +13 -0
  28. package/packages/core/artifact-passport.js +6 -1
  29. package/packages/core/artifact-verification-envelope.js +386 -0
  30. package/packages/core/buildchain-config.js +6 -2
  31. package/packages/core/controller-evidence.js +2 -3
  32. package/packages/core/index.js +10 -0
  33. package/packages/core/release-line-bootstrap.js +24 -0
  34. package/scripts/check-inventory.mjs +5 -0
  35. package/scripts/generate-site-bundle.mjs +4 -0
  36. package/scripts/release-candidate-resolver.mjs +52 -29
  37. package/scripts/run-lifecycle-core.mjs +50 -6
@@ -0,0 +1,386 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT =
4
+ "kungfu-buildchain-artifact-verification-envelope";
5
+ export const ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT =
6
+ "kungfu-buildchain-artifact-verification-envelope-check";
7
+ export const KFX_ADMISSION_INPUTS_CONTRACT =
8
+ "kungfu-buildchain-kfx-admission-inputs";
9
+
10
+ const ARTIFACT_VERIFICATION_CONTRACT =
11
+ "kungfu-buildchain-artifact-verification";
12
+ const KFX_TRUST_INPUTS_SCHEMA = "kungfu.kfx-trust-inputs/v1";
13
+ const KFD_ASSESSMENT_SCHEMA = "kungfu.trust.assessment/v1";
14
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
15
+ const ROOT_FIELDS = Object.freeze([
16
+ "packageRoot",
17
+ "sourceRoot",
18
+ "dependencyRoot",
19
+ "buildPlanRoot",
20
+ "toolchainRoot",
21
+ "artifactRoot",
22
+ "qualificationRoot",
23
+ "verifierRoot",
24
+ ]);
25
+ const BINDING_FIELDS = Object.freeze([
26
+ "schema",
27
+ ...ROOT_FIELDS,
28
+ "issuer",
29
+ "publisher",
30
+ "contractVersion",
31
+ ]);
32
+
33
+ function stableJson(value) {
34
+ if (Array.isArray(value)) {
35
+ return `[${value.map(stableJson).join(",")}]`;
36
+ }
37
+ if (value && typeof value === "object") {
38
+ return `{${Object.keys(value)
39
+ .sort()
40
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
41
+ .join(",")}}`;
42
+ }
43
+ return JSON.stringify(value);
44
+ }
45
+
46
+ function jsonClone(value) {
47
+ return JSON.parse(JSON.stringify(value));
48
+ }
49
+
50
+ export function artifactVerificationEnvelopeDigest(value) {
51
+ return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
52
+ }
53
+
54
+ function issue(code, message, details = {}) {
55
+ return { level: "error", code, message, details };
56
+ }
57
+
58
+ function isObject(value) {
59
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
60
+ }
61
+
62
+ function isRoot(value) {
63
+ return SHA256_PATTERN.test(String(value || ""));
64
+ }
65
+
66
+ function nonEmptyString(value, label) {
67
+ const normalized = String(value || "").trim();
68
+ if (!normalized) {
69
+ throw new Error(`${label} must be a non-empty string`);
70
+ }
71
+ return normalized;
72
+ }
73
+
74
+ function nonNegativeInteger(value, label) {
75
+ if (!Number.isSafeInteger(value) || value < 0) {
76
+ throw new Error(`${label} must be a non-negative safe integer`);
77
+ }
78
+ return value;
79
+ }
80
+
81
+ function normalizeBindings(value = {}) {
82
+ if (!isObject(value)) {
83
+ throw new Error("bindings must be an object");
84
+ }
85
+ const supplied = Object.keys(value).sort();
86
+ const expected = [...BINDING_FIELDS].sort();
87
+ if (stableJson(supplied) !== stableJson(expected)) {
88
+ throw new Error(`bindings must contain exactly: ${BINDING_FIELDS.join(", ")}`);
89
+ }
90
+ if (value.schema !== KFX_TRUST_INPUTS_SCHEMA) {
91
+ throw new Error(`bindings.schema must be ${KFX_TRUST_INPUTS_SCHEMA}`);
92
+ }
93
+ const normalized = { schema: KFX_TRUST_INPUTS_SCHEMA };
94
+ for (const field of ROOT_FIELDS) {
95
+ const root = String(value[field] || "");
96
+ if (!isRoot(root)) {
97
+ throw new Error(`bindings.${field} must be a canonical lowercase sha256 root`);
98
+ }
99
+ normalized[field] = root;
100
+ }
101
+ normalized.issuer = nonEmptyString(value.issuer, "bindings.issuer");
102
+ normalized.publisher = nonEmptyString(value.publisher, "bindings.publisher");
103
+ normalized.contractVersion = nonEmptyString(value.contractVersion, "bindings.contractVersion");
104
+ return normalized;
105
+ }
106
+
107
+ function normalizeRevocation(value = {}, { issuedAt } = {}) {
108
+ if (!isObject(value)) {
109
+ throw new Error("revocation must be an object");
110
+ }
111
+ const revoked = value.revoked === true;
112
+ const status = nonEmptyString(value.status || (revoked ? "revoked" : "active"), "revocation.status");
113
+ if (!new Set(["active", "revoked"]).has(status) || revoked !== (status === "revoked")) {
114
+ throw new Error("revocation.status must agree with revocation.revoked");
115
+ }
116
+ const checkedAt = nonNegativeInteger(value.checkedAt ?? issuedAt, "revocation.checkedAt");
117
+ const root = String(value.root || "");
118
+ if (!isRoot(root)) {
119
+ throw new Error("revocation.root must be a canonical lowercase sha256 root");
120
+ }
121
+ return {
122
+ status,
123
+ revoked,
124
+ checkedAt,
125
+ source: nonEmptyString(value.source, "revocation.source"),
126
+ root,
127
+ };
128
+ }
129
+
130
+ function envelopeRoot(value) {
131
+ const basis = jsonClone(value);
132
+ if (isObject(basis.envelope)) {
133
+ delete basis.envelope.root;
134
+ }
135
+ return artifactVerificationEnvelopeDigest(basis);
136
+ }
137
+
138
+ function recomputeKfdReportRoot(report) {
139
+ const basis = jsonClone(report);
140
+ delete basis.report_hash;
141
+ return artifactVerificationEnvelopeDigest(basis);
142
+ }
143
+
144
+ export function verifyArtifactVerificationEnvelope({
145
+ envelope,
146
+ assessmentTime = Math.floor(Date.now() / 1000),
147
+ expectedEnvelopeRoot = "",
148
+ expectedIssuer = "",
149
+ expectedPublisher = "",
150
+ expectedContractVersion = "",
151
+ } = {}) {
152
+ const issues = [];
153
+ const at = Number.isSafeInteger(assessmentTime) && assessmentTime >= 0
154
+ ? assessmentTime
155
+ : -1;
156
+ if (!isObject(envelope)) {
157
+ issues.push(issue("envelope.object", "artifact verification envelope must be an object"));
158
+ return {
159
+ schemaVersion: 1,
160
+ contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
161
+ ok: false,
162
+ outcome: "fail",
163
+ assessmentTime,
164
+ issues,
165
+ };
166
+ }
167
+
168
+ const metadata = isObject(envelope.envelope) ? envelope.envelope : {};
169
+ if (
170
+ metadata.contract !== ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT ||
171
+ metadata.schemaVersion !== 1 ||
172
+ metadata.canonicalization !== "buildchain-stable-json/v1"
173
+ ) {
174
+ issues.push(issue("envelope.contract", "artifact verification envelope contract metadata is invalid"));
175
+ }
176
+ const computedEnvelopeRoot = envelopeRoot(envelope);
177
+ if (!isRoot(metadata.root) || metadata.root !== computedEnvelopeRoot) {
178
+ issues.push(issue("envelope.root", "artifact verification envelope root does not match canonical content", {
179
+ expected: computedEnvelopeRoot,
180
+ actual: metadata.root || "",
181
+ }));
182
+ }
183
+ if (expectedEnvelopeRoot && metadata.root !== expectedEnvelopeRoot) {
184
+ issues.push(issue("envelope.root.expected", "artifact verification envelope root does not match the pinned root"));
185
+ }
186
+
187
+ const verificationOk =
188
+ envelope.contract === ARTIFACT_VERIFICATION_CONTRACT &&
189
+ envelope.schemaVersion === 1 &&
190
+ envelope.outcome === "pass" &&
191
+ envelope.ok === true &&
192
+ envelope.trust === "pass" &&
193
+ envelope.passport?.verification?.ok === true &&
194
+ envelope.passport?.verification?.trust === "pass";
195
+ if (!verificationOk) {
196
+ issues.push(issue("verification.invalid", "base artifact verification must be a passing v1 report"));
197
+ }
198
+
199
+ let bindings;
200
+ try {
201
+ bindings = normalizeBindings(envelope.bindings);
202
+ } catch (error) {
203
+ issues.push(issue("bindings.invalid", error.message));
204
+ }
205
+ if (bindings) {
206
+ const subjectRoot = String(envelope.subject?.digest || "");
207
+ const matchedRoot = String(envelope.match?.artifact?.digest || "");
208
+ if (
209
+ !isRoot(subjectRoot) ||
210
+ subjectRoot !== matchedRoot ||
211
+ bindings.artifactRoot !== subjectRoot
212
+ ) {
213
+ issues.push(issue("bindings.artifactRoot", "artifact root must match the exact verified subject and passport artifact"));
214
+ }
215
+ if (expectedIssuer && bindings.issuer !== expectedIssuer) {
216
+ issues.push(issue("bindings.issuer", "issuer does not match the expected authority"));
217
+ }
218
+ if (expectedPublisher && bindings.publisher !== expectedPublisher) {
219
+ issues.push(issue("bindings.publisher", "publisher does not match the expected identity"));
220
+ }
221
+ if (expectedContractVersion && bindings.contractVersion !== expectedContractVersion) {
222
+ issues.push(issue("bindings.contractVersion", "verification contract version does not match the expected version"));
223
+ }
224
+ }
225
+
226
+ const issuedAt = envelope.issuedAt;
227
+ const expiresAt = envelope.expiresAt;
228
+ if (
229
+ at < 0 ||
230
+ !Number.isSafeInteger(issuedAt) ||
231
+ issuedAt < 0 ||
232
+ !Number.isSafeInteger(expiresAt) ||
233
+ expiresAt <= issuedAt ||
234
+ at < issuedAt ||
235
+ at >= expiresAt
236
+ ) {
237
+ issues.push(issue("lifecycle.invalid", "envelope must be active at assessmentTime with valid issuedAt/expiresAt bounds"));
238
+ }
239
+ const revocation = envelope.revocation;
240
+ if (
241
+ !isObject(revocation) ||
242
+ !isRoot(revocation.root) ||
243
+ !Number.isSafeInteger(revocation.checkedAt) ||
244
+ revocation.checkedAt < issuedAt ||
245
+ (at >= 0 && revocation.checkedAt > at) ||
246
+ !["active", "revoked"].includes(revocation.status) ||
247
+ revocation.revoked !== (revocation.status === "revoked") ||
248
+ envelope.revoked !== revocation.revoked
249
+ ) {
250
+ issues.push(issue("lifecycle.revocation", "revocation binding is invalid or inconsistent"));
251
+ }
252
+ if (envelope.revoked === true) {
253
+ issues.push(issue("lifecycle.revoked", "artifact verification envelope has been revoked"));
254
+ }
255
+
256
+ const assessment = envelope.kfdAssessment;
257
+ const report = isObject(assessment?.report) ? assessment.report : {};
258
+ if (
259
+ !isObject(assessment) ||
260
+ assessment.schema !== KFD_ASSESSMENT_SCHEMA ||
261
+ assessment.state !== "fresh" ||
262
+ report.state !== "fresh"
263
+ ) {
264
+ issues.push(issue("kfd.assessment.state", "KFD assessment must be a fresh ADR-0052 assessment"));
265
+ }
266
+ if (
267
+ !isRoot(assessment?.assessment_key) ||
268
+ report.assessment_key !== assessment?.assessment_key
269
+ ) {
270
+ issues.push(issue("kfd.assessment.key", "KFD assessment key must be canonical and match the report"));
271
+ }
272
+ const computedReportRoot = isObject(report) ? recomputeKfdReportRoot(report) : "";
273
+ if (!isRoot(report.report_hash) || report.report_hash !== computedReportRoot) {
274
+ issues.push(issue("kfd.report.root", "KFD report hash does not match canonical report content", {
275
+ expected: computedReportRoot,
276
+ actual: report.report_hash || "",
277
+ }));
278
+ }
279
+ if (
280
+ !String(report.purpose || "").trim() ||
281
+ !isRoot(report.query_proof_root) ||
282
+ !isRoot(report.contract_world?.root) ||
283
+ !isRoot(report.policy?.root) ||
284
+ !Array.isArray(report.fact_surfaces) ||
285
+ report.fact_surfaces.length === 0 ||
286
+ report.fact_surfaces.some((surface) => !isRoot(surface?.root))
287
+ ) {
288
+ issues.push(issue("kfd.report.bindings", "KFD report must bind purpose, query proof, contract world, policy, and fact-surface roots"));
289
+ }
290
+ if (bindings && bindings.qualificationRoot !== report.report_hash) {
291
+ issues.push(issue("bindings.qualificationRoot", "qualification root must equal the KFD report hash"));
292
+ }
293
+
294
+ const ok = issues.length === 0;
295
+ return {
296
+ schemaVersion: 1,
297
+ contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
298
+ ok,
299
+ outcome: ok ? "pass" : "fail",
300
+ assessmentTime,
301
+ envelopeRoot: metadata.root || "",
302
+ bindings: bindings || undefined,
303
+ kfdAssessment: isObject(assessment)
304
+ ? {
305
+ state: assessment.state || "",
306
+ assessmentKey: assessment.assessment_key || "",
307
+ reportRoot: report.report_hash || "",
308
+ }
309
+ : undefined,
310
+ issues,
311
+ };
312
+ }
313
+
314
+ export function sealArtifactVerificationReport({
315
+ report,
316
+ bindings,
317
+ kfdAssessment,
318
+ issuedAt,
319
+ expiresAt,
320
+ revocation,
321
+ } = {}) {
322
+ if (!isObject(report)) {
323
+ throw new Error("report must be an artifact verification object");
324
+ }
325
+ const normalizedIssuedAt = nonNegativeInteger(issuedAt, "issuedAt");
326
+ const normalizedExpiresAt = nonNegativeInteger(expiresAt, "expiresAt");
327
+ if (normalizedExpiresAt <= normalizedIssuedAt) {
328
+ throw new Error("expiresAt must be greater than issuedAt");
329
+ }
330
+ const normalizedRevocation = normalizeRevocation(revocation, {
331
+ issuedAt: normalizedIssuedAt,
332
+ });
333
+ const result = {
334
+ ...jsonClone(report),
335
+ issuedAt: normalizedIssuedAt,
336
+ expiresAt: normalizedExpiresAt,
337
+ revoked: normalizedRevocation.revoked,
338
+ revocation: normalizedRevocation,
339
+ bindings: normalizeBindings(bindings),
340
+ kfdAssessment: jsonClone(kfdAssessment),
341
+ envelope: {
342
+ schemaVersion: 1,
343
+ contract: ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
344
+ canonicalization: "buildchain-stable-json/v1",
345
+ root: "",
346
+ },
347
+ };
348
+ result.envelope.root = envelopeRoot(result);
349
+ const check = verifyArtifactVerificationEnvelope({
350
+ envelope: result,
351
+ assessmentTime: normalizedRevocation.checkedAt,
352
+ });
353
+ if (!check.ok) {
354
+ throw new Error(`artifact verification envelope is invalid: ${check.issues.map((entry) => entry.code).join(", ")}`);
355
+ }
356
+ return result;
357
+ }
358
+
359
+ export function projectArtifactVerificationEnvelopeToKfx({
360
+ envelope,
361
+ assessmentTime = Math.floor(Date.now() / 1000),
362
+ expectedEnvelopeRoot = "",
363
+ expectedIssuer = "",
364
+ expectedPublisher = "",
365
+ expectedContractVersion = "",
366
+ } = {}) {
367
+ const check = verifyArtifactVerificationEnvelope({
368
+ envelope,
369
+ assessmentTime,
370
+ expectedEnvelopeRoot,
371
+ expectedIssuer,
372
+ expectedPublisher,
373
+ expectedContractVersion,
374
+ });
375
+ if (!check.ok) {
376
+ throw new Error(`artifact verification envelope failed closed: ${check.issues.map((entry) => entry.code).join(", ")}`);
377
+ }
378
+ return {
379
+ schemaVersion: 1,
380
+ contract: KFX_ADMISSION_INPUTS_CONTRACT,
381
+ envelopeRoot: envelope.envelope.root,
382
+ attestation: jsonClone(envelope),
383
+ trustInputs: jsonClone(envelope.bindings),
384
+ kfdAssessment: jsonClone(envelope.kfdAssessment),
385
+ };
386
+ }
@@ -1181,7 +1181,7 @@ export function getPublishContract(loadedConfig) {
1181
1181
  return loadedConfig?.config?.publish;
1182
1182
  }
1183
1183
 
1184
- export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv }) {
1184
+ export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv, timeoutMinutes }) {
1185
1185
  const lifecycle = loadedConfig?.config?.lifecycle || {};
1186
1186
  const selected = stage || getLifecycleStage(loadedConfig, name);
1187
1187
  if (!selected) {
@@ -1193,7 +1193,11 @@ export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, sta
1193
1193
  ...(selected.env || {}),
1194
1194
  ...(extraEnv || {}),
1195
1195
  };
1196
- const timeout = selected.timeoutMinutes ? selected.timeoutMinutes * 60_000 : undefined;
1196
+ const effectiveTimeoutMinutes = selected.timeoutMinutes ?? timeoutMinutes;
1197
+ if (effectiveTimeoutMinutes !== undefined && (!Number.isFinite(effectiveTimeoutMinutes) || effectiveTimeoutMinutes <= 0)) {
1198
+ throw new Error("lifecycle timeoutMinutes must be a positive number");
1199
+ }
1200
+ const timeout = effectiveTimeoutMinutes ? effectiveTimeoutMinutes * 60_000 : undefined;
1197
1201
  const execOptions = {
1198
1202
  cwd,
1199
1203
  env,
@@ -344,7 +344,6 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
344
344
  const issues = status === "passed"
345
345
  ? missingEvidence.map((kind) => `required controller evidence is missing: ${kind}`)
346
346
  : [];
347
- const qualifying = status === "passed" && plan.qualifying === true && issues.length === 0;
348
347
  const receipt = {
349
348
  schemaVersion: 1,
350
349
  contract: BUILDCHAIN_CONTROLLER_EVIDENCE_CONTRACT,
@@ -354,11 +353,11 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
354
353
  runtime: { ...plan.runtime },
355
354
  planDigest: plan.digest,
356
355
  status,
357
- qualifying,
356
+ qualifying: status === "passed" && plan.qualifying === true && issues.length === 0,
358
357
  stages: normalizedStages,
359
358
  evidence: normalizedEvidence,
360
359
  issues,
361
- ...(reason && !qualifying ? {
360
+ ...(reason ? {
362
361
  reason: {
363
362
  code: nonEmptyString(reason.code, "reason.code"),
364
363
  summary: nonEmptyString(reason.summary, "reason.summary"),
@@ -179,6 +179,16 @@ export {
179
179
  verifyArtifactPassport,
180
180
  } from "./artifact-passport.js";
181
181
 
182
+ export {
183
+ ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
184
+ ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
185
+ KFX_ADMISSION_INPUTS_CONTRACT,
186
+ artifactVerificationEnvelopeDigest,
187
+ projectArtifactVerificationEnvelopeToKfx,
188
+ sealArtifactVerificationReport,
189
+ verifyArtifactVerificationEnvelope,
190
+ } from "./artifact-verification-envelope.js";
191
+
182
192
  export {
183
193
  BUILDCHAIN_CONTRACT_LOCK,
184
194
  BUILDCHAIN_RUNTIME_CONTRACT_WORLD,
@@ -65,6 +65,29 @@ function writeJson(value) {
65
65
  return `${JSON.stringify(value, null, 2)}\n`;
66
66
  }
67
67
 
68
+ function updateReleaseImpactVersionState(cwd, plan) {
69
+ const filePath = path.join(cwd, ".buildchain", "release-impact.json");
70
+ if (!fs.existsSync(filePath)) {
71
+ return;
72
+ }
73
+ const current = readJsonIfExists(filePath);
74
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
75
+ throw new Error(".buildchain/release-impact.json must be a JSON object");
76
+ }
77
+ const next = {
78
+ ...current,
79
+ release: {
80
+ ...(current.release && typeof current.release === "object" ? current.release : {}),
81
+ version: plan.initialVersion,
82
+ line: plan.line,
83
+ },
84
+ };
85
+ const content = writeJson(next);
86
+ if (content !== writeJson(current)) {
87
+ fs.writeFileSync(filePath, content);
88
+ }
89
+ }
90
+
68
91
  function discoverVersionStateFiles(cwd, loadedConfig) {
69
92
  if (loadedConfig?.config?.version?.files?.length) {
70
93
  return discoverConfiguredVersionStateFiles(cwd, loadedConfig);
@@ -212,6 +235,7 @@ export function writeReleaseLineBootstrapVersionState({
212
235
  for (const file of changed) {
213
236
  fs.writeFileSync(path.join(cwd, file.path), file.content);
214
237
  }
238
+ updateReleaseImpactVersionState(cwd, plan);
215
239
  const lifecycleVersionState =
216
240
  getLifecycleStage(loadedConfig, "version-state") ||
217
241
  getLifecycleStage(loadedConfig, "version_state");
@@ -21,6 +21,7 @@ const requiredPaths = [
21
21
  ".github/pull_request_template.md",
22
22
  "bin/buildchain.mjs",
23
23
  "packages/core/homebrew.js",
24
+ "packages/core/artifact-verification-envelope.js",
24
25
  "packages/core/build-facts.js",
25
26
  "packages/core/publication-package.js",
26
27
  "packages/core/release-line-bootstrap.js",
@@ -239,6 +240,9 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
239
240
  if (rootPackage.exports?.["./controller-evidence"] !== "./packages/core/controller-evidence.js") {
240
241
  throw new Error("root package must export @kungfu-tech/buildchain/controller-evidence");
241
242
  }
243
+ if (rootPackage.exports?.["./artifact-verification-envelope"] !== "./packages/core/artifact-verification-envelope.js") {
244
+ throw new Error("root package must export @kungfu-tech/buildchain/artifact-verification-envelope");
245
+ }
242
246
  if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
243
247
  throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
244
248
  }
@@ -843,6 +847,7 @@ for (const requiredSnippet of [
843
847
  "expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
844
848
  "publication-version: ${{ needs.publication-plan.outputs.version }}",
845
849
  "name: Plan exact publication version",
850
+ "name: Install exact publication planning dependencies",
846
851
  "DRY_RUN: ${{ inputs.dry-run }}",
847
852
  "if (dryRun) {",
848
853
  "Enforce Buildchain stable release canary gate",
@@ -444,6 +444,8 @@ function cliCommandMeta(id) {
444
444
  "publication-artifact": { group: "reusable-build", purpose: "Generate publication artifact manifests, passports, and source bundles for paper/report repositories." },
445
445
  "publication-artifact-manifest": { group: "reusable-build", purpose: "Write a site-consumable publication artifact manifest, publication passport, and source bundle." },
446
446
  "publication-artifact-npm-package": { group: "reusable-build", purpose: "Synthesize the declared npm paper package from a publication artifact manifest, passport, registry, source bundle, and primary artifact." },
447
+ project: { group: "release-passport-trust", purpose: "Inspect read-only consumer projection command families." },
448
+ "project-kfx-admission": { group: "release-passport-trust", purpose: "Project one verified sealed envelope into direct KFX admission inputs without reconstructing evidence." },
447
449
  "release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
448
450
  "release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
449
451
  "release-propagation": { group: "site-and-propagation", purpose: "Plan channel-preserving downstream release PRs and write exact upstream release locks." },
@@ -455,6 +457,7 @@ function cliCommandMeta(id) {
455
457
  validate: { group: "getting-started", purpose: "Validate .buildchain/buildchain.toml and declared lifecycle surfaces." },
456
458
  verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
457
459
  "verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
460
+ "verify-artifact-envelope": { group: "release-passport-trust", purpose: "Verify exact roots, identity, lifecycle, revocation, and an existing KFD assessment in a sealed artifact envelope." },
458
461
  "verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
459
462
  "verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
460
463
  "verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
@@ -485,6 +488,7 @@ function nodeApiMeta(exportName) {
485
488
  "./publication-control-plane-audit": { group: "release-passport-trust", summary: "Read-only publication control-plane snapshot evaluation APIs." },
486
489
  "./buildchain-publication-authority": { group: "release-passport-trust", summary: "Buildchain-owned closed-world publication authority descriptor registry." },
487
490
  "./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
491
+ "./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
488
492
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
489
493
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
490
494
  "./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
@@ -353,6 +353,9 @@ export async function resolveReleaseCandidateArtifacts({
353
353
  outputDir = ".buildchain/release-candidate",
354
354
  fetchImpl = globalThis.fetch,
355
355
  download = true,
356
+ waitSeconds = 600,
357
+ pollIntervalMs = 15000,
358
+ sleepImpl = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
356
359
  } = {}) {
357
360
  const repoInfo = splitRepository(repository);
358
361
  const sha = assertSha(targetSha, "targetSha");
@@ -377,45 +380,63 @@ export async function resolveReleaseCandidateArtifacts({
377
380
  if (!pullRequest) {
378
381
  throw new Error(`no same-repository merged channel PR found for ${sha} into ${normalizedTarget}`);
379
382
  }
380
- const runs = await githubJson({
381
- apiUrl,
382
- token,
383
- fetchImpl,
384
- path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
385
- });
386
- const candidateRuns = selectReleaseCandidateRuns({
387
- runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
388
- pullRequest,
389
- workflowName,
390
- });
391
- if (!candidateRuns.length) {
392
- throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number}`);
383
+ const timeoutMs = Number(waitSeconds) * 1000;
384
+ const intervalMs = Number(pollIntervalMs);
385
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
386
+ throw new Error(`release-candidate waitSeconds must be a non-negative number, got ${waitSeconds}`);
387
+ }
388
+ if (!Number.isFinite(intervalMs) || intervalMs < 0) {
389
+ throw new Error(`release-candidate pollIntervalMs must be a non-negative number, got ${pollIntervalMs}`);
393
390
  }
391
+ const deadline = Date.now() + timeoutMs;
394
392
  let run;
395
393
  let artifactResponse;
396
394
  let selected;
397
- const selectionErrors = [];
398
- for (const candidateRun of candidateRuns) {
399
- const candidateArtifactResponse = await githubJson({
395
+ let selectionErrors = [];
396
+ let candidateRuns = [];
397
+ while (!selected) {
398
+ const runs = await githubJson({
400
399
  apiUrl,
401
400
  token,
402
401
  fetchImpl,
403
- path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${candidateRun.id}/artifacts?per_page=100`,
402
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
404
403
  });
405
- try {
406
- selected = selectReleaseCandidateArtifacts({
407
- artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
408
- artifactName,
404
+ candidateRuns = selectReleaseCandidateRuns({
405
+ runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
406
+ pullRequest,
407
+ workflowName,
408
+ });
409
+ selectionErrors = [];
410
+ for (const candidateRun of candidateRuns) {
411
+ const candidateArtifactResponse = await githubJson({
412
+ apiUrl,
413
+ token,
414
+ fetchImpl,
415
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${candidateRun.id}/artifacts?per_page=100`,
409
416
  });
410
- run = candidateRun;
411
- artifactResponse = candidateArtifactResponse;
412
- break;
413
- } catch (error) {
414
- selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
417
+ try {
418
+ selected = selectReleaseCandidateArtifacts({
419
+ artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
420
+ artifactName,
421
+ });
422
+ run = candidateRun;
423
+ artifactResponse = candidateArtifactResponse;
424
+ break;
425
+ } catch (error) {
426
+ selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
427
+ }
415
428
  }
416
- }
417
- if (!run || !artifactResponse || !selected) {
418
- throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts: ${selectionErrors.join("; ")}`);
429
+ if (selected) break;
430
+ if (Date.now() >= deadline) {
431
+ if (!candidateRuns.length) {
432
+ throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number} within ${waitSeconds}s`);
433
+ }
434
+ throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts within ${waitSeconds}s: ${selectionErrors.join("; ")}`);
435
+ }
436
+ console.log(
437
+ `> waiting for successful ${workflowName} release-candidate evidence for channel PR #${pullRequest.number}`,
438
+ );
439
+ await sleepImpl(intervalMs);
419
440
  }
420
441
  const payloadArtifacts = selectPayloadArtifacts({
421
442
  artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
@@ -551,6 +572,8 @@ export async function resolveReleaseCandidateArtifactsCli() {
551
572
  publishPackageMain: env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN"),
552
573
  outputDir: env("BUILDCHAIN_RC_OUTPUT_DIR", ".buildchain/release-candidate"),
553
574
  download: releaseCandidateDownloadEnabled(env("BUILDCHAIN_RC_DOWNLOAD", "true")),
575
+ waitSeconds: env("BUILDCHAIN_RC_WAIT_SECONDS", "600"),
576
+ pollIntervalMs: env("BUILDCHAIN_RC_POLL_INTERVAL_MS", "15000"),
554
577
  });
555
578
  writeGitHubOutputs({
556
579
  "promote-only-release-candidate": String(result.enabled === true),