@kungfu-tech/buildchain 4.0.1-alpha.41 → 4.0.1-alpha.42

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 (39) hide show
  1. package/README.md +9 -6
  2. package/actions/v4-release-candidate-promote/README.md +3 -0
  3. package/architecture/ci-lane-change-budget.json +53 -1
  4. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  5. package/architecture/maintainability-policy.json +15 -13
  6. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  7. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  8. package/dist/site/agent-index.json +1 -0
  9. package/dist/site/artifact-schemas.json +2 -0
  10. package/dist/site/buildchain-contract.json +70 -19
  11. package/dist/site/buildchain-site.json +62 -14
  12. package/dist/site/capability-registry.json +5 -5
  13. package/dist/site/controller-registry.json +6 -2
  14. package/dist/site/kfd-claims.json +99 -12
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +2 -2
  17. package/dist/site/node-api-registry.json +261 -17
  18. package/dist/site/page-registry.json +55 -7
  19. package/dist/site/public-surface-audit.json +43 -7
  20. package/dist/site/publication-authority-registry.json +28 -1
  21. package/dist/site/publication-registry.json +4 -4
  22. package/dist/site/release-provenance.json +2 -0
  23. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  24. package/dist/site/site-manifest.json +6 -6
  25. package/dist/site/workflow-registry.json +53 -6
  26. package/docs/MAP.md +1 -1
  27. package/docs/node-api-reference.md +60 -32
  28. package/docs/v4-declarative-release-tail-migration.md +43 -0
  29. package/package.json +3 -1
  30. package/packages/core/buildchain-contract.js +31 -1
  31. package/packages/core/buildchain-publication-authority.js +1 -0
  32. package/packages/core/index.js +1 -0
  33. package/packages/core/v4-publication-qualification.js +222 -0
  34. package/scripts/check-inventory.mjs +2 -2
  35. package/scripts/generate-release-candidate-passport.mjs +26 -1
  36. package/scripts/generate-site-bundle.mjs +6 -0
  37. package/scripts/release-candidate-resolver.mjs +44 -0
  38. package/scripts/site-capability-metadata.mjs +1 -0
  39. package/scripts/v4-declarative-promotion-admission.mjs +42 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "4.0.1-alpha.41",
3
+ "version": "4.0.1-alpha.42",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -48,6 +48,7 @@
48
48
  "./v4-tail-reseal": "./packages/core/v4-tail-reseal.js",
49
49
  "./v4-tail-reseal-receipt": "./packages/core/v4-tail-reseal-receipt.js",
50
50
  "./v4-publication-rehearsal": "./packages/core/v4-publication-rehearsal.js",
51
+ "./v4-publication-qualification": "./packages/core/v4-publication-qualification.js",
51
52
  "./adopter-delivery-gate": "./packages/core/adopter-delivery-gate.js",
52
53
  "./kfd-adopter-category-driver": "./packages/core/kfd-adopter-category-driver.js",
53
54
  "./buildchain-delivery-infrastructure": "./packages/core/buildchain-delivery-infrastructure.js",
@@ -114,6 +115,7 @@
114
115
  "./site/schemas/v4-publication-rehearsal-capsule-v1.schema.json": "./dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json",
115
116
  "./site/schemas/v4-adopter-delivery-v1.schema.json": "./dist/site/schemas/v4-adopter-delivery-v1.schema.json",
116
117
  "./site/schemas/release-tail-capabilities-v1.schema.json": "./dist/site/schemas/release-tail-capabilities-v1.schema.json",
118
+ "./site/schemas/v4-publication-qualification-v1.schema.json": "./dist/site/schemas/v4-publication-qualification-v1.schema.json",
117
119
  "./site/buildchain-contract.json": "./dist/site/buildchain-contract.json",
118
120
  "./site/product-mechanism.json": "./dist/site/product-mechanism.json",
119
121
  "./site/release-provenance.json": "./dist/site/release-provenance.json",
@@ -166,6 +166,36 @@ function declarativeAuditableDemoSurface(root, pkg, majorLine) {
166
166
  });
167
167
  }
168
168
 
169
+ function v4ReleaseCandidatePromoteSurface(root, majorLine) {
170
+ return surface(root, {
171
+ id: "v4-release-candidate-promote-action",
172
+ kind: "action",
173
+ path: "actions/v4-release-candidate-promote/action.yml",
174
+ publicRef: `kungfu-systems/buildchain/actions/v4-release-candidate-promote@${majorLine}`,
175
+ requiredInputs: [
176
+ "token", "repository", "source-sha", "version", "tag", "channel",
177
+ "candidate-passport-path", "stage-capsules-path",
178
+ "publication-qualification-path",
179
+ ],
180
+ requiredOutputs: [
181
+ "release-passport-path", "release-passport-root", "transaction-state",
182
+ "declaration-root", "transaction-root", "state-root",
183
+ "receipt-roots-json",
184
+ ],
185
+ breakingDefaults: {
186
+ statePath: ".buildchain/release-tail/state.json",
187
+ executionBoundary: "built-in-provider-plane-only",
188
+ },
189
+ optionalInputs: ["artifact-paths", "state-path", "failure-after-capability"],
190
+ guarantees: [
191
+ "v4 promotion consumes only sealed candidate, Stage Capsule, qualification, and artifact evidence",
192
+ "GitHub Release, signed-channel commit, activation, readback, and released-evidence synthesis use built-in provider adapters",
193
+ "durable provider checkpoints resume completed capabilities without replay",
194
+ "no consumer command, script, shell, checkout, or executable tail hook is accepted",
195
+ ],
196
+ });
197
+ }
198
+
169
199
  export function createBuildchainContractWorld({
170
200
  root = process.cwd(),
171
201
  packageJson = undefined,
@@ -484,7 +514,7 @@ export function createBuildchainContractWorld({
484
514
  "Build Images owns encoding, Buildchain owns qualification and receipts, and site repositories own browser loading and accessibility behavior",
485
515
  "media qualification does not claim browser playback, responsive layout, reduced-motion behavior, accessibility, or production deployment",
486
516
  ],
487
- }), declarativeAuditableDemoSurface(root, pkg, majorLine),
517
+ }), declarativeAuditableDemoSurface(root, pkg, majorLine), v4ReleaseCandidatePromoteSurface(root, majorLine),
488
518
  surface(root, {
489
519
  id: "promote-buildchain-ref-action",
490
520
  kind: "action",
@@ -58,6 +58,7 @@ const DESCRIPTORS = Object.freeze([
58
58
  [".github/workflows/release-tail.yml", "product-publication", true, ["artifact.publish", "signed-channel.commit", "release.activate", "released-evidence.synthesize"], "caller-secrets", "caller-bound", "caller-bound", "fixed"],
59
59
  [".github/workflows/release-verify.yml", "governance-write"],
60
60
  [".github/workflows/stable-candidate-patrol.yml", "governance-write"],
61
+ [".github/workflows/v4-declarative-release-tail-dogfood.yml", "product-publication", true, ["artifact.publish", "signed-channel.commit", "release.activate", "released-evidence.synthesize"], "caller-secrets", "none", "fixed", "fixed"],
61
62
  ]);
62
63
 
63
64
  export function buildchainPublicationAuthorityDescriptors() {
@@ -646,6 +646,7 @@ export {
646
646
  export * from "./release-propagation.js";
647
647
  export * from "./v4-floating-consumer-policy.js";
648
648
  export * from "./v4-runtime-ref-resume-authority.js";
649
+ export * from "./v4-publication-qualification.js";
649
650
 
650
651
  export {
651
652
  RELEASE_ACTIVATION_CONTRACT,
@@ -0,0 +1,222 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const V4_PUBLICATION_QUALIFICATION_SCHEMA =
4
+ "kungfu.buildchain.v4-publication-qualification/v1";
5
+
6
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
7
+ const SHA = /^[0-9a-f]{40}$/u;
8
+ const FORBIDDEN_EXECUTION_KEYS = new Set([
9
+ "cmd",
10
+ "command",
11
+ "eval",
12
+ "executable",
13
+ "javascript",
14
+ "run",
15
+ "script",
16
+ "shell",
17
+ ]);
18
+
19
+ export class V4PublicationQualificationError extends Error {
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.name = "V4PublicationQualificationError";
23
+ this.code = code;
24
+ }
25
+ }
26
+
27
+ function fail(code, message) {
28
+ throw new V4PublicationQualificationError(code, message);
29
+ }
30
+
31
+ function canonical(value) {
32
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
33
+ if (value && typeof value === "object") {
34
+ return `{${Object.keys(value)
35
+ .sort()
36
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
37
+ .join(",")}}`;
38
+ }
39
+ return JSON.stringify(value);
40
+ }
41
+
42
+ export function v4PublicationQualificationRoot(value) {
43
+ return `sha256:${crypto.createHash("sha256").update(canonical(value)).digest("hex")}`;
44
+ }
45
+
46
+ export function assertNoV4ExecutionFields(value, location = "$") {
47
+ if (Array.isArray(value)) {
48
+ value.forEach((entry, index) =>
49
+ assertNoV4ExecutionFields(entry, `${location}[${index}]`),
50
+ );
51
+ return;
52
+ }
53
+ if (!value || typeof value !== "object") return;
54
+ for (const [key, entry] of Object.entries(value)) {
55
+ if (FORBIDDEN_EXECUTION_KEYS.has(key.toLowerCase())) {
56
+ fail(
57
+ "execution-field-forbidden",
58
+ `v4 declarative publication forbids ${location}.${key}`,
59
+ );
60
+ }
61
+ assertNoV4ExecutionFields(entry, `${location}.${key}`);
62
+ }
63
+ }
64
+
65
+ function exactRoot(value, label) {
66
+ const root = String(value || "").toLowerCase();
67
+ if (!ROOT.test(root))
68
+ fail("schema-invalid", `${label} must be a sha256 root`);
69
+ return root;
70
+ }
71
+
72
+ function exactSha(value, label) {
73
+ const sha = String(value || "").toLowerCase();
74
+ if (!SHA.test(sha))
75
+ fail("schema-invalid", `${label} must be an exact Git SHA`);
76
+ return sha;
77
+ }
78
+
79
+ function normalizedArtifacts(artifacts) {
80
+ if (!Array.isArray(artifacts) || artifacts.length === 0) {
81
+ fail(
82
+ "schema-invalid",
83
+ "publication qualification requires typed artifacts",
84
+ );
85
+ }
86
+ const normalized = artifacts
87
+ .map((artifact) => {
88
+ const value = {
89
+ role: String(artifact?.role || ""),
90
+ platform: String(artifact?.platform || ""),
91
+ artifactRoot: exactRoot(artifact?.artifactRoot, "artifactRoot"),
92
+ manifestRoot: exactRoot(artifact?.manifestRoot, "manifestRoot"),
93
+ };
94
+ if (!value.role || !value.platform)
95
+ fail("schema-invalid", "artifact role and platform are required");
96
+ return value;
97
+ })
98
+ .sort((left, right) =>
99
+ `${left.role}\0${left.platform}`.localeCompare(
100
+ `${right.role}\0${right.platform}`,
101
+ ),
102
+ );
103
+ if (
104
+ new Set(normalized.map(({ role, platform }) => `${role}\0${platform}`))
105
+ .size !== normalized.length
106
+ ) {
107
+ fail("schema-invalid", "artifact role/platform bindings must be unique");
108
+ }
109
+ return normalized;
110
+ }
111
+
112
+ export function createV4PublicationQualificationReceipt({
113
+ repository,
114
+ candidateRoot,
115
+ sourceSha,
116
+ sourceRoot,
117
+ policyDigest,
118
+ artifacts,
119
+ issuedAt,
120
+ expiresAt,
121
+ }) {
122
+ const body = {
123
+ schema: V4_PUBLICATION_QUALIFICATION_SCHEMA,
124
+ repository: String(repository || ""),
125
+ candidateRoot: exactRoot(candidateRoot, "candidateRoot"),
126
+ sourceSha: exactSha(sourceSha, "sourceSha"),
127
+ sourceRoot: exactRoot(sourceRoot, "sourceRoot"),
128
+ policyDigest: exactRoot(policyDigest, "policyDigest"),
129
+ artifacts: normalizedArtifacts(artifacts),
130
+ issuedAt: String(issuedAt || ""),
131
+ expiresAt: String(expiresAt || ""),
132
+ };
133
+ if (!/^[^/\s]+\/[^/\s]+$/u.test(body.repository))
134
+ fail("schema-invalid", "repository must be owner/repo");
135
+ const issued = Date.parse(body.issuedAt);
136
+ const expires = Date.parse(body.expiresAt);
137
+ if (
138
+ !Number.isFinite(issued) ||
139
+ !Number.isFinite(expires) ||
140
+ expires <= issued
141
+ )
142
+ fail("schema-invalid", "qualification freshness interval is invalid");
143
+ assertNoV4ExecutionFields(body);
144
+ const artifactRoot = v4PublicationQualificationRoot(body.artifacts);
145
+ const receipt = { ...body, artifactRoot };
146
+ return {
147
+ ...receipt,
148
+ receiptRoot: v4PublicationQualificationRoot(receipt),
149
+ };
150
+ }
151
+
152
+ export function validateV4PublicationQualificationReceipt(
153
+ receipt,
154
+ {
155
+ repository,
156
+ candidateRoot,
157
+ sourceSha,
158
+ sourceRoot,
159
+ artifactRoot,
160
+ policyDigest,
161
+ evaluatedAt = new Date().toISOString(),
162
+ } = {},
163
+ ) {
164
+ if (!receipt)
165
+ fail("qualification-missing", "qualification receipt is required");
166
+ assertNoV4ExecutionFields(receipt);
167
+ const recreated = createV4PublicationQualificationReceipt(receipt);
168
+ if (
169
+ receipt.artifactRoot !== recreated.artifactRoot ||
170
+ receipt.receiptRoot !== recreated.receiptRoot
171
+ ) {
172
+ fail(
173
+ "qualification-tampered",
174
+ "qualification receipt root does not verify",
175
+ );
176
+ }
177
+ if (repository && receipt.repository !== repository)
178
+ fail("repository-mismatch", "qualification repository does not match");
179
+ if (candidateRoot && receipt.candidateRoot !== candidateRoot)
180
+ fail("candidate-mismatch", "qualification candidate root does not match");
181
+ if (sourceSha && receipt.sourceSha !== sourceSha)
182
+ fail("source-mismatch", "qualification source SHA does not match");
183
+ if (sourceRoot && receipt.sourceRoot !== sourceRoot)
184
+ fail("source-mismatch", "qualification source root does not match");
185
+ if (artifactRoot && receipt.artifactRoot !== artifactRoot)
186
+ fail("artifact-mismatch", "qualification artifact root does not match");
187
+ if (policyDigest && receipt.policyDigest !== policyDigest)
188
+ fail("policy-mismatch", "qualification policy digest does not match");
189
+ const evaluated = Date.parse(evaluatedAt);
190
+ if (!Number.isFinite(evaluated))
191
+ fail("schema-invalid", "evaluatedAt is invalid");
192
+ if (
193
+ evaluated < Date.parse(receipt.issuedAt) ||
194
+ evaluated >= Date.parse(receipt.expiresAt)
195
+ )
196
+ fail(
197
+ "qualification-stale",
198
+ "qualification receipt is outside its freshness window",
199
+ );
200
+ return Object.freeze({
201
+ ok: true,
202
+ receiptRoot: receipt.receiptRoot,
203
+ artifactRoot: receipt.artifactRoot,
204
+ });
205
+ }
206
+
207
+ export function assertV4DeclarativePromotionInputs(inputs) {
208
+ const forbidden = Object.entries(inputs || {}).filter(
209
+ ([name, value]) =>
210
+ /(command|cmd|script|shell|run)$/iu.test(name) &&
211
+ String(value || "").trim() !== "",
212
+ );
213
+ if (forbidden.length > 0) {
214
+ fail(
215
+ "legacy-command-input-forbidden",
216
+ `v4 declarative publication rejects: ${forbidden
217
+ .map(([name]) => name)
218
+ .sort()
219
+ .join(", ")}`,
220
+ );
221
+ }
222
+ }
@@ -759,8 +759,8 @@ const registeredActionIds = (workflowRegistry.actions || []).map((entry) => entr
759
759
  const readmeActionIndex = fs.readFileSync(path.join(root, "README.md"), "utf8");
760
760
  const mapActionIndex = fs.readFileSync(path.join(root, "docs/MAP.md"), "utf8");
761
761
  const retrospectiveActionIndex = fs.readFileSync(path.join(root, ".github/retrospectives/2026-07-10-buildchain-consolidation.md"), "utf8");
762
- if (registeredActionIds.length !== 7) {
763
- throw new Error(`workflow-registry.json must expose the seven current action entries, got ${registeredActionIds.length}`);
762
+ if (registeredActionIds.length !== 8) {
763
+ throw new Error(`workflow-registry.json must expose the eight current action entries, got ${registeredActionIds.length}`);
764
764
  }
765
765
  for (const actionId of registeredActionIds) {
766
766
  if (!readmeActionIndex.includes(`actions/${actionId}`) || !mapActionIndex.includes(`actions/${actionId}`)) {
@@ -8,6 +8,7 @@ import { scanV4FloatingConsumerPolicy, v4ConsumerPolicyScannerRoot } from "../pa
8
8
  import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
9
9
  import { V4_STAGE_CAPSULE_CONTRACT, V4_STAGE_CAPSULE_IDENTITY_CONTRACT, v4StageCapsuleIdentityRoot, v4StageCapsuleRoot, validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
10
10
  import { v4RuntimeResumeDocumentRoot } from "../packages/core/v4-runtime-ref-resume-authority.js";
11
+ import { createV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
11
12
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
12
13
 
13
14
  const env = (name, fallback = "") => process.env[name] || fallback;
@@ -32,7 +33,20 @@ export function createReleaseCandidateStageCapsules({
32
33
  const coordinateByPlatform = new Map(
33
34
  coordinates.artifacts.map((entry) => [entry.platformId, entry]),
34
35
  );
35
- const qualificationRoot = passport.controllerReceipts?.[0]?.receiptDigest;
36
+ const qualificationArtifacts = passport.platformMatrix.map((platform) => {
37
+ const coordinate = coordinateByPlatform.get(platform.platformId);
38
+ if (!coordinate || coordinate.name !== platform.artifactName) throw new Error(`Stage Capsule coordinate missing for ${platform.platformId}`);
39
+ const manifest = readJsonFile(platform.manifestPath);
40
+ return { role: platform.artifactName, platform: platform.platformId, artifactRoot: coordinate.digest, manifestRoot: v4ContentRoot("stage-capsule-artifact-manifest", manifest) };
41
+ });
42
+ const qualificationExpiresAt = coordinates.artifacts.map((entry) => entry.expiresAt).sort((left, right) => Date.parse(left) - Date.parse(right))[0];
43
+ const qualificationReceipt = createV4PublicationQualificationReceipt({
44
+ repository: passport.repository, candidateRoot: `sha256:${passport.candidateHash}`,
45
+ sourceSha: passport.source.headSha, sourceRoot: v4ContentRoot("candidate-identity", passport.source),
46
+ policyDigest: passport.consumerPolicy.receiptRoot, artifacts: qualificationArtifacts,
47
+ issuedAt: passport.createdAt || new Date(Date.parse(qualificationExpiresAt) - 86_400_000).toISOString(), expiresAt: qualificationExpiresAt,
48
+ });
49
+ const qualificationRoot = qualificationReceipt.receiptRoot;
36
50
  const entries = passport.platformMatrix
37
51
  .map((platform) => {
38
52
  const coordinate = coordinateByPlatform.get(platform.platformId);
@@ -93,6 +107,9 @@ export function createReleaseCandidateStageCapsules({
93
107
  platform: platform.platformId,
94
108
  artifactName: coordinate.name,
95
109
  artifactDigest: coordinate.digest,
110
+ publicationArtifact: qualificationArtifacts.find(
111
+ ({ platform: platformId }) => platformId === platform.platformId,
112
+ ),
96
113
  artifact: coordinate,
97
114
  capsule,
98
115
  };
@@ -112,6 +129,8 @@ export function createReleaseCandidateStageCapsules({
112
129
  treeSha: passport.source.treeHash,
113
130
  },
114
131
  consumerPolicyReceiptRoot: passport.consumerPolicy.receiptRoot,
132
+ publicationQualificationRoot: qualificationRoot,
133
+ publicationQualificationReceipt: qualificationReceipt,
115
134
  capsules: entries,
116
135
  };
117
136
  return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
@@ -150,12 +169,16 @@ function writeReleaseCandidateStageCapsules({
150
169
  stageCapsulesPath,
151
170
  `${JSON.stringify(stageCapsules, null, 2)}\n`,
152
171
  );
172
+ const qualificationPath = path.join(path.dirname(outputPath), "release-candidate-publication-qualification.json");
173
+ fs.writeFileSync(qualificationPath, `${JSON.stringify(stageCapsules.publicationQualificationReceipt, null, 2)}\n`);
153
174
  return {
154
175
  path: path
155
176
  .relative(process.cwd(), stageCapsulesPath)
156
177
  .split(path.sep)
157
178
  .join("/"),
158
179
  root: stageCapsules.root,
180
+ qualificationPath: path.relative(process.cwd(), qualificationPath).split(path.sep).join("/"),
181
+ qualificationRoot: stageCapsules.publicationQualificationRoot,
159
182
  };
160
183
  }
161
184
 
@@ -246,6 +269,8 @@ export function generateReleaseCandidatePassportCli() {
246
269
  }),
247
270
  "release-candidate-stage-capsules-path": stageCapsules.path,
248
271
  "release-candidate-stage-capsules-root": stageCapsules.root,
272
+ "release-candidate-publication-qualification-path": stageCapsules.qualificationPath,
273
+ "release-candidate-publication-qualification-root": stageCapsules.qualificationRoot,
249
274
  });
250
275
  return passport;
251
276
  }
@@ -66,10 +66,15 @@ const RELEASE_TAIL_CAPABILITIES_SCHEMA =
66
66
  "schemas/release-tail-capabilities-v1.schema.json";
67
67
  const RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE =
68
68
  "contracts/release-tail-capabilities-v1.schema.json";
69
+ const V4_PUBLICATION_QUALIFICATION_SCHEMA =
70
+ "schemas/v4-publication-qualification-v1.schema.json";
71
+ const V4_PUBLICATION_QUALIFICATION_SCHEMA_SOURCE =
72
+ "contracts/v4-publication-qualification-v1.schema.json";
69
73
  const PUBLICATION_REHEARSAL_SITE_SCHEMAS = [
70
74
  V4_PUBLICATION_REHEARSAL_SCHEMA,
71
75
  V4_ADOPTER_DELIVERY_SCHEMA,
72
76
  RELEASE_TAIL_CAPABILITIES_SCHEMA,
77
+ V4_PUBLICATION_QUALIFICATION_SCHEMA,
73
78
  ];
74
79
  const root = path.resolve(import.meta.dirname, "..");
75
80
  const outputDir = path.join(root, "dist", "site");
@@ -102,6 +107,7 @@ function siteFileBytes(name, value) {
102
107
  if (name === DEV_DELIVERY_AUTHORITY_SCHEMA) return readText(DEV_DELIVERY_AUTHORITY_SCHEMA_SOURCE);
103
108
  if (name === V4_COMPATIBILITY_FACTS_SCHEMA) return readText(V4_COMPATIBILITY_FACTS_SCHEMA_SOURCE);
104
109
  if (name === V4_PUBLICATION_REHEARSAL_SCHEMA) return readText(V4_PUBLICATION_REHEARSAL_SCHEMA_SOURCE);
110
+ if (name === V4_PUBLICATION_QUALIFICATION_SCHEMA) return readText(V4_PUBLICATION_QUALIFICATION_SCHEMA_SOURCE);
105
111
  if (name === V4_ADOPTER_DELIVERY_SCHEMA) return readText(V4_ADOPTER_DELIVERY_SCHEMA_SOURCE);
106
112
  if (name === RELEASE_TAIL_CAPABILITIES_SCHEMA) return readText(RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE);
107
113
  return stableJson(value);
@@ -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 || "",
@@ -230,6 +230,7 @@ export function nodeApiMeta(exportName) {
230
230
  "./v4-tail-reseal": { group: "release-passport-trust", summary: "Effect-pure retained Stage Capsule validation and explicit Warrant-, credential-, and release-tail-fenced macOS reseal planning APIs." },
231
231
  "./v4-tail-reseal-receipt": { group: "release-passport-trust", summary: "Four-platform byte readback, standard candidate Passport binding, and terminal v4 tail-reseal receipt APIs." },
232
232
  "./v4-publication-rehearsal": { group: "release-passport-trust", summary: "Content-addressed v4 publication rehearsal capsules, shared production release-tail execution, replay, provider observation, and fail-closed verification APIs." },
233
+ "./v4-publication-qualification": { group: "release-passport-trust", summary: "Sealed v4 candidate, source, artifact, policy, freshness, and command-free promotion admission APIs." },
233
234
  "./adopter-delivery-gate": { group: "reusable-build", summary: "Protocol-neutral adopter delivery gate, driver interface, artifact profiles, and canonical result APIs." },
234
235
  "./kfd-adopter-category-driver": { group: "kfd-trust", summary: "Published KFD adopter-category driver and exact package-cut verification APIs." },
235
236
  "./buildchain-delivery-infrastructure": { group: "reusable-build", summary: "Public delivery-infrastructure instance creation and KFD verification APIs." },
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { assertV4DeclarativePromotionInputs } from "../packages/core/v4-publication-qualification.js";
5
+
6
+ export function admitV4DeclarativePromotion({
7
+ inputs,
8
+ runtimeRef,
9
+ declarative,
10
+ }) {
11
+ if (!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))) {
12
+ return Object.freeze({ mode: "legacy", admitted: true });
13
+ }
14
+ if (declarative !== true) {
15
+ const error = new Error(
16
+ "Buildchain v4 release-candidate-promote requires declarative-release-tail=true",
17
+ );
18
+ error.code = "v4-declarative-release-tail-required";
19
+ throw error;
20
+ }
21
+ assertV4DeclarativePromotionInputs(inputs);
22
+ return Object.freeze({ mode: "v4-declarative", admitted: true });
23
+ }
24
+
25
+ if (
26
+ process.argv[1] &&
27
+ import.meta.url === pathToFileURL(process.argv[1]).href
28
+ ) {
29
+ try {
30
+ const result = admitV4DeclarativePromotion({
31
+ inputs: JSON.parse(process.env.BUILDCHAIN_PROMOTION_INPUTS_JSON || "{}"),
32
+ runtimeRef: process.env.BUILDCHAIN_RUNTIME_REF,
33
+ declarative: process.env.BUILDCHAIN_DECLARATIVE_RELEASE_TAIL === "true",
34
+ });
35
+ process.stdout.write(`${JSON.stringify(result)}\n`);
36
+ } catch (error) {
37
+ process.stderr.write(
38
+ `v4-declarative-promotion-admission: ${error.code || "rejected"}: ${error.message}\n`,
39
+ );
40
+ process.exitCode = 1;
41
+ }
42
+ }