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

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 (50) hide show
  1. package/architecture/agent-change-map.md +7 -5
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +4 -1
  4. package/architecture/maintainability-debt.json +45 -119
  5. package/architecture/maintainability-policy.json +13 -8
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +2 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-release-invocation-fixtures.json +119 -0
  10. package/architecture/v4-release-topology.json +238 -0
  11. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  12. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  13. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  14. package/dist/site/buildchain-contract.json +9 -9
  15. package/dist/site/buildchain-site.json +9 -9
  16. package/dist/site/kfd-claims.json +9 -7
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +1 -1
  19. package/dist/site/node-api-registry.json +3 -3
  20. package/dist/site/page-registry.json +4 -4
  21. package/dist/site/public-surface-audit.json +23 -10
  22. package/dist/site/publication-authority-registry.json +2 -3
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/site-manifest.json +5 -5
  25. package/dist/site/workflow-registry.json +19 -11
  26. package/docs/dev-delivery-warrant.md +21 -0
  27. package/docs/node-api-reference.md +3 -3
  28. package/package.json +2 -2
  29. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  30. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  31. package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
  32. package/packages/core/dev-delivery-warrant-state.js +12 -28
  33. package/packages/core/v4-canonical-contracts.js +8 -0
  34. package/packages/core/v4-floating-consumer-policy.js +4 -1
  35. package/packages/core/v4-release-invocation.js +356 -0
  36. package/scripts/audit-publication-control-plane.mjs +0 -1
  37. package/scripts/check-inventory.mjs +27 -59
  38. package/scripts/check-maintainability.mjs +13 -5
  39. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  41. package/scripts/check-v4-release-topology.mjs +237 -0
  42. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  43. package/scripts/dev-delivery-warrant.mjs +29 -7
  44. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  45. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  46. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  47. package/scripts/capture-package-release-propagation.mjs +0 -263
  48. package/scripts/publication-commit-evidence.mjs +0 -444
  49. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  50. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -0,0 +1,246 @@
1
+ import {
2
+ devDeliveryClone as clone,
3
+ devDeliveryContentRoot,
4
+ devDeliveryExactRoot as exactRoot,
5
+ devDeliveryExactSha as exactSha,
6
+ devDeliveryPositiveInteger as positiveInteger,
7
+ devDeliveryText as text,
8
+ devDeliveryTimestamp as timestamp,
9
+ } from "./dev-delivery-common.js";
10
+ import {
11
+ TERMINAL_STATES,
12
+ normalizeDevDeliveryQueue,
13
+ } from "./dev-delivery-warrant-state.js";
14
+
15
+ export const LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA =
16
+ "kungfu.buildchain.legacy-terminal-recovery-request/v1";
17
+ export const LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA =
18
+ "kungfu.buildchain.legacy-hosted-terminal-evidence/v1";
19
+ export const LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA =
20
+ "kungfu.buildchain.legacy-terminal-recovery-receipt/v1";
21
+
22
+ function legacyLiveNativeCandidate(candidate) {
23
+ return (
24
+ !TERMINAL_STATES.has(candidate.status) &&
25
+ candidate.deliveryClass !== "non-native-fast" &&
26
+ (!candidate.environmentRoot || !candidate.nativeCommandContract)
27
+ );
28
+ }
29
+
30
+ function normalizeProviderJob(input, runAttempt) {
31
+ const job = {
32
+ id: positiveInteger(input?.id, "provider job id"),
33
+ name: text(input?.name),
34
+ runAttempt: positiveInteger(input?.runAttempt, "provider job runAttempt"),
35
+ status: text(input?.status).toLowerCase(),
36
+ conclusion: text(input?.conclusion).toLowerCase(),
37
+ completedAt: timestamp(input?.completedAt, "provider job completedAt"),
38
+ };
39
+ if (job.runAttempt !== runAttempt) {
40
+ throw new Error("legacy provider job run attempt mismatch");
41
+ }
42
+ if (job.status !== "completed" || !job.conclusion) {
43
+ throw new Error(
44
+ "legacy recovery requires every provider job to be terminal",
45
+ );
46
+ }
47
+ return job;
48
+ }
49
+
50
+ function normalizeEvidence(input) {
51
+ const runAttempt = positiveInteger(input?.runAttempt, "runAttempt");
52
+ if (!Array.isArray(input?.jobs) || input.jobs.length === 0) {
53
+ throw new Error("legacy terminal evidence requires provider jobs");
54
+ }
55
+ const jobs = input.jobs
56
+ .map((job) => normalizeProviderJob(job, runAttempt))
57
+ .sort((left, right) => left.id - right.id);
58
+ if (new Set(jobs.map((job) => job.id)).size !== jobs.length) {
59
+ throw new Error(
60
+ "legacy terminal evidence contains duplicate provider jobs",
61
+ );
62
+ }
63
+ if (!jobs.some((job) => job.conclusion === "failure")) {
64
+ throw new Error(
65
+ "legacy failed run evidence requires a failed provider job",
66
+ );
67
+ }
68
+ const runUpdatedAt = timestamp(input?.runUpdatedAt, "runUpdatedAt");
69
+ if (
70
+ jobs.some((job) => Date.parse(job.completedAt) > Date.parse(runUpdatedAt))
71
+ ) {
72
+ throw new Error("legacy provider job completion follows the run readback");
73
+ }
74
+ const body = {
75
+ schema: text(input?.schema),
76
+ candidateId: exactRoot(input?.candidateId, "candidateId"),
77
+ pullRequestNumber: positiveInteger(
78
+ input?.pullRequestNumber,
79
+ "pullRequestNumber",
80
+ ),
81
+ sourceHead: exactSha(input?.sourceHead, "sourceHead"),
82
+ sourceWorkflowRunId: positiveInteger(
83
+ input?.sourceWorkflowRunId,
84
+ "sourceWorkflowRunId",
85
+ ),
86
+ runAttempt,
87
+ runStatus: text(input?.runStatus).toLowerCase(),
88
+ runConclusion: text(input?.runConclusion).toLowerCase(),
89
+ runUpdatedAt,
90
+ totalJobCount: jobs.length,
91
+ nonterminalJobCount: 0,
92
+ workerTerminationProven: true,
93
+ jobs,
94
+ reason: text(input?.reason),
95
+ };
96
+ if (body.schema !== LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA) {
97
+ throw new Error(
98
+ `legacy terminal evidence must use ${LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA}`,
99
+ );
100
+ }
101
+ if (
102
+ body.runStatus !== "completed" ||
103
+ body.runConclusion !== "failure" ||
104
+ !body.reason
105
+ ) {
106
+ throw new Error(
107
+ "legacy terminal evidence requires one completed failed run with zero nonterminal jobs and proven worker termination",
108
+ );
109
+ }
110
+ const evidenceRoot = devDeliveryContentRoot(body);
111
+ if (exactRoot(input?.evidenceRoot, "evidenceRoot") !== evidenceRoot) {
112
+ throw new Error("legacy terminal evidence root mismatch");
113
+ }
114
+ return { ...body, evidenceRoot };
115
+ }
116
+
117
+ function assertExactCoverage(candidates, evidence) {
118
+ const expected = candidates.map((candidate) => candidate.candidateId).sort();
119
+ const observed = evidence.map((entry) => entry.candidateId).sort();
120
+ if (
121
+ expected.length !== observed.length ||
122
+ expected.some((candidateId, index) => candidateId !== observed[index])
123
+ ) {
124
+ throw new Error(
125
+ "legacy terminal recovery must cover every live legacy native candidate exactly once",
126
+ );
127
+ }
128
+ }
129
+
130
+ export function recoverLegacyTerminalDevDeliveryQueue(
131
+ queueInput,
132
+ requestInput,
133
+ { now = new Date().toISOString() } = {},
134
+ ) {
135
+ const currentTime = timestamp(now, "now");
136
+ const request = requestInput || {};
137
+ if (request.schema !== LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA) {
138
+ throw new Error(
139
+ `legacy terminal recovery must use ${LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA}`,
140
+ );
141
+ }
142
+ const before = normalizeDevDeliveryQueue(queueInput, {
143
+ allowLegacyV3Readback: true,
144
+ });
145
+ const expectedOldStateRoot = exactRoot(
146
+ request.expectedOldStateRoot,
147
+ "expectedOldStateRoot",
148
+ );
149
+ if (expectedOldStateRoot !== before.stateRoot) {
150
+ throw new Error("legacy terminal recovery expected-old state drift");
151
+ }
152
+ const legacyCandidates = before.candidates.filter(legacyLiveNativeCandidate);
153
+ if (legacyCandidates.length === 0) {
154
+ throw new Error("legacy terminal recovery found no live legacy candidate");
155
+ }
156
+ if (!Array.isArray(request.evidence)) {
157
+ throw new Error("legacy terminal recovery evidence must be an array");
158
+ }
159
+ const evidence = request.evidence.map(normalizeEvidence);
160
+ if (
161
+ new Set(evidence.map((entry) => entry.candidateId)).size !== evidence.length
162
+ ) {
163
+ throw new Error(
164
+ "legacy terminal recovery evidence contains duplicate candidates",
165
+ );
166
+ }
167
+ assertExactCoverage(legacyCandidates, evidence);
168
+
169
+ const queue = clone(before);
170
+ delete queue.stateRoot;
171
+ const transitions = [];
172
+ for (const entry of evidence) {
173
+ const candidate = queue.candidates.find(
174
+ (row) => row.candidateId === entry.candidateId,
175
+ );
176
+ if (
177
+ !candidate ||
178
+ candidate.pullRequestNumber !== entry.pullRequestNumber ||
179
+ candidate.sourceHead !== entry.sourceHead ||
180
+ candidate.sourceWorkflowRunId !== entry.sourceWorkflowRunId
181
+ ) {
182
+ throw new Error(
183
+ "legacy terminal evidence does not match candidate identity",
184
+ );
185
+ }
186
+ const active = queue.activeWarrant?.candidateId === candidate.candidateId;
187
+ if (!active && candidate.status !== "queued") {
188
+ throw new Error(
189
+ `legacy candidate status ${candidate.status} requires the exact active Warrant`,
190
+ );
191
+ }
192
+ const priorStatus = candidate.status;
193
+ candidate.status = "terminal-failure";
194
+ candidate.updatedAt = currentTime;
195
+ candidate.terminal = {
196
+ outcome: "terminal-failure",
197
+ reason: entry.reason,
198
+ evidenceRoot: entry.evidenceRoot,
199
+ authority: "legacy-hosted-native-terminal-recovery",
200
+ sourceWorkflowRunId: entry.sourceWorkflowRunId,
201
+ runAttempt: entry.runAttempt,
202
+ workerTerminationProven: true,
203
+ closedAt: currentTime,
204
+ ...(active
205
+ ? {
206
+ fencingToken: queue.activeWarrant.fencingToken,
207
+ leaseGeneration: queue.activeWarrant.generation,
208
+ }
209
+ : {}),
210
+ };
211
+ transitions.push({
212
+ candidateId: candidate.candidateId,
213
+ pullRequestNumber: candidate.pullRequestNumber,
214
+ sourceHead: candidate.sourceHead,
215
+ priorStatus,
216
+ activeWarrant: active,
217
+ outcome: "terminal-failure",
218
+ evidenceRoot: entry.evidenceRoot,
219
+ });
220
+ }
221
+ queue.activeWarrant = null;
222
+ queue.generation += 1;
223
+ queue.updatedAt = currentTime;
224
+ queue.stateRoot = devDeliveryContentRoot(queue);
225
+ const after = normalizeDevDeliveryQueue(queue);
226
+ const requestBody = {
227
+ schema: LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA,
228
+ expectedOldStateRoot,
229
+ evidence,
230
+ };
231
+ const requestRoot = devDeliveryContentRoot(requestBody);
232
+ const receipt = {
233
+ schema: LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA,
234
+ action: "legacy-terminal-recovery",
235
+ expectedOldStateRoot,
236
+ nextStateRoot: after.stateRoot,
237
+ requestRoot,
238
+ transitions,
239
+ nextAction: "Select the next strictly valid queued candidate, if any.",
240
+ };
241
+ return {
242
+ queue: after,
243
+ receipt,
244
+ receiptRoot: devDeliveryContentRoot(receipt),
245
+ };
246
+ }
@@ -12,6 +12,8 @@ import {
12
12
  import {
13
13
  chainedDevDeliveryAttemptInput,
14
14
  createDevDeliveryCandidateIdentity,
15
+ EXACT_DEV_DELIVERY_PROOF_FIELDS,
16
+ matchesExactDevDeliveryCandidate,
15
17
  validateDevDeliveryCandidateChain,
16
18
  } from "./dev-delivery-candidate-identity.js";
17
19
  import {
@@ -380,6 +382,8 @@ export function transitionDevDeliveryQueue(queueInput, mutate, nowInput) {
380
382
  delete queue.stateRoot;
381
383
  const now = timestamp(nowInput, "now");
382
384
  const result = mutate(queue, before, now);
385
+ if (result.preserveState)
386
+ return { before, after: before, expectedOldStateRoot, result };
383
387
  queue.generation += 1;
384
388
  queue.updatedAt = now;
385
389
  const after = withQueueRoot(queue);
@@ -478,34 +482,14 @@ export function submitDevDeliveryCandidate(
478
482
  `candidate ${attemptedCandidate.candidateId} is terminal and cannot be resubmitted`,
479
483
  );
480
484
  }
481
- const exactProofFields = [
482
- "sourcePatchRoot",
483
- "sourceProofRoot",
484
- "planRoot",
485
- "closureRoot",
486
- "dependencyRoot",
487
- "toolchainRoot",
488
- "environmentRoot",
489
- "sourceWorkflowRunId",
490
- ];
491
- const exactProofMatches =
492
- exactProofFields.every(
493
- (field) => existing[field] === attemptedCandidate[field],
494
- ) &&
495
- JSON.stringify(existing.affectedPaths || []) ===
496
- JSON.stringify(attemptedCandidate.affectedPaths || []) &&
497
- JSON.stringify(existing.shardEvidenceRoots || []) ===
498
- JSON.stringify(attemptedCandidate.shardEvidenceRoots || []) &&
499
- existing.nativeCommandContract?.commandRoot ===
500
- attemptedCandidate.nativeCommandContract?.commandRoot &&
501
- existing.releaseBlockerPriority?.claimRoot ===
502
- attemptedCandidate.releaseBlockerPriority?.claimRoot;
503
- if (
504
- existing.sourceHead === attemptedCandidate.sourceHead &&
505
- exactProofMatches
506
- ) {
507
- action = "duplicate-noop";
485
+ if (matchesExactDevDeliveryCandidate(existing, attemptedCandidate)) {
486
+ action =
487
+ before.activeWarrant?.candidateId === existing.candidateId
488
+ ? "active-warrant-noop"
489
+ : "duplicate-noop";
508
490
  selected = existing;
491
+ if (action === "active-warrant-noop")
492
+ return { candidate: selected, action, preserveState: true };
509
493
  } else {
510
494
  if (before.activeWarrant?.candidateId === existing.candidateId) {
511
495
  throw new Error(
@@ -515,7 +499,7 @@ export function submitDevDeliveryCandidate(
515
499
  const headChanged =
516
500
  existing.sourceHead !== attemptedCandidate.sourceHead;
517
501
  existing.sourceHead = attemptedCandidate.sourceHead;
518
- for (const field of exactProofFields)
502
+ for (const field of EXACT_DEV_DELIVERY_PROOF_FIELDS)
519
503
  existing[field] = attemptedCandidate[field];
520
504
  if (attemptedCandidate.nativeCommandContract)
521
505
  existing.nativeCommandContract =
@@ -61,6 +61,14 @@ const ROOT_DOMAINS = new Set([
61
61
  "tail-reseal-artifact-files",
62
62
  "tail-reseal-receipt",
63
63
  "release-candidate-passport",
64
+ "release-invocation-publisher",
65
+ "release-invocation-runtime",
66
+ "release-invocation-candidate",
67
+ "release-invocation-target",
68
+ "release-invocation-authority",
69
+ "release-invocation",
70
+ "release-transaction",
71
+ "release-receipt",
64
72
  ]);
65
73
  const FAULT_CLASSES = new Set([
66
74
  "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,356 @@
1
+ import {
2
+ V4ContractFault,
3
+ v4CanonicalBytes,
4
+ v4ContentRoot,
5
+ validateV4Root,
6
+ } from "./v4-canonical-contracts.js";
7
+
8
+ export const V4_RELEASE_INVOCATION_CONTRACT =
9
+ "kungfu-buildchain-v4-release-invocation/v1";
10
+ export const V4_RELEASE_INVOCATION_ADAPTER_CONTRACT =
11
+ "kungfu-buildchain-v4-release-invocation-adapter/v1";
12
+ export const V4_RELEASE_TRANSACTION_CONTRACT =
13
+ "kungfu-buildchain-v4-release-transaction/v1";
14
+ export const V4_RELEASE_RECEIPT_CONTRACT =
15
+ "kungfu-buildchain-v4-release-receipt/v1";
16
+
17
+ const SHA_PATTERN = /^[0-9a-f]{40}$/u;
18
+ const TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
19
+ const ROUTE_SURFACES = new Set([
20
+ "alpha",
21
+ "stable",
22
+ "public",
23
+ "private",
24
+ "declarative",
25
+ "legacy-compatible",
26
+ ]);
27
+ const EXECUTION_MODES = new Set(["fresh", "resume"]);
28
+ const COMPARISON_STATES = new Set(["identical", "ahead", "behind", "diverged"]);
29
+
30
+ function fault(code, path, message) {
31
+ throw new V4ContractFault(code, path, message);
32
+ }
33
+
34
+ function exactKeys(value, keys, path) {
35
+ if (!value || typeof value !== "object" || Array.isArray(value))
36
+ fault("invalid-release-invocation", path, `${path} must be an object`);
37
+ const actual = Object.keys(value).sort();
38
+ const expected = [...keys].sort();
39
+ if (
40
+ actual.length !== expected.length ||
41
+ actual.some((key, index) => key !== expected[index])
42
+ )
43
+ fault(
44
+ "invalid-release-invocation-shape",
45
+ path,
46
+ `${path} keys are not canonical`,
47
+ );
48
+ }
49
+
50
+ function sha(value, path, nullable = false) {
51
+ if (nullable && value === null) return value;
52
+ if (typeof value !== "string" || !SHA_PATTERN.test(value))
53
+ fault("invalid-release-sha", path, `${path} must be an exact Git SHA`);
54
+ return value;
55
+ }
56
+
57
+ function text(value, path) {
58
+ if (
59
+ typeof value !== "string" ||
60
+ value.length === 0 ||
61
+ /[^\x20-\x7e]/u.test(value)
62
+ )
63
+ fault("invalid-release-text", path, `${path} must be printable ASCII`);
64
+ return value;
65
+ }
66
+
67
+ function validatePublisher(value) {
68
+ exactKeys(
69
+ value,
70
+ ["repository", "workflow", "workflowSha", "job"],
71
+ "$/publisher",
72
+ );
73
+ if (
74
+ value.repository !== "kungfu-systems/buildchain" ||
75
+ value.workflow !== ".github/workflows/.release-candidate-promote.yml" ||
76
+ value.job !== "apply"
77
+ )
78
+ fault(
79
+ "invalid-publisher-identity",
80
+ "$/publisher",
81
+ "publisher identity is not the canonical v4 APPLY job",
82
+ );
83
+ sha(value.workflowSha, "$/publisher/workflowSha");
84
+ }
85
+
86
+ function validateRuntime(value) {
87
+ exactKeys(value, ["repository", "commit", "tree"], "$/runtime");
88
+ if (value.repository !== "kungfu-systems/buildchain")
89
+ fault(
90
+ "invalid-runtime-identity",
91
+ "$/runtime/repository",
92
+ "runtime repository is not canonical",
93
+ );
94
+ sha(value.commit, "$/runtime/commit");
95
+ sha(value.tree, "$/runtime/tree");
96
+ }
97
+
98
+ function validateCandidate(value) {
99
+ exactKeys(value, ["repository", "commit", "tree", "version"], "$/candidate");
100
+ text(value.repository, "$/candidate/repository");
101
+ sha(value.commit, "$/candidate/commit");
102
+ sha(value.tree, "$/candidate/tree");
103
+ text(value.version, "$/candidate/version");
104
+ }
105
+
106
+ function validateTarget(value) {
107
+ exactKeys(value, ["channel", "tag", "expectedOldSha"], "$/target");
108
+ if (
109
+ !["alpha", "stable"].includes(value.channel) ||
110
+ !TAG_PATTERN.test(value.tag)
111
+ )
112
+ fault(
113
+ "invalid-release-target",
114
+ "$/target",
115
+ "release channel and exact tag are not canonical",
116
+ );
117
+ sha(value.expectedOldSha, "$/target/expectedOldSha", true);
118
+ }
119
+
120
+ function validateAuthority(value) {
121
+ exactKeys(
122
+ value,
123
+ ["policyRoot", "qualificationRoot", "warrantRoot"],
124
+ "$/authority",
125
+ );
126
+ for (const name of ["policyRoot", "qualificationRoot", "warrantRoot"])
127
+ validateV4Root(value[name], `$/authority/${name}`);
128
+ }
129
+
130
+ export function createV4ReleaseInvocation(value) {
131
+ exactKeys(
132
+ value,
133
+ ["schema", "publisher", "runtime", "candidate", "target", "authority"],
134
+ "$",
135
+ );
136
+ if (value.schema !== V4_RELEASE_INVOCATION_CONTRACT)
137
+ fault(
138
+ "invalid-release-invocation",
139
+ "$/schema",
140
+ "unsupported invocation schema",
141
+ );
142
+ validatePublisher(value.publisher);
143
+ validateRuntime(value.runtime);
144
+ validateCandidate(value.candidate);
145
+ validateTarget(value.target);
146
+ validateAuthority(value.authority);
147
+ v4CanonicalBytes(value);
148
+ const roots = {
149
+ publisherRoot: v4ContentRoot(
150
+ "release-invocation-publisher",
151
+ value.publisher,
152
+ ),
153
+ runtimeRoot: v4ContentRoot("release-invocation-runtime", value.runtime),
154
+ candidateRoot: v4ContentRoot(
155
+ "release-invocation-candidate",
156
+ value.candidate,
157
+ ),
158
+ targetRoot: v4ContentRoot("release-invocation-target", value.target),
159
+ authorityRoot: v4ContentRoot(
160
+ "release-invocation-authority",
161
+ value.authority,
162
+ ),
163
+ };
164
+ const invocationRoot = v4ContentRoot("release-invocation", {
165
+ schema: V4_RELEASE_INVOCATION_CONTRACT,
166
+ ...roots,
167
+ });
168
+ return { invocation: value, roots: { ...roots, invocationRoot } };
169
+ }
170
+
171
+ export function adaptV4ReleaseInvocation(value) {
172
+ exactKeys(value, ["schema", "route", "invocation"], "$adapter");
173
+ if (value.schema !== V4_RELEASE_INVOCATION_ADAPTER_CONTRACT)
174
+ fault(
175
+ "invalid-release-adapter",
176
+ "$adapter/schema",
177
+ "unsupported adapter schema",
178
+ );
179
+ exactKeys(value.route, ["surface", "execution"], "$adapter/route");
180
+ if (!ROUTE_SURFACES.has(value.route.surface))
181
+ fault(
182
+ "invalid-release-adapter",
183
+ "$adapter/route/surface",
184
+ "unsupported route surface",
185
+ );
186
+ if (!EXECUTION_MODES.has(value.route.execution))
187
+ fault(
188
+ "invalid-release-adapter",
189
+ "$adapter/route/execution",
190
+ "unsupported execution mode",
191
+ );
192
+ return createV4ReleaseInvocation(value.invocation);
193
+ }
194
+
195
+ export function planV4ReleaseRoute({
196
+ requestedSha,
197
+ observedSha,
198
+ comparisonStatus,
199
+ requestedChannel = "",
200
+ targetRef,
201
+ dryRun = false,
202
+ resume = false,
203
+ }) {
204
+ sha(requestedSha, "$route/requestedSha");
205
+ sha(observedSha, "$route/observedSha");
206
+ if (!COMPARISON_STATES.has(comparisonStatus))
207
+ fault(
208
+ "invalid-release-route",
209
+ "$route/comparisonStatus",
210
+ "unsupported source comparison state",
211
+ );
212
+ text(targetRef, "$route/targetRef");
213
+ const alphaLane = /^alpha\/v[0-9]+\/v[0-9]+\.[0-9]+$/u.test(targetRef);
214
+ const stableLane =
215
+ /^release\/v[0-9]+\/v[0-9]+\.[0-9]+$/u.test(targetRef) ||
216
+ ["publish-gate/major", "major-gate"].includes(targetRef);
217
+ if (!alphaLane && !stableLane)
218
+ fault(
219
+ "invalid-release-route",
220
+ "$route/targetRef",
221
+ "source lane is not a supported v4 release lane",
222
+ );
223
+ const derivedChannel = alphaLane ? "alpha" : "stable";
224
+ const normalizedRequested =
225
+ requestedChannel === "alpha"
226
+ ? "alpha"
227
+ : ["release", "stable", "major"].includes(requestedChannel)
228
+ ? "stable"
229
+ : requestedChannel === ""
230
+ ? derivedChannel
231
+ : "";
232
+ if (!normalizedRequested || normalizedRequested !== derivedChannel)
233
+ fault(
234
+ "invalid-release-route",
235
+ "$route/channel",
236
+ "requested channel does not match the source lane",
237
+ );
238
+ let decision = resume ? "Resume" : "Fresh";
239
+ let reason = resume ? "resume" : "fresh";
240
+ if (comparisonStatus === "ahead" && !resume && !dryRun) {
241
+ decision = "NoOp";
242
+ reason = "source-advanced";
243
+ } else if (
244
+ requestedSha !== observedSha &&
245
+ !(resume && comparisonStatus === "ahead") &&
246
+ !(dryRun && comparisonStatus === "ahead")
247
+ ) {
248
+ decision = "Blocked";
249
+ reason = `source-${comparisonStatus}`;
250
+ }
251
+ return Object.freeze({
252
+ decision,
253
+ reason,
254
+ channel: normalizedRequested,
255
+ targetRef,
256
+ requestedSha,
257
+ observedSha,
258
+ });
259
+ }
260
+
261
+ export function createV4ReleaseTransaction(value) {
262
+ exactKeys(
263
+ value,
264
+ ["invocationRoot", "publisherRoot", "runtimeRoot"],
265
+ "$transaction",
266
+ );
267
+ for (const name of ["invocationRoot", "publisherRoot", "runtimeRoot"])
268
+ validateV4Root(value[name], `$transaction/${name}`);
269
+ const transaction = {
270
+ schema: V4_RELEASE_TRANSACTION_CONTRACT,
271
+ invocationRoot: value.invocationRoot,
272
+ publisherRoot: value.publisherRoot,
273
+ runtimeRoot: value.runtimeRoot,
274
+ phases: ["QUALIFY", "APPLY", "SETTLE"],
275
+ writer: "canonical-v4-apply",
276
+ };
277
+ return {
278
+ transaction,
279
+ transactionRoot: v4ContentRoot("release-transaction", transaction),
280
+ };
281
+ }
282
+
283
+ export function createV4ReleaseReceipt(value) {
284
+ exactKeys(
285
+ value,
286
+ [
287
+ "schema",
288
+ "transactionRoot",
289
+ "outcome",
290
+ "releasePassportRoot",
291
+ "providerTransactionRoot",
292
+ "providerStateRoot",
293
+ "providerReceiptRoots",
294
+ ],
295
+ "$receipt",
296
+ );
297
+ if (value.schema !== V4_RELEASE_RECEIPT_CONTRACT)
298
+ fault(
299
+ "invalid-release-receipt",
300
+ "$receipt/schema",
301
+ "unsupported receipt schema",
302
+ );
303
+ validateV4Root(value.transactionRoot, "$receipt/transactionRoot");
304
+ if (!["complete", "blocked"].includes(value.outcome))
305
+ fault(
306
+ "invalid-release-receipt",
307
+ "$receipt/outcome",
308
+ "unsupported receipt outcome",
309
+ );
310
+ for (const name of [
311
+ "releasePassportRoot",
312
+ "providerTransactionRoot",
313
+ "providerStateRoot",
314
+ ]) {
315
+ if (value[name] !== null) validateV4Root(value[name], `$receipt/${name}`);
316
+ }
317
+ if (!Array.isArray(value.providerReceiptRoots))
318
+ fault(
319
+ "invalid-release-receipt",
320
+ "$receipt/providerReceiptRoots",
321
+ "receipt roots must be an array",
322
+ );
323
+ value.providerReceiptRoots.forEach((root, index) =>
324
+ validateV4Root(root, `$receipt/providerReceiptRoots/${index}`),
325
+ );
326
+ const canonicalRoots = [...new Set(value.providerReceiptRoots)].sort();
327
+ if (
328
+ canonicalRoots.length !== value.providerReceiptRoots.length ||
329
+ canonicalRoots.some(
330
+ (root, index) => root !== value.providerReceiptRoots[index],
331
+ )
332
+ )
333
+ fault(
334
+ "invalid-release-receipt",
335
+ "$receipt/providerReceiptRoots",
336
+ "provider receipt roots must be sorted and unique",
337
+ );
338
+ if (
339
+ value.outcome === "complete" &&
340
+ [
341
+ value.releasePassportRoot,
342
+ value.providerTransactionRoot,
343
+ value.providerStateRoot,
344
+ ].some((root) => root === null)
345
+ )
346
+ fault(
347
+ "invalid-release-receipt",
348
+ "$receipt/outcome",
349
+ "complete receipt requires every terminal root",
350
+ );
351
+ v4CanonicalBytes(value);
352
+ return {
353
+ receipt: value,
354
+ receiptRoot: v4ContentRoot("release-receipt", value),
355
+ };
356
+ }