@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
@@ -4,9 +4,11 @@ import {
4
4
  v4FloatingConsumerDocumentRoot,
5
5
  verifyV4FloatingConsumerPolicyCertification,
6
6
  } from "./v4-floating-consumer-evidence.js";
7
-
7
+ import {
8
+ verifyV4RuntimeAuthorizationReceipt,
9
+ verifyV4RuntimeResumeLineage,
10
+ } from "./v4-runtime-ref-resume-authority.js";
8
11
  const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
9
-
10
12
  export function isV4PromotionRouting(value = undefined) {
11
13
  return [
12
14
  value?.router?.ref,
@@ -14,7 +16,6 @@ export function isV4PromotionRouting(value = undefined) {
14
16
  value?.runtime?.requestedRef,
15
17
  ].some((ref) => /^v4(?:-alpha)?$/u.test(String(ref || "")));
16
18
  }
17
-
18
19
  function resolveInside(root, relative, label) {
19
20
  const resolvedRoot = path.resolve(root);
20
21
  const file = path.resolve(resolvedRoot, relative);
@@ -23,7 +24,6 @@ function resolveInside(root, relative, label) {
23
24
  }
24
25
  return file;
25
26
  }
26
-
27
27
  function resolveCallerLock(root, relative, label) {
28
28
  const file = resolveInside(root, relative, `${label} contract lock`);
29
29
  if (!fs.existsSync(file)) {
@@ -76,11 +76,36 @@ function normalizeCertification(value, expected) {
76
76
  }
77
77
  return { certificationRoot: verification.certificationRoot, certification };
78
78
  }
79
-
79
+ export function resolveV4ConsumerPolicyCertificationIdentity({
80
+ release = {},
81
+ routing = {},
82
+ runtimeResume = undefined,
83
+ sourceSha = "",
84
+ } = {}) {
85
+ const certification =
86
+ release?.certification?.certification || release?.certification;
87
+ const certifiedSourceSha = certification?.caller?.sourceSha || "";
88
+ const treeEquivalentSources = [
89
+ release?.builtSourceSha || release?.built_source_sha,
90
+ release?.promotionChannelSha || release?.promotion_channel_sha,
91
+ ];
92
+ const buildSha = runtimeResume?.lineage?.attempts?.build?.runtimeSha;
93
+ const certifiedSha = certification?.invocation?.resolvedRuntimeSha || "";
94
+ return {
95
+ sourceSha:
96
+ certifiedSourceSha === sourceSha ||
97
+ (release?.treeEquivalent === true &&
98
+ treeEquivalentSources.includes(certifiedSourceSha))
99
+ ? certifiedSourceSha
100
+ : sourceSha,
101
+ runtimeSha: buildSha || certifiedSha || routing?.runtime?.resolvedSha || "",
102
+ };
103
+ }
80
104
  export function requireV4ConsumerPolicyCertification({
81
105
  value,
82
106
  repository,
83
107
  sourceSha,
108
+ runtimeSha = "",
84
109
  routing,
85
110
  cwd,
86
111
  certificationRoot,
@@ -100,7 +125,7 @@ export function requireV4ConsumerPolicyCertification({
100
125
  return normalizeCertification(value, {
101
126
  repository,
102
127
  sourceSha,
103
- runtimeSha: routing?.runtime?.resolvedSha || "",
128
+ runtimeSha: runtimeSha || routing?.runtime?.resolvedSha || "",
104
129
  certificationRoot,
105
130
  ...(v4Routing ? resolveV4FloatingConsumerCallerLocks(value, cwd) : {}),
106
131
  });
@@ -112,15 +137,136 @@ export function releasePassportCertificationVerificationOptions({
112
137
  routing,
113
138
  }) {
114
139
  const authority = evidence?.certification?.authority || {};
140
+ const identity = resolveV4ConsumerPolicyCertificationIdentity({
141
+ release: { ...passport?.release, certification: evidence?.certification },
142
+ routing,
143
+ runtimeResume: passport?.v4RuntimeResume,
144
+ sourceSha: passport?.release?.sourceSha || "",
145
+ });
115
146
  return {
116
147
  certification: evidence?.certification,
117
148
  certificationRoot: evidence?.certificationRoot,
118
149
  repository: passport?.product?.repository || "",
119
- sourceSha: passport?.release?.sourceSha || "",
120
- resolvedRuntimeSha: routing?.runtime?.resolvedSha || "",
150
+ sourceSha: identity.sourceSha,
151
+ resolvedRuntimeSha: identity.runtimeSha,
121
152
  stableLockRoot: authority.contractLocks?.stable?.root || "",
122
153
  alphaLockRoot: authority.contractLocks?.alpha?.root || "",
123
154
  stableLockPath: authority.contractLocks?.stable?.path || "",
124
155
  alphaLockPath: authority.contractLocks?.alpha?.path || "",
125
156
  };
126
157
  }
158
+ export function normalizePromotionRouting(value = undefined) {
159
+ if (value === undefined) return undefined;
160
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
161
+ throw new Error("release.promotionRouting must be a JSON object");
162
+ }
163
+ if (value.contract !== "buildchain.promotion-routing/v1") {
164
+ throw new Error(
165
+ "release.promotionRouting contract must be buildchain.promotion-routing/v1",
166
+ );
167
+ }
168
+ for (const [label, field] of [
169
+ ["router.ref", value.router?.ref],
170
+ ["router.sha", value.router?.sha],
171
+ ["shell.ref", value.shell?.ref],
172
+ ["shell.sha", value.shell?.sha],
173
+ ["runtime.requestedRef", value.runtime?.requestedRef],
174
+ ["runtime.resolvedSha", value.runtime?.resolvedSha],
175
+ ["contractLock.path", value.contractLock?.path],
176
+ ["contractLock.digest", value.contractLock?.digest],
177
+ ["publication.channel", value.publication?.channel],
178
+ ["publication.targetRef", value.publication?.targetRef],
179
+ ]) {
180
+ if (!String(field || "").trim())
181
+ throw new Error(`release.promotionRouting.${label} is required`);
182
+ }
183
+ for (const [label, sha] of [
184
+ ["router.sha", value.router.sha],
185
+ ["shell.sha", value.shell.sha],
186
+ ["runtime.resolvedSha", value.runtime.resolvedSha],
187
+ ]) {
188
+ if (!/^[0-9a-f]{40}$/i.test(String(sha))) {
189
+ throw new Error(
190
+ `release.promotionRouting.${label} must be a 40-character Git SHA`,
191
+ );
192
+ }
193
+ }
194
+ if (!/^sha256:[0-9a-f]{64}$/i.test(String(value.contractLock.digest))) {
195
+ throw new Error(
196
+ "release.promotionRouting.contractLock.digest must be a sha256 digest",
197
+ );
198
+ }
199
+ return JSON.parse(JSON.stringify(value));
200
+ }
201
+
202
+ export function normalizePromotionEvidence({
203
+ release = {},
204
+ v4ConsumerPolicyCertification: certification,
205
+ v4ConsumerPolicyCertificationRoot: certificationRoot,
206
+ v4RuntimeResumeEvidence,
207
+ repository,
208
+ sourceSha,
209
+ cwd,
210
+ }) {
211
+ const routing = normalizePromotionRouting(release.promotionRouting);
212
+ const identity = resolveV4ConsumerPolicyCertificationIdentity({
213
+ release: { ...release, certification },
214
+ routing,
215
+ runtimeResume: v4RuntimeResumeEvidence,
216
+ sourceSha,
217
+ });
218
+ return {
219
+ routing,
220
+ consumerPolicy: requireV4ConsumerPolicyCertification({
221
+ value: certification,
222
+ repository,
223
+ sourceSha: identity.sourceSha,
224
+ runtimeSha: identity.runtimeSha,
225
+ routing,
226
+ cwd,
227
+ certificationRoot,
228
+ }),
229
+ };
230
+ }
231
+
232
+ export function normalizeV4RuntimeResumeEvidence(value, expected = {}) {
233
+ if (value === undefined || value === null || value === "") return undefined;
234
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
235
+ throw new Error("v4RuntimeResumeEvidence must be a JSON object");
236
+ }
237
+ const authorizationVerification = verifyV4RuntimeAuthorizationReceipt({
238
+ receipt: value.authorization,
239
+ receiptRoot: value.authorizationRoot,
240
+ repository: expected.repository,
241
+ sourceSha: expected.sourceSha,
242
+ runtimeSha: expected.resumeRuntimeSha,
243
+ consumerPolicyReceiptRoot: expected.consumerPolicyReceiptRoot,
244
+ });
245
+ if (!authorizationVerification.ok) {
246
+ throw new Error(
247
+ `v4 runtime authorization invalid: ${authorizationVerification.failures.join(", ")}`,
248
+ );
249
+ }
250
+ const lineageVerification = verifyV4RuntimeResumeLineage({
251
+ lineage: value.lineage,
252
+ lineageRoot: value.lineageRoot,
253
+ repository: expected.repository,
254
+ sourceSha: expected.sourceSha,
255
+ resumeRuntimeSha: expected.resumeRuntimeSha,
256
+ consumerPolicyReceiptRoot: expected.consumerPolicyReceiptRoot,
257
+ });
258
+ if (!lineageVerification.ok) {
259
+ throw new Error(
260
+ `v4 runtime resume lineage invalid: ${lineageVerification.failures.join(", ")}`,
261
+ );
262
+ }
263
+ if (value.lineage?.authorizationRoot !== value.authorizationRoot) {
264
+ throw new Error("v4 runtime resume lineage authorization root mismatch");
265
+ }
266
+ return {
267
+ authorizationRoot: value.authorizationRoot,
268
+ authorization: structuredClone(value.authorization),
269
+ lineageRoot: value.lineageRoot,
270
+ lineage: structuredClone(value.lineage),
271
+ };
272
+ }
@@ -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
+ }