@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +4 -2
  2. package/actions/release-tail/README.md +17 -0
  3. package/bin/buildchain.mjs +11 -0
  4. package/bin/internal/command-registry.mjs +2 -0
  5. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  6. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  7. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  8. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  9. package/dist/site/agent-index.json +3 -0
  10. package/dist/site/artifact-schemas.json +6 -0
  11. package/dist/site/buildchain-contract.json +35 -20
  12. package/dist/site/buildchain-site.json +307 -17
  13. package/dist/site/capability-registry.json +12 -9
  14. package/dist/site/cli-registry.json +102 -0
  15. package/dist/site/controller-registry.json +14 -2
  16. package/dist/site/kfd-claims.json +247 -16
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +48 -3
  19. package/dist/site/node-api-registry.json +3108 -1409
  20. package/dist/site/page-registry.json +275 -9
  21. package/dist/site/public-surface-audit.json +304 -15
  22. package/dist/site/publication-authority-registry.json +26 -1
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +5 -0
  25. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  26. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  27. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  28. package/dist/site/site-manifest.json +31 -7
  29. package/dist/site/workflow-registry.json +86 -9
  30. package/docs/MAP.md +5 -2
  31. package/docs/cli-reference.md +136 -0
  32. package/docs/node-api-reference.md +233 -84
  33. package/docs/publication-rehearsal.md +94 -0
  34. package/docs/release-tail-contract.md +160 -0
  35. package/docs/release-tail-provider-plane.md +120 -0
  36. package/package.json +8 -3
  37. package/packages/core/buildchain-agent-manuals.js +3 -0
  38. package/packages/core/buildchain-kfd-claims.js +1 -1
  39. package/packages/core/buildchain-publication-authority.js +1 -0
  40. package/packages/core/index.js +39 -0
  41. package/packages/core/paper-agent-entry.js +11 -5
  42. package/packages/core/paper-repository.js +1 -0
  43. package/packages/core/paper-scaffold-content.js +21 -0
  44. package/packages/core/paper.js +28 -2
  45. package/packages/core/publication-rehearsal-projection.js +173 -0
  46. package/packages/core/publication-rehearsal-runtime.js +909 -0
  47. package/packages/core/release-tail-compatibility.js +60 -0
  48. package/packages/core/release-tail-provider-adapters.js +461 -0
  49. package/packages/core/release-tail-provider-plane.js +1228 -0
  50. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  51. package/scripts/assemble-self-publication-admission.mjs +1 -1
  52. package/scripts/buildchain-cli-help.mjs +13 -0
  53. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  54. package/scripts/check-inventory.mjs +8 -8
  55. package/scripts/check-maintainability.mjs +9 -2
  56. package/scripts/check-release-tail-contract.mjs +435 -0
  57. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  58. package/scripts/generate-site-bundle.mjs +24 -0
  59. package/scripts/init-repo.mjs +26 -2
  60. package/scripts/materialize-self-release-candidate-version.mjs +131 -0
  61. package/scripts/release-tail.mjs +159 -0
  62. package/scripts/site-capability-metadata.mjs +11 -0
  63. package/scripts/v4-architecture.mjs +600 -0
  64. package/scripts/workflow-call-contract.mjs +184 -5
@@ -0,0 +1,1228 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const RELEASE_TAIL_DECLARATION_CONTRACT =
6
+ "kungfu-buildchain-release-tail-capabilities";
7
+ export const RELEASE_TAIL_TRANSACTION_POLICY = "buildchain.release-tail/v1";
8
+ export const RELEASE_TAIL_TRANSACTION_SCHEMA =
9
+ "kungfu.buildchain.release-tail.transaction/v1";
10
+ export const RELEASE_TAIL_EFFECT_SCHEMA =
11
+ "kungfu.buildchain.release-tail.effect/v1";
12
+ export const RELEASE_TAIL_OBSERVATION_SCHEMA =
13
+ "kungfu.buildchain.release-tail.observation/v1";
14
+ export const RELEASE_TAIL_RECEIPT_SCHEMA =
15
+ "kungfu.buildchain.release-tail.receipt/v1";
16
+
17
+ export const RELEASE_TAIL_STATES = Object.freeze([
18
+ "preparing",
19
+ "prepared",
20
+ "publishing",
21
+ "committing",
22
+ "activating",
23
+ "reading-back",
24
+ "settling",
25
+ "complete",
26
+ "blocked",
27
+ "repair-required",
28
+ "terminal-failure",
29
+ ]);
30
+
31
+ export const RELEASE_TAIL_CAPABILITY_REGISTRY = Object.freeze([
32
+ Object.freeze({
33
+ id: "artifact.publish",
34
+ executor: "provider-adapter",
35
+ adapter: "github-release-assets",
36
+ effectKind: "artifact-publication",
37
+ observationKind: "artifact-publication-readback",
38
+ receiptKind: "artifact-publication",
39
+ transactionState: "publishing",
40
+ }),
41
+ Object.freeze({
42
+ id: "signed-channel.commit",
43
+ executor: "provider-adapter",
44
+ adapter: "signed-static-channel",
45
+ effectKind: "signed-channel-commit",
46
+ observationKind: "signed-channel-readback",
47
+ receiptKind: "publication-commit",
48
+ transactionState: "committing",
49
+ }),
50
+ Object.freeze({
51
+ id: "release.activate",
52
+ executor: "provider-adapter",
53
+ adapter: "site-release-activation",
54
+ effectKind: "release-activation",
55
+ observationKind: "production-readback",
56
+ receiptKind: "activation-receipt-set",
57
+ transactionState: "activating",
58
+ }),
59
+ Object.freeze({
60
+ id: "released-evidence.synthesize",
61
+ executor: "buildchain-core",
62
+ adapter: "activation-receipt-projector",
63
+ effectKind: "released-evidence-projection",
64
+ observationKind: "released-evidence-validation",
65
+ receiptKind: "released-evidence",
66
+ transactionState: "settling",
67
+ }),
68
+ ]);
69
+
70
+ const CAPABILITY_BY_ID = new Map(
71
+ RELEASE_TAIL_CAPABILITY_REGISTRY.map((entry) => [entry.id, entry]),
72
+ );
73
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
74
+ const SHA = /^[0-9a-f]{40}$/u;
75
+ const FORBIDDEN_KEYS = new Set([
76
+ "cmd",
77
+ "command",
78
+ "eval",
79
+ "executable",
80
+ "javascript",
81
+ "run",
82
+ "script",
83
+ "shell",
84
+ ]);
85
+ const TOP_LEVEL_FIELDS = [
86
+ "contract",
87
+ "schemaVersion",
88
+ "transactionPolicy",
89
+ "subject",
90
+ "capabilities",
91
+ ];
92
+ const SUBJECT_FIELDS = ["repository", "sourceSha", "version", "tag", "channel"];
93
+ const CAPABILITY_FIELDS = [
94
+ "id",
95
+ "executor",
96
+ "adapter",
97
+ "artifactRoles",
98
+ "destination",
99
+ "channelPolicy",
100
+ "activationPolicy",
101
+ "readbackPredicates",
102
+ "effect",
103
+ "observation",
104
+ "receipt",
105
+ "operationIdentity",
106
+ "idempotency",
107
+ "retry",
108
+ "evidenceRequirements",
109
+ ];
110
+ const EFFECT_PLAN_FIELDS = [
111
+ "schema",
112
+ "transactionPolicy",
113
+ "transactionRoot",
114
+ "declarationRoot",
115
+ "subject",
116
+ "operationOrder",
117
+ "effects",
118
+ "planRoot",
119
+ ];
120
+ const EFFECT_FIELDS = [
121
+ "schema",
122
+ "kind",
123
+ "transactionRoot",
124
+ "operationId",
125
+ "capabilityId",
126
+ "executor",
127
+ "adapter",
128
+ "subjectRoot",
129
+ "targetRoot",
130
+ "attemptKey",
131
+ "subject",
132
+ "artifactRoles",
133
+ "destination",
134
+ "channelPolicy",
135
+ "activationPolicy",
136
+ "readbackPredicates",
137
+ "idempotency",
138
+ "retry",
139
+ "evidenceRequirements",
140
+ "effectRoot",
141
+ ];
142
+ const TERMINAL_STATES = new Set([
143
+ "complete",
144
+ "blocked",
145
+ "repair-required",
146
+ "terminal-failure",
147
+ ]);
148
+
149
+ export function releaseTailStableJson(value) {
150
+ if (Array.isArray(value)) {
151
+ return `[${value.map(releaseTailStableJson).join(",")}]`;
152
+ }
153
+ if (value && typeof value === "object") {
154
+ return `{${Object.keys(value)
155
+ .sort()
156
+ .map(
157
+ (key) => `${JSON.stringify(key)}:${releaseTailStableJson(value[key])}`,
158
+ )
159
+ .join(",")}}`;
160
+ }
161
+ return JSON.stringify(value);
162
+ }
163
+
164
+ export function releaseTailRoot(value) {
165
+ return `sha256:${crypto
166
+ .createHash("sha256")
167
+ .update(releaseTailStableJson(value))
168
+ .digest("hex")}`;
169
+ }
170
+
171
+ function fail(message) {
172
+ throw new Error(`invalid release-tail declaration: ${message}`);
173
+ }
174
+
175
+ function plainObject(value, label) {
176
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
177
+ fail(`${label} must be an object`);
178
+ }
179
+ return value;
180
+ }
181
+
182
+ function exactFields(value, fields, label) {
183
+ plainObject(value, label);
184
+ const actual = Object.keys(value).sort();
185
+ const expected = [...fields].sort();
186
+ if (actual.join("\n") !== expected.join("\n")) {
187
+ fail(`${label} fields must be exactly: ${fields.join(", ")}`);
188
+ }
189
+ }
190
+
191
+ function nonEmpty(value, label) {
192
+ if (typeof value !== "string" || value.trim() === "") {
193
+ fail(`${label} must be a non-empty string`);
194
+ }
195
+ return value;
196
+ }
197
+
198
+ function exactRoot(value, label) {
199
+ const normalized = nonEmpty(value, label).toLowerCase();
200
+ if (!ROOT.test(normalized)) fail(`${label} must be a sha256 content root`);
201
+ return normalized;
202
+ }
203
+
204
+ function oneOf(value, choices, label) {
205
+ if (!choices.includes(value)) {
206
+ fail(`${label} must be one of ${choices.join(", ")}`);
207
+ }
208
+ return value;
209
+ }
210
+
211
+ function rejectExecutableKeys(value, pointer = "$") {
212
+ if (Array.isArray(value)) {
213
+ value.forEach((entry, index) =>
214
+ rejectExecutableKeys(entry, `${pointer}/${index}`),
215
+ );
216
+ return;
217
+ }
218
+ if (!value || typeof value !== "object") return;
219
+ for (const [key, entry] of Object.entries(value)) {
220
+ if (FORBIDDEN_KEYS.has(key.toLowerCase())) {
221
+ fail(`executable key '${key}' is forbidden at ${pointer}`);
222
+ }
223
+ rejectExecutableKeys(entry, `${pointer}/${key}`);
224
+ }
225
+ }
226
+
227
+ function normalizeMessage(value, expectedSchema, expectedKind, label) {
228
+ exactFields(value, ["kind", "schema"], label);
229
+ if (value.schema !== expectedSchema) fail(`${label}.schema is not supported`);
230
+ if (value.kind !== expectedKind)
231
+ fail(`${label}.kind must be ${expectedKind}`);
232
+ return { kind: value.kind, schema: value.schema };
233
+ }
234
+
235
+ function normalizeCapability(value, subject) {
236
+ exactFields(
237
+ value,
238
+ CAPABILITY_FIELDS,
239
+ `capability ${value?.id || "<unknown>"}`,
240
+ );
241
+ const registry = CAPABILITY_BY_ID.get(value.id);
242
+ if (!registry) fail(`unsupported capability id: ${value.id || "<empty>"}`);
243
+ if (value.executor !== registry.executor) {
244
+ fail(`${value.id}.executor must be ${registry.executor}`);
245
+ }
246
+ if (value.adapter !== registry.adapter) {
247
+ fail(`${value.id}.adapter must be ${registry.adapter}`);
248
+ }
249
+ if (!Array.isArray(value.artifactRoles)) {
250
+ fail(`${value.id}.artifactRoles must be an array`);
251
+ }
252
+ const artifactRoles = value.artifactRoles.map((entry, index) => {
253
+ exactFields(entry, ["role", "root"], `${value.id}.artifactRoles[${index}]`);
254
+ return {
255
+ role: nonEmpty(entry.role, `${value.id}.artifactRoles[${index}].role`),
256
+ root: exactRoot(entry.root, `${value.id}.artifactRoles[${index}].root`),
257
+ };
258
+ });
259
+ if (
260
+ new Set(artifactRoles.map((entry) => entry.role)).size !==
261
+ artifactRoles.length
262
+ ) {
263
+ fail(`${value.id}.artifactRoles contains duplicate roles`);
264
+ }
265
+
266
+ exactFields(
267
+ value.destination,
268
+ ["kind", "locator"],
269
+ `${value.id}.destination`,
270
+ );
271
+ const destination = {
272
+ kind: nonEmpty(value.destination.kind, `${value.id}.destination.kind`),
273
+ locator: nonEmpty(
274
+ value.destination.locator,
275
+ `${value.id}.destination.locator`,
276
+ ),
277
+ };
278
+
279
+ exactFields(
280
+ value.channelPolicy,
281
+ ["channel", "tagPattern", "authorityMove"],
282
+ `${value.id}.channelPolicy`,
283
+ );
284
+ const channel = oneOf(
285
+ value.channelPolicy.channel,
286
+ ["alpha", "release", "stable"],
287
+ `${value.id}.channelPolicy.channel`,
288
+ );
289
+ if (channel !== subject.channel)
290
+ fail(`${value.id} channel does not match subject`);
291
+ const tagPattern = nonEmpty(
292
+ value.channelPolicy.tagPattern,
293
+ `${value.id}.channelPolicy.tagPattern`,
294
+ );
295
+ let tagExpression;
296
+ try {
297
+ tagExpression = new RegExp(tagPattern, "u");
298
+ } catch {
299
+ fail(`${value.id}.channelPolicy.tagPattern is not a valid expression`);
300
+ }
301
+ if (!tagExpression.test(subject.tag)) {
302
+ fail(`${value.id}.channelPolicy.tagPattern does not match subject.tag`);
303
+ }
304
+ const channelPolicy = {
305
+ channel,
306
+ tagPattern,
307
+ authorityMove: oneOf(
308
+ value.channelPolicy.authorityMove,
309
+ ["none", "signed-cas", "verified-ref"],
310
+ `${value.id}.channelPolicy.authorityMove`,
311
+ ),
312
+ };
313
+
314
+ exactFields(
315
+ value.activationPolicy,
316
+ ["mode", "environment"],
317
+ `${value.id}.activationPolicy`,
318
+ );
319
+ const activationPolicy = {
320
+ mode: oneOf(
321
+ value.activationPolicy.mode,
322
+ ["none", "receipt-set", "receipt-only"],
323
+ `${value.id}.activationPolicy.mode`,
324
+ ),
325
+ environment: oneOf(
326
+ value.activationPolicy.environment,
327
+ ["none", "shadow", "production"],
328
+ `${value.id}.activationPolicy.environment`,
329
+ ),
330
+ };
331
+
332
+ if (
333
+ !Array.isArray(value.readbackPredicates) ||
334
+ value.readbackPredicates.length === 0
335
+ ) {
336
+ fail(`${value.id} must declare at least one readback predicate`);
337
+ }
338
+ const readbackPredicates = value.readbackPredicates.map((entry, index) => {
339
+ exactFields(
340
+ entry,
341
+ ["id", "kind", "expected"],
342
+ `${value.id}.readbackPredicates[${index}]`,
343
+ );
344
+ return {
345
+ id: nonEmpty(entry.id, `${value.id}.readbackPredicates[${index}].id`),
346
+ kind: nonEmpty(
347
+ entry.kind,
348
+ `${value.id}.readbackPredicates[${index}].kind`,
349
+ ),
350
+ expected: nonEmpty(
351
+ entry.expected,
352
+ `${value.id}.readbackPredicates[${index}].expected`,
353
+ ),
354
+ };
355
+ });
356
+ if (
357
+ new Set(readbackPredicates.map((entry) => entry.id)).size !==
358
+ readbackPredicates.length
359
+ ) {
360
+ fail(`${value.id}.readbackPredicates contains duplicate ids`);
361
+ }
362
+
363
+ exactFields(
364
+ value.operationIdentity,
365
+ [
366
+ "transactionRoot",
367
+ "capabilityId",
368
+ "subjectRoot",
369
+ "targetRoot",
370
+ "attemptKey",
371
+ ],
372
+ `${value.id}.operationIdentity`,
373
+ );
374
+ if (value.operationIdentity.capabilityId !== value.id) {
375
+ fail(
376
+ `${value.id}.operationIdentity.capabilityId must match the capability id`,
377
+ );
378
+ }
379
+ const operationIdentity = {
380
+ transactionRoot: exactRoot(
381
+ value.operationIdentity.transactionRoot,
382
+ `${value.id}.operationIdentity.transactionRoot`,
383
+ ),
384
+ capabilityId: value.id,
385
+ subjectRoot: exactRoot(
386
+ value.operationIdentity.subjectRoot,
387
+ `${value.id}.operationIdentity.subjectRoot`,
388
+ ),
389
+ targetRoot: exactRoot(
390
+ value.operationIdentity.targetRoot,
391
+ `${value.id}.operationIdentity.targetRoot`,
392
+ ),
393
+ attemptKey: nonEmpty(
394
+ value.operationIdentity.attemptKey,
395
+ `${value.id}.operationIdentity.attemptKey`,
396
+ ),
397
+ };
398
+
399
+ exactFields(
400
+ value.idempotency,
401
+ ["scope", "duplicate"],
402
+ `${value.id}.idempotency`,
403
+ );
404
+ const idempotency = {
405
+ scope: oneOf(
406
+ value.idempotency.scope,
407
+ ["operation", "subject-target"],
408
+ `${value.id}.idempotency.scope`,
409
+ ),
410
+ duplicate: oneOf(
411
+ value.idempotency.duplicate,
412
+ ["return-observed-receipt", "readback-before-retry"],
413
+ `${value.id}.idempotency.duplicate`,
414
+ ),
415
+ };
416
+
417
+ exactFields(
418
+ value.retry,
419
+ ["class", "localAttempts", "exhausted"],
420
+ `${value.id}.retry`,
421
+ );
422
+ const localAttempts = value.retry.localAttempts;
423
+ if (
424
+ !Number.isSafeInteger(localAttempts) ||
425
+ localAttempts < 0 ||
426
+ localAttempts > 3
427
+ ) {
428
+ fail(`${value.id}.retry.localAttempts must be between zero and three`);
429
+ }
430
+ const retry = {
431
+ class: oneOf(
432
+ value.retry.class,
433
+ ["never", "readback", "provider-transient"],
434
+ `${value.id}.retry.class`,
435
+ ),
436
+ localAttempts,
437
+ exhausted: oneOf(
438
+ value.retry.exhausted,
439
+ ["blocked", "repair-required", "terminal-failure"],
440
+ `${value.id}.retry.exhausted`,
441
+ ),
442
+ };
443
+ if (retry.class === "never" && retry.localAttempts !== 0) {
444
+ fail(`${value.id}.retry.localAttempts must be zero for retry class never`);
445
+ }
446
+
447
+ if (
448
+ !Array.isArray(value.evidenceRequirements) ||
449
+ value.evidenceRequirements.length === 0
450
+ ) {
451
+ fail(`${value.id}.evidenceRequirements must not be empty`);
452
+ }
453
+ const evidenceRequirements = value.evidenceRequirements.map((entry, index) =>
454
+ nonEmpty(entry, `${value.id}.evidenceRequirements[${index}]`),
455
+ );
456
+
457
+ return {
458
+ id: value.id,
459
+ executor: value.executor,
460
+ adapter: value.adapter,
461
+ artifactRoles,
462
+ destination,
463
+ channelPolicy,
464
+ activationPolicy,
465
+ readbackPredicates,
466
+ effect: normalizeMessage(
467
+ value.effect,
468
+ RELEASE_TAIL_EFFECT_SCHEMA,
469
+ registry.effectKind,
470
+ `${value.id}.effect`,
471
+ ),
472
+ observation: normalizeMessage(
473
+ value.observation,
474
+ RELEASE_TAIL_OBSERVATION_SCHEMA,
475
+ registry.observationKind,
476
+ `${value.id}.observation`,
477
+ ),
478
+ receipt: normalizeMessage(
479
+ value.receipt,
480
+ RELEASE_TAIL_RECEIPT_SCHEMA,
481
+ registry.receiptKind,
482
+ `${value.id}.receipt`,
483
+ ),
484
+ operationIdentity,
485
+ idempotency,
486
+ retry,
487
+ evidenceRequirements,
488
+ };
489
+ }
490
+
491
+ export function parseReleaseTailDeclaration(input) {
492
+ let value = input;
493
+ if (typeof input === "string") {
494
+ try {
495
+ value = JSON.parse(input);
496
+ } catch (error) {
497
+ fail(`input is not valid JSON: ${error.message}`);
498
+ }
499
+ }
500
+ value = structuredClone(plainObject(value, "declaration"));
501
+ rejectExecutableKeys(value);
502
+ exactFields(value, TOP_LEVEL_FIELDS, "declaration");
503
+ if (value.contract !== RELEASE_TAIL_DECLARATION_CONTRACT) {
504
+ fail(`contract must be ${RELEASE_TAIL_DECLARATION_CONTRACT}`);
505
+ }
506
+ if (value.schemaVersion !== 1) fail("schemaVersion must be 1");
507
+ if (value.transactionPolicy !== RELEASE_TAIL_TRANSACTION_POLICY) {
508
+ fail(`transactionPolicy must be ${RELEASE_TAIL_TRANSACTION_POLICY}`);
509
+ }
510
+ exactFields(value.subject, SUBJECT_FIELDS, "subject");
511
+ const subject = {
512
+ repository: nonEmpty(value.subject.repository, "subject.repository"),
513
+ sourceSha: nonEmpty(
514
+ value.subject.sourceSha,
515
+ "subject.sourceSha",
516
+ ).toLowerCase(),
517
+ version: nonEmpty(value.subject.version, "subject.version"),
518
+ tag: nonEmpty(value.subject.tag, "subject.tag"),
519
+ channel: oneOf(
520
+ value.subject.channel,
521
+ ["alpha", "release", "stable"],
522
+ "subject.channel",
523
+ ),
524
+ };
525
+ if (!SHA.test(subject.sourceSha)) {
526
+ fail("subject.sourceSha must be an exact 40-character Git SHA");
527
+ }
528
+ if (!Array.isArray(value.capabilities) || value.capabilities.length === 0) {
529
+ fail("capabilities must not be empty");
530
+ }
531
+ const capabilities = value.capabilities.map((entry) =>
532
+ normalizeCapability(entry, subject),
533
+ );
534
+ if (
535
+ new Set(capabilities.map((entry) => entry.id)).size !== capabilities.length
536
+ ) {
537
+ fail("capability ids must be unique");
538
+ }
539
+ const order = new Map(
540
+ RELEASE_TAIL_CAPABILITY_REGISTRY.map((entry, index) => [entry.id, index]),
541
+ );
542
+ capabilities.sort((left, right) => order.get(left.id) - order.get(right.id));
543
+ const transactionRoots = new Set(
544
+ capabilities.map((entry) => entry.operationIdentity.transactionRoot),
545
+ );
546
+ if (transactionRoots.size !== 1) {
547
+ fail("all operation identities must bind one transaction root");
548
+ }
549
+ return {
550
+ contract: RELEASE_TAIL_DECLARATION_CONTRACT,
551
+ schemaVersion: 1,
552
+ transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
553
+ subject,
554
+ capabilities,
555
+ };
556
+ }
557
+
558
+ function createEffect(declaration, capability) {
559
+ const identity = capability.operationIdentity;
560
+ const envelope = {
561
+ schema: RELEASE_TAIL_EFFECT_SCHEMA,
562
+ kind: capability.effect.kind,
563
+ transactionRoot: identity.transactionRoot,
564
+ operationId: releaseTailRoot(identity),
565
+ capabilityId: capability.id,
566
+ executor: capability.executor,
567
+ adapter: capability.adapter,
568
+ subjectRoot: identity.subjectRoot,
569
+ targetRoot: identity.targetRoot,
570
+ attemptKey: identity.attemptKey,
571
+ subject: declaration.subject,
572
+ artifactRoles: capability.artifactRoles,
573
+ destination: capability.destination,
574
+ channelPolicy: capability.channelPolicy,
575
+ activationPolicy: capability.activationPolicy,
576
+ readbackPredicates: capability.readbackPredicates,
577
+ idempotency: capability.idempotency,
578
+ retry: capability.retry,
579
+ evidenceRequirements: capability.evidenceRequirements,
580
+ };
581
+ return { ...envelope, effectRoot: releaseTailRoot(envelope) };
582
+ }
583
+
584
+ export function compileReleaseTailDeclaration(input) {
585
+ const declaration = parseReleaseTailDeclaration(input);
586
+ const effects = declaration.capabilities.map((capability) =>
587
+ createEffect(declaration, capability),
588
+ );
589
+ const plan = {
590
+ schema: "kungfu.buildchain.release-tail.effect-plan/v1",
591
+ transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
592
+ transactionRoot:
593
+ declaration.capabilities[0].operationIdentity.transactionRoot,
594
+ declarationRoot: releaseTailRoot(declaration),
595
+ subject: declaration.subject,
596
+ operationOrder: effects.map((effect) => effect.operationId),
597
+ effects,
598
+ };
599
+ return { ...plan, planRoot: releaseTailRoot(plan) };
600
+ }
601
+
602
+ export function validateReleaseTailEffectPlan(plan) {
603
+ const issues = [];
604
+ try {
605
+ exactFields(plan, EFFECT_PLAN_FIELDS, "effect plan");
606
+ rejectExecutableKeys(plan);
607
+ exactFields(plan.subject, SUBJECT_FIELDS, "effect plan subject");
608
+ } catch (error) {
609
+ issues.push(error.message);
610
+ }
611
+ if (plan?.schema !== "kungfu.buildchain.release-tail.effect-plan/v1") {
612
+ issues.push("effect plan schema is invalid");
613
+ }
614
+ if (plan?.transactionPolicy !== RELEASE_TAIL_TRANSACTION_POLICY) {
615
+ issues.push("effect plan transactionPolicy is invalid");
616
+ }
617
+ for (const field of ["transactionRoot", "declarationRoot", "planRoot"]) {
618
+ if (!ROOT.test(plan?.[field] || "")) issues.push(`${field} is invalid`);
619
+ }
620
+ if (!Array.isArray(plan?.effects) || plan.effects.length === 0) {
621
+ issues.push("effects must be a non-empty array");
622
+ } else {
623
+ const operationIds = [];
624
+ for (const effect of plan.effects) {
625
+ try {
626
+ exactFields(effect, EFFECT_FIELDS, `effect ${effect?.capabilityId}`);
627
+ } catch (error) {
628
+ issues.push(error.message);
629
+ }
630
+ const descriptor = CAPABILITY_BY_ID.get(effect?.capabilityId);
631
+ if (!descriptor) {
632
+ issues.push(
633
+ `unsupported effect capability: ${effect?.capabilityId || "<empty>"}`,
634
+ );
635
+ continue;
636
+ }
637
+ if (
638
+ effect.schema !== RELEASE_TAIL_EFFECT_SCHEMA ||
639
+ effect.kind !== descriptor.effectKind ||
640
+ effect.executor !== descriptor.executor ||
641
+ effect.adapter !== descriptor.adapter
642
+ ) {
643
+ issues.push(
644
+ `effect ${effect.capabilityId} does not match the capability registry`,
645
+ );
646
+ }
647
+ if (effect.transactionRoot !== plan.transactionRoot) {
648
+ issues.push(`effect ${effect.capabilityId} transactionRoot mismatch`);
649
+ }
650
+ if (
651
+ releaseTailStableJson(effect.subject) !==
652
+ releaseTailStableJson(plan.subject)
653
+ ) {
654
+ issues.push(`effect ${effect.capabilityId} subject mismatch`);
655
+ }
656
+ if (
657
+ !ROOT.test(effect.subjectRoot || "") ||
658
+ !ROOT.test(effect.targetRoot || "")
659
+ ) {
660
+ issues.push(
661
+ `effect ${effect.capabilityId} has an invalid subject or target root`,
662
+ );
663
+ }
664
+ if (
665
+ effect.operationId !==
666
+ releaseTailRoot({
667
+ transactionRoot: effect.transactionRoot,
668
+ capabilityId: effect.capabilityId,
669
+ subjectRoot: effect.subjectRoot,
670
+ targetRoot: effect.targetRoot,
671
+ attemptKey: effect.attemptKey,
672
+ })
673
+ ) {
674
+ issues.push(`effect ${effect.capabilityId} operationId mismatch`);
675
+ }
676
+ const effectBody = structuredClone(effect);
677
+ delete effectBody.effectRoot;
678
+ if (effect.effectRoot !== releaseTailRoot(effectBody)) {
679
+ issues.push(`effect ${effect.capabilityId} effectRoot mismatch`);
680
+ }
681
+ operationIds.push(effect.operationId);
682
+ }
683
+ if (new Set(operationIds).size !== operationIds.length) {
684
+ issues.push("effect plan operation ids must be unique");
685
+ }
686
+ if (operationIds.join("\n") !== (plan.operationOrder || []).join("\n")) {
687
+ issues.push("effect plan operationOrder mismatch");
688
+ }
689
+ }
690
+ const body = structuredClone(plan || {});
691
+ delete body.planRoot;
692
+ if (plan?.planRoot !== releaseTailRoot(body))
693
+ issues.push("planRoot mismatch");
694
+ return { valid: issues.length === 0, issues };
695
+ }
696
+
697
+ export function createReleaseTailTransaction(input) {
698
+ const plan = input?.schema?.endsWith("effect-plan/v1")
699
+ ? structuredClone(input)
700
+ : compileReleaseTailDeclaration(input);
701
+ const planValidation = validateReleaseTailEffectPlan(plan);
702
+ if (!planValidation.valid) {
703
+ throw new Error(
704
+ `invalid release-tail effect plan: ${planValidation.issues.join("; ")}`,
705
+ );
706
+ }
707
+ const transaction = {
708
+ schema: RELEASE_TAIL_TRANSACTION_SCHEMA,
709
+ transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
710
+ transactionRoot: plan.transactionRoot,
711
+ declarationRoot: plan.declarationRoot,
712
+ planRoot: plan.planRoot,
713
+ subject: plan.subject,
714
+ state: "prepared",
715
+ operationOrder: plan.operationOrder,
716
+ operations: plan.effects.map((effect) => ({
717
+ operationId: effect.operationId,
718
+ capabilityId: effect.capabilityId,
719
+ effect,
720
+ status: "pending",
721
+ effectAttempts: 0,
722
+ readbackAttempts: 0,
723
+ observationRoots: [],
724
+ receipt: null,
725
+ })),
726
+ observations: [],
727
+ receipts: [],
728
+ failure: null,
729
+ };
730
+ return refreshTransaction(transaction);
731
+ }
732
+
733
+ function transactionBody(transaction) {
734
+ const copy = structuredClone(transaction);
735
+ delete copy.stateRoot;
736
+ return copy;
737
+ }
738
+
739
+ function refreshTransaction(transaction) {
740
+ transaction.stateRoot = releaseTailRoot(transactionBody(transaction));
741
+ return transaction;
742
+ }
743
+
744
+ export function validateReleaseTailTransaction(transaction) {
745
+ const issues = [];
746
+ if (transaction?.schema !== RELEASE_TAIL_TRANSACTION_SCHEMA) {
747
+ issues.push(`schema must be ${RELEASE_TAIL_TRANSACTION_SCHEMA}`);
748
+ }
749
+ if (!RELEASE_TAIL_STATES.includes(transaction?.state))
750
+ issues.push("state is invalid");
751
+ if (!ROOT.test(transaction?.transactionRoot || "")) {
752
+ issues.push("transactionRoot is invalid");
753
+ }
754
+ if (!ROOT.test(transaction?.declarationRoot || "")) {
755
+ issues.push("declarationRoot is invalid");
756
+ }
757
+ if (!ROOT.test(transaction?.planRoot || ""))
758
+ issues.push("planRoot is invalid");
759
+ if (!Array.isArray(transaction?.operations)) {
760
+ issues.push("operations must be an array");
761
+ } else {
762
+ const ids = transaction.operations.map((entry) => entry.operationId);
763
+ if (ids.join("\n") !== (transaction.operationOrder || []).join("\n")) {
764
+ issues.push("operations must preserve operationOrder");
765
+ }
766
+ for (const operation of transaction.operations) {
767
+ if (!["pending", "complete", "failed"].includes(operation.status)) {
768
+ issues.push(
769
+ `operation ${operation.operationId || "<unknown>"} status is invalid`,
770
+ );
771
+ }
772
+ if (
773
+ !Number.isSafeInteger(operation.effectAttempts) ||
774
+ operation.effectAttempts < 0
775
+ ) {
776
+ issues.push(
777
+ `operation ${operation.operationId || "<unknown>"} effectAttempts is invalid`,
778
+ );
779
+ }
780
+ if (
781
+ !Number.isSafeInteger(operation.readbackAttempts) ||
782
+ operation.readbackAttempts < 0
783
+ ) {
784
+ issues.push(
785
+ `operation ${operation.operationId || "<unknown>"} readbackAttempts is invalid`,
786
+ );
787
+ }
788
+ if (!Array.isArray(operation.observationRoots)) {
789
+ issues.push(
790
+ `operation ${operation.operationId || "<unknown>"} observationRoots is invalid`,
791
+ );
792
+ }
793
+ }
794
+ }
795
+ if (!Array.isArray(transaction?.observations)) {
796
+ issues.push("observations must be an array");
797
+ } else {
798
+ for (const observation of transaction.observations) {
799
+ const body = structuredClone(observation);
800
+ delete body.observationRoot;
801
+ if (observation.observationRoot !== releaseTailRoot(body)) {
802
+ issues.push(
803
+ `observation ${observation.operationId || "<unknown>"} root mismatch`,
804
+ );
805
+ }
806
+ }
807
+ }
808
+ if (!Array.isArray(transaction?.receipts)) {
809
+ issues.push("receipts must be an array");
810
+ } else {
811
+ for (const receipt of transaction.receipts) {
812
+ const body = structuredClone(receipt);
813
+ delete body.receiptRoot;
814
+ if (receipt.receiptRoot !== releaseTailRoot(body)) {
815
+ issues.push(
816
+ `receipt ${receipt.operationId || "<unknown>"} root mismatch`,
817
+ );
818
+ }
819
+ }
820
+ }
821
+ if (
822
+ transaction?.stateRoot !==
823
+ releaseTailRoot(transactionBody(transaction || {}))
824
+ ) {
825
+ issues.push("stateRoot mismatch");
826
+ }
827
+ return { valid: issues.length === 0, issues };
828
+ }
829
+
830
+ export function readReleaseTailTransaction(filePath) {
831
+ const transaction = JSON.parse(fs.readFileSync(filePath, "utf8"));
832
+ const validation = validateReleaseTailTransaction(transaction);
833
+ if (!validation.valid) {
834
+ throw new Error(
835
+ `invalid release-tail transaction: ${validation.issues.join("; ")}`,
836
+ );
837
+ }
838
+ return transaction;
839
+ }
840
+
841
+ export function writeReleaseTailTransaction(filePath, transaction) {
842
+ const validation = validateReleaseTailTransaction(transaction);
843
+ if (!validation.valid) {
844
+ throw new Error(
845
+ `invalid release-tail transaction: ${validation.issues.join("; ")}`,
846
+ );
847
+ }
848
+ const resolved = path.resolve(filePath);
849
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
850
+ const temporary = `${resolved}.tmp-${process.pid}`;
851
+ fs.writeFileSync(temporary, `${JSON.stringify(transaction, null, 2)}\n`, {
852
+ mode: 0o600,
853
+ });
854
+ fs.renameSync(temporary, resolved);
855
+ return resolved;
856
+ }
857
+
858
+ function normalizeAdapterObservation(effect, raw, phase) {
859
+ const value = raw && typeof raw === "object" ? raw : {};
860
+ const outcome = oneOf(
861
+ value.outcome || "transient",
862
+ ["observed", "absent", "transient", "conflict"],
863
+ `${effect.capabilityId} adapter observation outcome`,
864
+ );
865
+ const subjectRoot = value.subjectRoot
866
+ ? exactRoot(
867
+ value.subjectRoot,
868
+ `${effect.capabilityId} observation subjectRoot`,
869
+ )
870
+ : "";
871
+ const targetRoot = value.targetRoot
872
+ ? exactRoot(
873
+ value.targetRoot,
874
+ `${effect.capabilityId} observation targetRoot`,
875
+ )
876
+ : "";
877
+ let status = outcome;
878
+ if (outcome === "observed") {
879
+ status =
880
+ subjectRoot === effect.subjectRoot && targetRoot === effect.targetRoot
881
+ ? "matched"
882
+ : "stale";
883
+ }
884
+ const evidenceRoots = Array.isArray(value.evidenceRoots)
885
+ ? [
886
+ ...new Set(
887
+ value.evidenceRoots.map((root, index) =>
888
+ exactRoot(
889
+ root,
890
+ `${effect.capabilityId} observation evidenceRoots[${index}]`,
891
+ ),
892
+ ),
893
+ ),
894
+ ].sort()
895
+ : [];
896
+ const observation = {
897
+ schema: RELEASE_TAIL_OBSERVATION_SCHEMA,
898
+ kind: CAPABILITY_BY_ID.get(effect.capabilityId).observationKind,
899
+ transactionRoot: effect.transactionRoot,
900
+ operationId: effect.operationId,
901
+ capabilityId: effect.capabilityId,
902
+ phase,
903
+ status,
904
+ subjectRoot,
905
+ targetRoot,
906
+ providerCode: /^[a-z0-9][a-z0-9._-]{0,159}$/u.test(
907
+ String(value.providerCode || status),
908
+ )
909
+ ? String(value.providerCode || status)
910
+ : "provider-observation",
911
+ evidenceRoots,
912
+ };
913
+ return { ...observation, observationRoot: releaseTailRoot(observation) };
914
+ }
915
+
916
+ function createReceipt(operation, action, observation) {
917
+ const effect = operation.effect;
918
+ const body = {
919
+ schema: RELEASE_TAIL_RECEIPT_SCHEMA,
920
+ kind: CAPABILITY_BY_ID.get(effect.capabilityId).receiptKind,
921
+ transactionRoot: effect.transactionRoot,
922
+ operationId: effect.operationId,
923
+ capabilityId: effect.capabilityId,
924
+ subjectRoot: effect.subjectRoot,
925
+ targetRoot: effect.targetRoot,
926
+ effectRoot: effect.effectRoot,
927
+ action,
928
+ effectAttempts: operation.effectAttempts,
929
+ readbackAttempts: operation.readbackAttempts,
930
+ observationRoots: [...operation.observationRoots],
931
+ evidenceRoots: observation.evidenceRoots,
932
+ };
933
+ return { ...body, receiptRoot: releaseTailRoot(body) };
934
+ }
935
+
936
+ function capabilityForOperation(operation) {
937
+ return CAPABILITY_BY_ID.get(operation.capabilityId);
938
+ }
939
+
940
+ function nextPendingOperation(transaction) {
941
+ return (
942
+ transaction.operations.find((entry) => entry.status === "pending") || null
943
+ );
944
+ }
945
+
946
+ function adapterFor(adapters, operation) {
947
+ const descriptor = capabilityForOperation(operation);
948
+ const adapter = adapters?.[descriptor.adapter];
949
+ if (
950
+ !adapter ||
951
+ typeof adapter.readback !== "function" ||
952
+ typeof adapter.apply !== "function"
953
+ ) {
954
+ throw new Error(
955
+ `release-tail adapter '${descriptor.adapter}' must provide readback and apply`,
956
+ );
957
+ }
958
+ return adapter;
959
+ }
960
+
961
+ function recordObservation(transaction, operationId, observation) {
962
+ const next = structuredClone(transaction);
963
+ const operation = next.operations.find(
964
+ (entry) => entry.operationId === operationId,
965
+ );
966
+ if (!operation)
967
+ throw new Error(`unknown release-tail operation: ${operationId}`);
968
+ operation.observationRoots.push(observation.observationRoot);
969
+ next.observations.push(observation);
970
+ return refreshTransaction(next);
971
+ }
972
+
973
+ function completeOperation(transaction, operationId, action, observation) {
974
+ const next = structuredClone(transaction);
975
+ const operation = next.operations.find(
976
+ (entry) => entry.operationId === operationId,
977
+ );
978
+ operation.status = "complete";
979
+ operation.receipt = createReceipt(operation, action, observation);
980
+ next.receipts = next.operations
981
+ .filter((entry) => entry.receipt)
982
+ .map((entry) => entry.receipt);
983
+ next.failure = null;
984
+ next.state = nextPendingOperation(next)
985
+ ? capabilityForOperation(nextPendingOperation(next)).transactionState
986
+ : "complete";
987
+ return refreshTransaction(next);
988
+ }
989
+
990
+ function failOperation(
991
+ transaction,
992
+ operationId,
993
+ state,
994
+ code,
995
+ observation = null,
996
+ ) {
997
+ const next = structuredClone(transaction);
998
+ const operation = next.operations.find(
999
+ (entry) => entry.operationId === operationId,
1000
+ );
1001
+ operation.status = "failed";
1002
+ next.state = state;
1003
+ next.failure = {
1004
+ operationId,
1005
+ capabilityId: operation.capabilityId,
1006
+ code,
1007
+ observationRoot: observation?.observationRoot || "",
1008
+ };
1009
+ return refreshTransaction(next);
1010
+ }
1011
+
1012
+ async function checkpoint(callback, transaction) {
1013
+ if (callback) await callback(structuredClone(transaction));
1014
+ }
1015
+
1016
+ export async function executeReleaseTailTransaction(
1017
+ transaction,
1018
+ { adapters, checkpoint: checkpointCallback } = {},
1019
+ ) {
1020
+ const validation = validateReleaseTailTransaction(transaction);
1021
+ if (!validation.valid) {
1022
+ throw new Error(
1023
+ `invalid release-tail transaction: ${validation.issues.join("; ")}`,
1024
+ );
1025
+ }
1026
+ if (TERMINAL_STATES.has(transaction.state))
1027
+ return structuredClone(transaction);
1028
+ let current = structuredClone(transaction);
1029
+ while (nextPendingOperation(current)) {
1030
+ const pending = nextPendingOperation(current);
1031
+ const capability = CAPABILITY_BY_ID.get(pending.capabilityId);
1032
+ const adapter = adapterFor(adapters, pending);
1033
+ current.state = capability.transactionState;
1034
+ current = refreshTransaction(current);
1035
+ await checkpoint(checkpointCallback, current);
1036
+
1037
+ const policy = pending.effect.retry;
1038
+ const maxCycles = 1 + policy.localAttempts;
1039
+ let applied = pending.effectAttempts > 0;
1040
+ let completed = false;
1041
+
1042
+ for (let cycle = 0; cycle < maxCycles; cycle += 1) {
1043
+ current.state = "reading-back";
1044
+ current = refreshTransaction(current);
1045
+ await checkpoint(checkpointCallback, current);
1046
+ let rawReadback;
1047
+ try {
1048
+ rawReadback = await adapter.readback(structuredClone(pending.effect));
1049
+ } catch (error) {
1050
+ rawReadback = {
1051
+ outcome:
1052
+ error?.releaseTailClass === "conflict" ? "conflict" : "transient",
1053
+ providerCode: error?.releaseTailCode || "readback-error",
1054
+ };
1055
+ }
1056
+ let operation = current.operations.find(
1057
+ (entry) => entry.operationId === pending.operationId,
1058
+ );
1059
+ operation.readbackAttempts += 1;
1060
+ current = refreshTransaction(current);
1061
+ const preObservation = normalizeAdapterObservation(
1062
+ pending.effect,
1063
+ rawReadback,
1064
+ applied ? "retry-readback" : "pre-effect-readback",
1065
+ );
1066
+ current = recordObservation(current, pending.operationId, preObservation);
1067
+ await checkpoint(checkpointCallback, current);
1068
+ if (preObservation.status === "matched") {
1069
+ current = completeOperation(
1070
+ current,
1071
+ pending.operationId,
1072
+ applied ? "applied-and-observed" : "observed-existing",
1073
+ preObservation,
1074
+ );
1075
+ await checkpoint(checkpointCallback, current);
1076
+ completed = true;
1077
+ break;
1078
+ }
1079
+ if (preObservation.status === "conflict") {
1080
+ current = failOperation(
1081
+ current,
1082
+ pending.operationId,
1083
+ "terminal-failure",
1084
+ "provider-conflict",
1085
+ preObservation,
1086
+ );
1087
+ await checkpoint(checkpointCallback, current);
1088
+ return current;
1089
+ }
1090
+ if (preObservation.status === "stale") {
1091
+ current = failOperation(
1092
+ current,
1093
+ pending.operationId,
1094
+ "repair-required",
1095
+ "stale-readback",
1096
+ preObservation,
1097
+ );
1098
+ await checkpoint(checkpointCallback, current);
1099
+ return current;
1100
+ }
1101
+ if (preObservation.status === "transient") {
1102
+ if (cycle + 1 < maxCycles) continue;
1103
+ break;
1104
+ }
1105
+ if (applied && policy.class !== "provider-transient") continue;
1106
+
1107
+ operation = current.operations.find(
1108
+ (entry) => entry.operationId === pending.operationId,
1109
+ );
1110
+ operation.effectAttempts += 1;
1111
+ current = refreshTransaction(current);
1112
+ await checkpoint(checkpointCallback, current);
1113
+ try {
1114
+ await adapter.apply(structuredClone(pending.effect));
1115
+ } catch (error) {
1116
+ if (error?.releaseTailClass === "conflict") {
1117
+ current = failOperation(
1118
+ current,
1119
+ pending.operationId,
1120
+ "terminal-failure",
1121
+ error?.releaseTailCode || "provider-conflict",
1122
+ );
1123
+ await checkpoint(checkpointCallback, current);
1124
+ return current;
1125
+ }
1126
+ }
1127
+ applied = true;
1128
+
1129
+ current.state = "reading-back";
1130
+ current = refreshTransaction(current);
1131
+ await checkpoint(checkpointCallback, current);
1132
+ let postRaw;
1133
+ try {
1134
+ postRaw = await adapter.readback(structuredClone(pending.effect));
1135
+ } catch (error) {
1136
+ postRaw = {
1137
+ outcome:
1138
+ error?.releaseTailClass === "conflict" ? "conflict" : "transient",
1139
+ providerCode: error?.releaseTailCode || "post-effect-readback-error",
1140
+ };
1141
+ }
1142
+ operation = current.operations.find(
1143
+ (entry) => entry.operationId === pending.operationId,
1144
+ );
1145
+ operation.readbackAttempts += 1;
1146
+ current = refreshTransaction(current);
1147
+ const postObservation = normalizeAdapterObservation(
1148
+ pending.effect,
1149
+ postRaw,
1150
+ "post-effect-readback",
1151
+ );
1152
+ current = recordObservation(
1153
+ current,
1154
+ pending.operationId,
1155
+ postObservation,
1156
+ );
1157
+ await checkpoint(checkpointCallback, current);
1158
+ if (postObservation.status === "matched") {
1159
+ current = completeOperation(
1160
+ current,
1161
+ pending.operationId,
1162
+ "applied-and-observed",
1163
+ postObservation,
1164
+ );
1165
+ await checkpoint(checkpointCallback, current);
1166
+ completed = true;
1167
+ break;
1168
+ }
1169
+ if (postObservation.status === "conflict") {
1170
+ current = failOperation(
1171
+ current,
1172
+ pending.operationId,
1173
+ "terminal-failure",
1174
+ "provider-conflict",
1175
+ postObservation,
1176
+ );
1177
+ await checkpoint(checkpointCallback, current);
1178
+ return current;
1179
+ }
1180
+ if (postObservation.status === "stale") {
1181
+ current = failOperation(
1182
+ current,
1183
+ pending.operationId,
1184
+ "repair-required",
1185
+ "stale-readback",
1186
+ postObservation,
1187
+ );
1188
+ await checkpoint(checkpointCallback, current);
1189
+ return current;
1190
+ }
1191
+ }
1192
+
1193
+ if (!completed) {
1194
+ current = failOperation(
1195
+ current,
1196
+ pending.operationId,
1197
+ policy.exhausted,
1198
+ "local-retry-exhausted",
1199
+ );
1200
+ await checkpoint(checkpointCallback, current);
1201
+ return current;
1202
+ }
1203
+ }
1204
+ return current;
1205
+ }
1206
+
1207
+ export function releaseTailRetryPolicyFromDeclaration(input) {
1208
+ const declaration = parseReleaseTailDeclaration(input);
1209
+ return Object.fromEntries(
1210
+ declaration.capabilities.map((capability) => [
1211
+ capability.id,
1212
+ capability.retry,
1213
+ ]),
1214
+ );
1215
+ }
1216
+
1217
+ export function createReleaseTailAdapterSet(declaration, adapters) {
1218
+ const parsed = parseReleaseTailDeclaration(declaration);
1219
+ const result = { declarations: {} };
1220
+ for (const capability of parsed.capabilities) {
1221
+ const adapter = adapters?.[capability.adapter];
1222
+ if (!adapter)
1223
+ throw new Error(`missing release-tail adapter: ${capability.adapter}`);
1224
+ result[capability.adapter] = adapter;
1225
+ result.declarations[capability.id] = { retry: capability.retry };
1226
+ }
1227
+ return result;
1228
+ }