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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/architecture/agent-change-map.md +15 -6
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +9 -2
  4. package/architecture/maintainability-debt.json +66 -136
  5. package/architecture/maintainability-policy.json +17 -12
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +3 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  10. package/architecture/v4-release-invocation-fixtures.json +139 -0
  11. package/architecture/v4-release-topology.json +316 -0
  12. package/architecture/v4-runtime-semantic-closure.json +4 -1
  13. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  14. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  15. package/contracts/v4-release-invocation-v1.schema.json +134 -0
  16. package/dist/site/buildchain-contract.json +9 -9
  17. package/dist/site/buildchain-site.json +9 -9
  18. package/dist/site/kfd-claims.json +28 -7
  19. package/dist/site/kfd-upstream-aggregate.json +1 -1
  20. package/dist/site/manual-registry.json +1 -1
  21. package/dist/site/node-api-registry.json +92 -17
  22. package/dist/site/page-registry.json +4 -4
  23. package/dist/site/public-surface-audit.json +42 -10
  24. package/dist/site/publication-authority-registry.json +2 -3
  25. package/dist/site/publication-registry.json +4 -4
  26. package/dist/site/site-manifest.json +5 -5
  27. package/dist/site/workflow-registry.json +38 -11
  28. package/docs/dev-delivery-warrant.md +21 -0
  29. package/docs/node-api-reference.md +19 -16
  30. package/package.json +2 -2
  31. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  32. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  33. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  34. package/packages/core/dev-delivery-warrant-legacy-recovery.js +248 -0
  35. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  36. package/packages/core/dev-delivery-warrant-state.js +54 -54
  37. package/packages/core/dev-delivery-warrant.js +8 -0
  38. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  39. package/packages/core/release-tail-product-capabilities.js +29 -0
  40. package/packages/core/release-tail-provider-plane.js +2 -2
  41. package/packages/core/v4-canonical-contracts.js +14 -0
  42. package/packages/core/v4-floating-consumer-policy.js +4 -1
  43. package/packages/core/v4-product-publication.js +387 -0
  44. package/packages/core/v4-protected-publication-source.js +93 -0
  45. package/packages/core/v4-publication-qualification.js +1 -0
  46. package/packages/core/v4-release-invocation.js +425 -0
  47. package/scripts/audit-publication-control-plane.mjs +0 -1
  48. package/scripts/check-inventory.mjs +27 -59
  49. package/scripts/check-maintainability.mjs +13 -5
  50. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  51. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  52. package/scripts/check-v4-release-topology.mjs +490 -0
  53. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  54. package/scripts/dev-delivery-warrant.mjs +41 -7
  55. package/scripts/generate-channel-promotion-workflow.mjs +14 -55
  56. package/scripts/release-candidate-resolver.mjs +23 -24
  57. package/scripts/resume-from-candidate-run.mjs +16 -17
  58. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  59. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  60. package/scripts/v4-product-publication-intent.mjs +114 -0
  61. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  62. package/scripts/capture-package-release-propagation.mjs +0 -263
  63. package/scripts/publication-commit-evidence.mjs +0 -444
  64. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  65. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -0,0 +1,72 @@
1
+ import { devDeliveryContentRoot } from "./dev-delivery-common.js";
2
+ import {
3
+ DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT,
4
+ TERMINAL_STATES,
5
+ transitionDevDeliveryQueue,
6
+ } from "./dev-delivery-warrant-state.js";
7
+
8
+ export const DEV_DELIVERY_WRITER_PROTOCOL = Object.freeze({
9
+ schema: "kungfu.buildchain.dev-delivery-writer-protocol/v1",
10
+ requiredCandidateCapabilities: [
11
+ "environment-root-v1",
12
+ "native-command-contract-v1",
13
+ ],
14
+ });
15
+ export const DEV_DELIVERY_WRITER_PROTOCOL_FENCE_RECEIPT_SCHEMA =
16
+ "kungfu.buildchain.dev-delivery-writer-protocol-fence-receipt/v1";
17
+ if (
18
+ devDeliveryContentRoot(DEV_DELIVERY_WRITER_PROTOCOL) !==
19
+ DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT
20
+ ) {
21
+ throw new Error("dev delivery writer protocol root drift");
22
+ }
23
+
24
+ export function fenceDevDeliveryWriterProtocol(
25
+ queueInput,
26
+ { now = new Date().toISOString() } = {},
27
+ ) {
28
+ const transaction = transitionDevDeliveryQueue(
29
+ queueInput,
30
+ (queue) => {
31
+ if (
32
+ queue.policy.minimumWriterProtocolRoot ===
33
+ DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT
34
+ ) {
35
+ return {
36
+ action: "minimum-writer-protocol-already-fenced",
37
+ preserveState: true,
38
+ };
39
+ }
40
+ if (
41
+ queue.activeWarrant ||
42
+ queue.candidates.some(
43
+ (candidate) => !TERMINAL_STATES.has(candidate.status),
44
+ )
45
+ ) {
46
+ throw new Error(
47
+ "minimum writer protocol can be fenced only while the queue is idle",
48
+ );
49
+ }
50
+ queue.policy.minimumWriterProtocolRoot =
51
+ DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT;
52
+ return { action: "minimum-writer-protocol-fenced" };
53
+ },
54
+ now,
55
+ );
56
+ const receipt = {
57
+ schema: DEV_DELIVERY_WRITER_PROTOCOL_FENCE_RECEIPT_SCHEMA,
58
+ action: transaction.result.action,
59
+ repository: transaction.after.repository,
60
+ protectedBase: transaction.after.protectedBase,
61
+ minimumWriterProtocolRoot: DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT,
62
+ expectedOldStateRoot: transaction.expectedOldStateRoot,
63
+ nextStateRoot: transaction.after.stateRoot,
64
+ nextAction:
65
+ "Use only a Buildchain runtime that preserves the rooted minimum writer protocol.",
66
+ };
67
+ return {
68
+ queue: transaction.after,
69
+ receipt,
70
+ receiptRoot: devDeliveryContentRoot(receipt),
71
+ };
72
+ }
@@ -0,0 +1,29 @@
1
+ export const RELEASE_TAIL_PRODUCT_CAPABILITIES = Object.freeze([
2
+ Object.freeze({
3
+ id: "product.version-state.materialize",
4
+ executor: "provider-adapter",
5
+ adapter: "github-version-state",
6
+ effectKind: "product-version-state-materialization",
7
+ observationKind: "product-version-state-readback",
8
+ receiptKind: "product-version-state",
9
+ transactionState: "preparing",
10
+ }),
11
+ Object.freeze({
12
+ id: "product.package.publish",
13
+ executor: "provider-adapter",
14
+ adapter: "npm-trusted-publishing",
15
+ effectKind: "product-package-publication",
16
+ observationKind: "product-package-readback",
17
+ receiptKind: "product-package-publication",
18
+ transactionState: "publishing",
19
+ }),
20
+ Object.freeze({
21
+ id: "product.release-refs.converge",
22
+ executor: "provider-adapter",
23
+ adapter: "github-release-refs",
24
+ effectKind: "product-release-ref-convergence",
25
+ observationKind: "product-release-ref-readback",
26
+ receiptKind: "product-release-ref-convergence",
27
+ transactionState: "committing",
28
+ }),
29
+ ]);
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
-
4
+ import { RELEASE_TAIL_PRODUCT_CAPABILITIES } from "./release-tail-product-capabilities.js";
5
5
  export const RELEASE_TAIL_DECLARATION_CONTRACT =
6
6
  "kungfu-buildchain-release-tail-capabilities";
7
7
  export const RELEASE_TAIL_TRANSACTION_POLICY = "buildchain.release-tail/v1";
@@ -27,8 +27,8 @@ export const RELEASE_TAIL_STATES = Object.freeze([
27
27
  "repair-required",
28
28
  "terminal-failure",
29
29
  ]);
30
-
31
30
  export const RELEASE_TAIL_CAPABILITY_REGISTRY = Object.freeze([
31
+ ...RELEASE_TAIL_PRODUCT_CAPABILITIES,
32
32
  Object.freeze({
33
33
  id: "artifact.publish",
34
34
  executor: "provider-adapter",
@@ -56,11 +56,25 @@ const ROOT_DOMAINS = new Set([
56
56
  "stable-publication-fence",
57
57
  "partial-mutation-recovery-checkpoint",
58
58
  "partial-mutation-recovery-plan",
59
+ "v4-product-required-artifacts",
60
+ "v4-product-publication-intent",
61
+ "v4-product-publication-operation",
62
+ "v4-product-publication-plan",
59
63
  "tail-reseal-plan",
60
64
  "tail-reseal-admission",
61
65
  "tail-reseal-artifact-files",
62
66
  "tail-reseal-receipt",
63
67
  "release-candidate-passport",
68
+ "release-invocation-publisher",
69
+ "release-invocation-runtime",
70
+ "release-invocation-candidate",
71
+ "release-invocation-target",
72
+ "release-invocation-authority",
73
+ "release-invocation-provider",
74
+ "release-invocation-parent",
75
+ "release-invocation",
76
+ "release-transaction",
77
+ "release-receipt",
64
78
  ]);
65
79
  const FAULT_CLASSES = new Set([
66
80
  "validation",
@@ -302,8 +302,11 @@ function classifyBuildchainUses(records, failures, { repository } = {}) {
302
302
  parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector;
303
303
  const protectedBootstrap =
304
304
  repository === BUILDCHAIN_REPOSITORY && bootstrapShape;
305
+ const unauthorizedBootstrap =
306
+ repository !== BUILDCHAIN_REPOSITORY && bootstrapShape;
305
307
  const channel =
306
- CHANNELS[parsed.selector] || (protectedBootstrap ? "alpha" : "");
308
+ !unauthorizedBootstrap &&
309
+ (CHANNELS[parsed.selector] || (protectedBootstrap ? "alpha" : ""));
307
310
  uses.push({
308
311
  ...record,
309
312
  ...parsed,
@@ -0,0 +1,387 @@
1
+ import { v4ContentRoot, validateV4Root } from "./v4-canonical-contracts.js";
2
+ import {
3
+ RELEASE_TAIL_DECLARATION_CONTRACT,
4
+ RELEASE_TAIL_EFFECT_SCHEMA,
5
+ RELEASE_TAIL_OBSERVATION_SCHEMA,
6
+ RELEASE_TAIL_RECEIPT_SCHEMA,
7
+ RELEASE_TAIL_TRANSACTION_POLICY,
8
+ } from "./release-tail-provider-plane.js";
9
+ import { RELEASE_TAIL_PRODUCT_CAPABILITIES } from "./release-tail-product-capabilities.js";
10
+
11
+ export const V4_PRODUCT_PUBLICATION_INTENT_CONTRACT =
12
+ "kungfu-buildchain-v4-product-publication-intent/v1";
13
+ export const V4_PRODUCT_PUBLICATION_PLAN_CONTRACT =
14
+ "kungfu-buildchain-v4-product-publication-plan/v1";
15
+
16
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-alpha\.(\d+))?$/u;
17
+ const SHA = /^[0-9a-f]{40}$/u;
18
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
19
+ const REPOSITORY = /^[^/\s]+\/[^/\s]+$/u;
20
+
21
+ function fail(message) {
22
+ throw new Error(`invalid v4 product publication: ${message}`);
23
+ }
24
+
25
+ function parseVersion(value, label) {
26
+ const normalized = String(value || "").trim();
27
+ const match = normalized.match(VERSION);
28
+ if (!match) fail(`${label} must be an exact stable or alpha version`);
29
+ return {
30
+ value: normalized,
31
+ major: Number(match[1]),
32
+ minor: Number(match[2]),
33
+ patch: Number(match[3]),
34
+ alpha: match[4] === undefined ? null : Number(match[4]),
35
+ };
36
+ }
37
+
38
+ function channelName(value) {
39
+ if (value === "alpha") return "alpha";
40
+ if (["release", "stable", "major"].includes(value)) return "stable";
41
+ fail(`unsupported channel '${value}'`);
42
+ }
43
+
44
+ function assertLane({ channel, targetRef, version }) {
45
+ const expression =
46
+ channel === "alpha"
47
+ ? /^alpha\/v(\d+)\/v(\d+)\.(\d+)$/u
48
+ : /^release\/v(\d+)\/v(\d+)\.(\d+)$/u;
49
+ const match = String(targetRef || "").match(expression);
50
+ if (!match) fail(`target ref '${targetRef}' is not a ${channel} lane`);
51
+ if (
52
+ Number(match[1]) !== version.major ||
53
+ Number(match[2]) !== version.major ||
54
+ Number(match[3]) !== version.minor
55
+ )
56
+ fail("target ref does not match the publication version line");
57
+ }
58
+
59
+ function observedAlphaNumber(version, expected) {
60
+ const parsed = parseVersion(version, "observed version");
61
+ return parsed.major === expected.major &&
62
+ parsed.minor === expected.minor &&
63
+ parsed.patch === expected.patch
64
+ ? parsed.alpha
65
+ : null;
66
+ }
67
+
68
+ function requiredString(value, label) {
69
+ const normalized = String(value || "").trim();
70
+ if (!normalized) fail(`${label} must be a non-empty string`);
71
+ return normalized;
72
+ }
73
+
74
+ function requiredRoot(value, label) {
75
+ const normalized = requiredString(value, label).toLowerCase();
76
+ if (!ROOT.test(normalized)) fail(`${label} must be a sha256 content root`);
77
+ return normalized;
78
+ }
79
+
80
+ export function selectV4ProductPublicationIntent({
81
+ channel,
82
+ targetRef,
83
+ sourceSha,
84
+ sourceTimestamp,
85
+ repository,
86
+ packageName,
87
+ distTag,
88
+ sealedBundleRoot,
89
+ requiredArtifactsRoot,
90
+ candidateVersion,
91
+ recoveredVersion = "",
92
+ observedVersions = [],
93
+ }) {
94
+ const normalizedChannel = channelName(channel);
95
+ if (!SHA.test(String(sourceSha || "")))
96
+ fail("sourceSha must be an exact Git SHA");
97
+ if (!REPOSITORY.test(String(repository || "")))
98
+ fail("repository must be owner/repo");
99
+ const normalizedTimestamp = requiredString(
100
+ sourceTimestamp,
101
+ "sourceTimestamp",
102
+ );
103
+ if (Number.isNaN(Date.parse(normalizedTimestamp)))
104
+ fail("sourceTimestamp must be an ISO timestamp");
105
+ const normalizedPackageName = requiredString(packageName, "packageName");
106
+ const normalizedDistTag = requiredString(distTag, "distTag");
107
+ if (!/^[a-z0-9][a-z0-9._-]*$/u.test(normalizedDistTag))
108
+ fail("distTag must be an npm dist-tag");
109
+ if (!Array.isArray(observedVersions))
110
+ fail("observedVersions must be an array");
111
+ const candidate = parseVersion(candidateVersion, "candidateVersion");
112
+ assertLane({ channel: normalizedChannel, targetRef, version: candidate });
113
+ const observed = [...new Set(observedVersions.map(String))].sort();
114
+ let version;
115
+ let mode = "fresh";
116
+ if (String(recoveredVersion || "").trim()) {
117
+ const recovered = parseVersion(recoveredVersion, "recoveredVersion");
118
+ if (
119
+ recovered.major !== candidate.major ||
120
+ recovered.minor !== candidate.minor ||
121
+ recovered.patch !== candidate.patch ||
122
+ (normalizedChannel === "alpha" && recovered.alpha === null) ||
123
+ (normalizedChannel === "stable" && recovered.alpha !== null)
124
+ )
125
+ fail("recoveredVersion is incompatible with the candidate release line");
126
+ version = recovered.value;
127
+ mode = "resume";
128
+ } else if (normalizedChannel === "alpha") {
129
+ if (candidate.alpha === null)
130
+ fail("fresh alpha publication requires an alpha candidate version");
131
+ const highest = observed.reduce(
132
+ (current, entry) =>
133
+ Math.max(current, observedAlphaNumber(entry, candidate) ?? -1),
134
+ candidate.alpha,
135
+ );
136
+ version = `${candidate.major}.${candidate.minor}.${candidate.patch}-alpha.${highest + 1}`;
137
+ } else {
138
+ version = `${candidate.major}.${candidate.minor}.${candidate.patch}`;
139
+ }
140
+ const intent = {
141
+ schema: V4_PRODUCT_PUBLICATION_INTENT_CONTRACT,
142
+ mode,
143
+ channel: normalizedChannel,
144
+ targetRef,
145
+ sourceSha,
146
+ sourceTimestamp: new Date(normalizedTimestamp).toISOString(),
147
+ repository,
148
+ packageName: normalizedPackageName,
149
+ distTag: normalizedDistTag,
150
+ sealedBundleRoot: requiredRoot(sealedBundleRoot, "sealedBundleRoot"),
151
+ requiredArtifactsRoot: requiredRoot(
152
+ requiredArtifactsRoot,
153
+ "requiredArtifactsRoot",
154
+ ),
155
+ candidateVersion: candidate.value,
156
+ version,
157
+ exactTag: `v${version}`,
158
+ observedVersions: observed,
159
+ };
160
+ return {
161
+ ...intent,
162
+ intentRoot: v4ContentRoot("v4-product-publication-intent", intent),
163
+ };
164
+ }
165
+
166
+ function alphaReferences(intent, version) {
167
+ const line = `v${version.major}.${version.minor}`;
168
+ return [
169
+ { ref: `refs/tags/${intent.exactTag}`, target: "source" },
170
+ { ref: `refs/heads/${intent.targetRef}`, target: "version-state" },
171
+ {
172
+ ref: `refs/heads/dev/v${version.major}/${line}`,
173
+ target: "version-state",
174
+ },
175
+ { ref: `refs/tags/${line}-alpha`, target: "version-state" },
176
+ { ref: `refs/tags/v${version.major}-alpha`, target: "version-state" },
177
+ ];
178
+ }
179
+
180
+ function stableReferences(intent, version) {
181
+ return [
182
+ { ref: `refs/tags/${intent.exactTag}`, target: "source" },
183
+ { ref: `refs/heads/${intent.targetRef}`, target: "version-state" },
184
+ {
185
+ ref: `refs/tags/v${version.major}.${version.minor}`,
186
+ target: "version-state",
187
+ },
188
+ { ref: `refs/tags/v${version.major}`, target: "version-state" },
189
+ ];
190
+ }
191
+
192
+ export function createV4ProductPublicationPlan({
193
+ intent,
194
+ invocationRoot,
195
+ transactionRoot,
196
+ }) {
197
+ validateV4Root(invocationRoot, "$/productPlan/invocationRoot");
198
+ validateV4Root(transactionRoot, "$/productPlan/transactionRoot");
199
+ const selected = selectV4ProductPublicationIntent({
200
+ channel: intent.channel,
201
+ targetRef: intent.targetRef,
202
+ sourceSha: intent.sourceSha,
203
+ sourceTimestamp: intent.sourceTimestamp,
204
+ repository: intent.repository,
205
+ packageName: intent.packageName,
206
+ distTag: intent.distTag,
207
+ sealedBundleRoot: intent.sealedBundleRoot,
208
+ requiredArtifactsRoot: intent.requiredArtifactsRoot,
209
+ candidateVersion: intent.candidateVersion,
210
+ recoveredVersion: intent.mode === "resume" ? intent.version : "",
211
+ observedVersions: intent.observedVersions,
212
+ });
213
+ if (selected.intentRoot !== intent.intentRoot)
214
+ fail("intentRoot does not match the canonical publication intent");
215
+ const parsed = parseVersion(intent.version, "intent.version");
216
+ const references =
217
+ intent.channel === "alpha"
218
+ ? alphaReferences(intent, parsed)
219
+ : stableReferences(intent, parsed);
220
+ const stateRef = `refs/heads/buildchain/v4-product-state/${intent.sourceSha}-${intent.version.replaceAll(".", "-")}`;
221
+ const operations = [
222
+ {
223
+ id: "product.version-state.materialize",
224
+ adapter: "github-version-state",
225
+ authority: "contents-write",
226
+ target: {
227
+ repository: intent.repository,
228
+ version: intent.version,
229
+ sourceSha: intent.sourceSha,
230
+ sourceTimestamp: intent.sourceTimestamp,
231
+ stateRef,
232
+ },
233
+ },
234
+ {
235
+ id: "product.package.publish",
236
+ adapter: "npm-trusted-publishing",
237
+ authority: "oidc-provider-mutation",
238
+ target: {
239
+ packageName: intent.packageName,
240
+ version: intent.version,
241
+ distTag: intent.distTag,
242
+ sealedBundleRoot: intent.sealedBundleRoot,
243
+ requiredArtifactsRoot: intent.requiredArtifactsRoot,
244
+ },
245
+ },
246
+ {
247
+ id: "product.release-refs.converge",
248
+ adapter: "github-release-refs",
249
+ authority: "contents-write",
250
+ target: {
251
+ repository: intent.repository,
252
+ sourceSha: intent.sourceSha,
253
+ stateRef,
254
+ references,
255
+ },
256
+ },
257
+ ].map((operation) => ({
258
+ ...operation,
259
+ operationRoot: v4ContentRoot("v4-product-publication-operation", operation),
260
+ }));
261
+ const plan = {
262
+ schema: V4_PRODUCT_PUBLICATION_PLAN_CONTRACT,
263
+ intentRoot: intent.intentRoot,
264
+ invocationRoot,
265
+ transactionRoot,
266
+ operationOrder: operations.map(({ id }) => id),
267
+ operations,
268
+ };
269
+ return {
270
+ ...plan,
271
+ planRoot: v4ContentRoot("v4-product-publication-plan", plan),
272
+ };
273
+ }
274
+
275
+ const PRODUCT_CAPABILITIES = Object.fromEntries(
276
+ RELEASE_TAIL_PRODUCT_CAPABILITIES.map((capability) => [
277
+ capability.id,
278
+ {
279
+ ...capability,
280
+ destinationKind:
281
+ capability.id === "product.package.publish"
282
+ ? "npm-package"
283
+ : capability.adapter,
284
+ },
285
+ ]),
286
+ );
287
+
288
+ function exactTagPattern(tag) {
289
+ return `^${tag.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}$`;
290
+ }
291
+
292
+ export function createV4ProductPublicationDeclaration({ intent, plan }) {
293
+ const rebuilt = createV4ProductPublicationPlan({
294
+ intent,
295
+ invocationRoot: plan.invocationRoot,
296
+ transactionRoot: plan.transactionRoot,
297
+ });
298
+ if (rebuilt.planRoot !== plan.planRoot)
299
+ fail("planRoot does not match the canonical publication plan");
300
+ const operationById = new Map(
301
+ plan.operations.map((operation) => [operation.id, operation]),
302
+ );
303
+ return {
304
+ contract: RELEASE_TAIL_DECLARATION_CONTRACT,
305
+ schemaVersion: 1,
306
+ transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
307
+ subject: {
308
+ repository: intent.repository,
309
+ sourceSha: intent.sourceSha,
310
+ version: intent.version,
311
+ tag: intent.exactTag,
312
+ channel: intent.channel,
313
+ },
314
+ capabilities: plan.operationOrder.map((id) => {
315
+ const operation = operationById.get(id);
316
+ const descriptor = PRODUCT_CAPABILITIES[id];
317
+ if (!operation || !descriptor)
318
+ fail(`unsupported product publication operation '${id}'`);
319
+ return {
320
+ id,
321
+ executor: "provider-adapter",
322
+ adapter: operation.adapter,
323
+ artifactRoles: [
324
+ { role: "publication-intent", root: intent.intentRoot },
325
+ ...(id === "product.package.publish"
326
+ ? [
327
+ { role: "sealed-bundle", root: intent.sealedBundleRoot },
328
+ {
329
+ role: "required-artifacts",
330
+ root: intent.requiredArtifactsRoot,
331
+ },
332
+ ]
333
+ : []),
334
+ ],
335
+ destination: {
336
+ kind: descriptor.destinationKind,
337
+ locator: `${operation.adapter}:${operation.operationRoot}`,
338
+ },
339
+ channelPolicy: {
340
+ channel: intent.channel,
341
+ tagPattern: exactTagPattern(intent.exactTag),
342
+ authorityMove:
343
+ id === "product.package.publish" ? "none" : "verified-ref",
344
+ },
345
+ activationPolicy: { mode: "none", environment: "none" },
346
+ readbackPredicates: [
347
+ {
348
+ id: `${id}.target-root`,
349
+ kind: "exact-root",
350
+ expected: operation.operationRoot,
351
+ },
352
+ ],
353
+ effect: {
354
+ schema: RELEASE_TAIL_EFFECT_SCHEMA,
355
+ kind: descriptor.effectKind,
356
+ },
357
+ observation: {
358
+ schema: RELEASE_TAIL_OBSERVATION_SCHEMA,
359
+ kind: descriptor.observationKind,
360
+ },
361
+ receipt: {
362
+ schema: RELEASE_TAIL_RECEIPT_SCHEMA,
363
+ kind: descriptor.receiptKind,
364
+ },
365
+ operationIdentity: {
366
+ transactionRoot: plan.transactionRoot,
367
+ capabilityId: id,
368
+ subjectRoot: intent.intentRoot,
369
+ targetRoot: operation.operationRoot,
370
+ attemptKey: `${plan.planRoot}:${id}`,
371
+ },
372
+ idempotency: {
373
+ scope: "subject-target",
374
+ duplicate: "readback-before-retry",
375
+ },
376
+ retry: {
377
+ class: "provider-transient",
378
+ localAttempts: 1,
379
+ exhausted: "blocked",
380
+ },
381
+ evidenceRequirements: [
382
+ "provider readback must match the rooted publication operation",
383
+ ],
384
+ };
385
+ }),
386
+ };
387
+ }
@@ -0,0 +1,93 @@
1
+ import { v4ContentRoot } from "./v4-canonical-contracts.js";
2
+
3
+ const SHA_PATTERN = /^[0-9a-f]{40}$/u;
4
+
5
+ function exactSha(value, label) {
6
+ const normalized = String(value || "").toLowerCase();
7
+ if (!SHA_PATTERN.test(normalized)) {
8
+ throw new Error(`${label} must be an exact Git SHA`);
9
+ }
10
+ return normalized;
11
+ }
12
+
13
+ function exactParents(value, label) {
14
+ if (!Array.isArray(value)) {
15
+ throw new Error(`${label} must be an ordered Git parent list`);
16
+ }
17
+ return value.map((entry, index) => exactSha(entry, `${label}[${index}]`));
18
+ }
19
+
20
+ export function bindV4ProtectedPublicationSource({
21
+ repository,
22
+ protectedCommit,
23
+ candidateCommit,
24
+ pullRequest,
25
+ } = {}) {
26
+ if (!/^[^/\s]+\/[^/\s]+$/u.test(String(repository || ""))) {
27
+ throw new Error("protected publication source requires owner/repository");
28
+ }
29
+ const protectedSource = {
30
+ sha: exactSha(protectedCommit?.sha, "protected source SHA"),
31
+ tree: exactSha(protectedCommit?.tree, "protected source tree"),
32
+ parents: exactParents(protectedCommit?.parents, "protected source parents"),
33
+ };
34
+ const candidateSource = {
35
+ sha: exactSha(candidateCommit?.sha, "candidate source SHA"),
36
+ tree: exactSha(candidateCommit?.tree, "candidate source tree"),
37
+ parents: exactParents(candidateCommit?.parents, "candidate source parents"),
38
+ };
39
+ if (protectedSource.tree !== candidateSource.tree) {
40
+ throw new Error(
41
+ "protected publication source tree does not match the qualified candidate tree",
42
+ );
43
+ }
44
+
45
+ let mode = "exact-commit";
46
+ let pullRequestNumber = null;
47
+ if (protectedSource.sha !== candidateSource.sha) {
48
+ mode = "merge-equivalent";
49
+ const sameParents =
50
+ protectedSource.parents.length === candidateSource.parents.length &&
51
+ protectedSource.parents.every(
52
+ (parent, index) => parent === candidateSource.parents[index],
53
+ );
54
+ if (!sameParents) {
55
+ throw new Error(
56
+ "protected publication source is not parent-equivalent to the qualified merge candidate",
57
+ );
58
+ }
59
+ pullRequestNumber = Number(pullRequest?.number || 0);
60
+ const pullRequestHead = exactSha(
61
+ pullRequest?.headSha,
62
+ "publication pull request head SHA",
63
+ );
64
+ const pullRequestMerge = exactSha(
65
+ pullRequest?.mergeSha,
66
+ "publication pull request merge SHA",
67
+ );
68
+ if (
69
+ !Number.isSafeInteger(pullRequestNumber) ||
70
+ pullRequestNumber <= 0 ||
71
+ pullRequest?.merged !== true ||
72
+ pullRequestMerge !== protectedSource.sha ||
73
+ !candidateSource.parents.includes(pullRequestHead)
74
+ ) {
75
+ throw new Error(
76
+ "protected publication source does not match the merged pull request and qualified merge candidate",
77
+ );
78
+ }
79
+ }
80
+
81
+ const binding = {
82
+ schema: "kungfu.buildchain.v4-protected-publication-source/v1",
83
+ repository,
84
+ mode,
85
+ protectedSource,
86
+ candidateSource,
87
+ pullRequestNumber,
88
+ };
89
+ return {
90
+ ...binding,
91
+ bindingRoot: v4ContentRoot("candidate-identity", binding),
92
+ };
93
+ }
@@ -207,6 +207,7 @@ export function validateV4PublicationQualificationReceipt(
207
207
  export function assertV4DeclarativePromotionInputs(inputs) {
208
208
  const forbidden = Object.entries(inputs || {}).filter(
209
209
  ([name, value]) =>
210
+ name !== "dry-run" &&
210
211
  /(command|cmd|script|shell|run)$/iu.test(name) &&
211
212
  String(value || "").trim() !== "",
212
213
  );