@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -0,0 +1,190 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
7
+ publicationArtifactCandidateDigest,
8
+ } from "./publication-artifact-candidate.js";
9
+
10
+ export const PUBLICATION_SEALED_BUNDLE_CONTRACT = "kungfu-buildchain-publication-sealed-bundle";
11
+
12
+ function requiredString(value, label) {
13
+ const normalized = String(value || "").trim();
14
+ if (!normalized) {
15
+ throw new Error(`${label} must be a non-empty string`);
16
+ }
17
+ return normalized;
18
+ }
19
+
20
+ function safeRelativePath(value, label) {
21
+ const normalized = requiredString(value, label).replaceAll("\\", "/");
22
+ if (normalized.startsWith("/") || normalized.split("/").some((part) => part === ".." || part === "")) {
23
+ throw new Error(`${label} must be a safe relative path`);
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ function normalizeSha256(value, label) {
29
+ const normalized = requiredString(value, label)
30
+ .replace(/^sha256:/, "")
31
+ .toLowerCase();
32
+ if (!/^[0-9a-f]{64}$/.test(normalized)) {
33
+ throw new Error(`${label} must be a sha256 digest`);
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function sha256File(filePath) {
39
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
40
+ }
41
+
42
+ function normalizeFile(entry, label) {
43
+ const size = Number(entry?.size ?? entry?.bytes);
44
+ if (!Number.isSafeInteger(size) || size < 0) {
45
+ throw new Error(`${label}.size must be a non-negative safe integer`);
46
+ }
47
+ return {
48
+ path: safeRelativePath(entry.path, `${label}.path`),
49
+ size,
50
+ sha256: normalizeSha256(entry.sha256, `${label}.sha256`),
51
+ };
52
+ }
53
+
54
+ function candidatePayload(candidate) {
55
+ if (candidate?.contract !== PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT || Number(candidate?.schemaVersion) !== 1) {
56
+ throw new Error("publication artifact candidate contract mismatch");
57
+ }
58
+ const { candidateDigest: _candidateDigest, ...payload } = candidate;
59
+ const digest = publicationArtifactCandidateDigest(payload);
60
+ if (normalizeSha256(candidate.candidateDigest, "candidate.candidateDigest") !== digest) {
61
+ throw new Error("publication artifact candidate digest mismatch");
62
+ }
63
+ return { payload, digest };
64
+ }
65
+
66
+ function selectFile(files, filePath, label) {
67
+ const normalizedPath = safeRelativePath(filePath, label);
68
+ const matches = files.filter((entry) => entry.path === normalizedPath);
69
+ if (matches.length !== 1) {
70
+ throw new Error(`${label} must identify exactly one candidate file`);
71
+ }
72
+ return matches[0];
73
+ }
74
+
75
+ export function createPublicationSealedBundle({
76
+ candidate,
77
+ packageName,
78
+ packageVersion,
79
+ npmTarballPath,
80
+ npmIntegrity,
81
+ releaseAssetPaths = [],
82
+ githubReleaseRequired = true,
83
+ } = {}) {
84
+ const { digest } = candidatePayload(candidate);
85
+ const files = (candidate.files || [])
86
+ .map((entry, index) => normalizeFile(entry, `candidate.files[${index}]`))
87
+ .sort((left, right) => left.path.localeCompare(right.path));
88
+ if (new Set(files.map((entry) => entry.path)).size !== files.length) {
89
+ throw new Error("publication sealed bundle candidate paths must be unique");
90
+ }
91
+ const npmTarball = selectFile(files, npmTarballPath, "npmTarballPath");
92
+ const releaseAssets = [...new Set(releaseAssetPaths)]
93
+ .map((assetPath, index) => selectFile(files, assetPath, `releaseAssetPaths[${index}]`))
94
+ .sort((left, right) => left.path.localeCompare(right.path));
95
+ if (releaseAssets.length === 0) {
96
+ throw new Error("publication sealed bundle requires at least one release asset");
97
+ }
98
+ const normalizedVersion = requiredString(packageVersion, "packageVersion");
99
+ return {
100
+ schemaVersion: 1,
101
+ contract: PUBLICATION_SEALED_BUNDLE_CONTRACT,
102
+ root: `sha256:${digest}`,
103
+ candidate,
104
+ files,
105
+ npm: {
106
+ name: requiredString(packageName, "packageName"),
107
+ version: normalizedVersion,
108
+ path: npmTarball.path,
109
+ size: npmTarball.size,
110
+ sha256: npmTarball.sha256,
111
+ integrity: requiredString(npmIntegrity, "npmIntegrity"),
112
+ },
113
+ releaseAssets,
114
+ completion: {
115
+ githubReleaseRequired: Boolean(githubReleaseRequired),
116
+ },
117
+ durablePath: `sealed-bundle/sha256/${digest}`,
118
+ resumeCommand:
119
+ `buildchain paper resume --version ${JSON.stringify(normalizedVersion)} ` +
120
+ `--state-ref ${JSON.stringify(`refs/heads/buildchain/release-state/${normalizedVersion.replaceAll(".", "-")}`)}`,
121
+ };
122
+ }
123
+
124
+ export function verifyPublicationSealedBundle({ bundleRoot, manifest } = {}) {
125
+ if (manifest?.contract !== PUBLICATION_SEALED_BUNDLE_CONTRACT || Number(manifest?.schemaVersion) !== 1) {
126
+ throw new Error("publication sealed bundle contract mismatch");
127
+ }
128
+ const resolvedRoot = path.resolve(requiredString(bundleRoot, "bundleRoot"));
129
+ const { digest } = candidatePayload(manifest.candidate);
130
+ if (normalizeSha256(manifest.root, "manifest.root") !== digest) {
131
+ throw new Error("publication sealed bundle root mismatch");
132
+ }
133
+ const files = (manifest.files || [])
134
+ .map((entry, index) => normalizeFile(entry, `manifest.files[${index}]`))
135
+ .sort((left, right) => left.path.localeCompare(right.path));
136
+ const candidateFiles = (manifest.candidate.files || [])
137
+ .map((entry, index) => normalizeFile(entry, `candidate.files[${index}]`))
138
+ .sort((left, right) => left.path.localeCompare(right.path));
139
+ if (JSON.stringify(files) !== JSON.stringify(candidateFiles)) {
140
+ throw new Error("publication sealed bundle file inventory differs from candidate");
141
+ }
142
+ for (const [index, entry] of files.entries()) {
143
+ const filePath = path.resolve(resolvedRoot, entry.path);
144
+ if (!filePath.startsWith(`${resolvedRoot}${path.sep}`)) {
145
+ throw new Error(`manifest.files[${index}].path escapes bundle root`);
146
+ }
147
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
148
+ throw new Error(`publication sealed bundle file is missing: ${entry.path}`);
149
+ }
150
+ const size = fs.statSync(filePath).size;
151
+ const digestValue = sha256File(filePath);
152
+ if (size !== entry.size || digestValue !== entry.sha256) {
153
+ throw new Error(`publication sealed bundle file mismatch: ${entry.path}`);
154
+ }
155
+ }
156
+ const npmFile = selectFile(files, manifest.npm?.path, "manifest.npm.path");
157
+ if (
158
+ npmFile.size !== Number(manifest.npm?.size) ||
159
+ npmFile.sha256 !== normalizeSha256(manifest.npm?.sha256, "manifest.npm.sha256")
160
+ ) {
161
+ throw new Error("publication sealed bundle npm tarball inventory mismatch");
162
+ }
163
+ const releaseAssets = (manifest.releaseAssets || []).map((entry, index) => {
164
+ const selected = selectFile(files, entry.path, `manifest.releaseAssets[${index}].path`);
165
+ const normalized = normalizeFile(entry, `manifest.releaseAssets[${index}]`);
166
+ if (selected.size !== normalized.size || selected.sha256 !== normalized.sha256) {
167
+ throw new Error(`publication sealed bundle release asset mismatch: ${entry.path}`);
168
+ }
169
+ return selected;
170
+ });
171
+ if (releaseAssets.length === 0) {
172
+ throw new Error("publication sealed bundle requires at least one release asset");
173
+ }
174
+ return {
175
+ ok: true,
176
+ root: `sha256:${digest}`,
177
+ bundleRoot: resolvedRoot,
178
+ files,
179
+ npm: {
180
+ ...manifest.npm,
181
+ path: npmFile.path,
182
+ absolutePath: path.resolve(resolvedRoot, npmFile.path),
183
+ },
184
+ releaseAssets: releaseAssets.map((entry) => ({
185
+ ...entry,
186
+ absolutePath: path.resolve(resolvedRoot, entry.path),
187
+ })),
188
+ manifest,
189
+ };
190
+ }
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  export const RELEASE_TRANSACTION_STATES = Object.freeze([
6
6
  "prepared",
7
+ "sealed",
7
8
  "publishing",
8
9
  "publish_failed",
9
10
  "published",
@@ -18,7 +19,8 @@ const TERMINAL_STATES = new Set(["complete", "abandoned", "failed_permanently"])
18
19
  const BLOCKED_RECOVERY_STATES = new Set(["abandoned", "failed_permanently"]);
19
20
 
20
21
  const ALLOWED_TRANSITIONS = new Map([
21
- ["prepared", new Set(["publishing", "abandoned", "failed_permanently"])],
22
+ ["prepared", new Set(["sealed", "publishing", "abandoned", "failed_permanently"])],
23
+ ["sealed", new Set(["publishing", "repair_required", "abandoned", "failed_permanently"])],
22
24
  ["publishing", new Set(["publish_failed", "published", "repair_required", "abandoned", "failed_permanently"])],
23
25
  ["publish_failed", new Set(["publishing", "repair_required", "abandoned", "failed_permanently"])],
24
26
  ["published", new Set(["finalizing", "complete", "repair_required", "abandoned", "failed_permanently"])],
@@ -209,7 +211,12 @@ export function createReleaseTransaction({
209
211
  actor = "",
210
212
  runId = "",
211
213
  } = {}) {
212
- const id = releaseTransactionId({ repository, version, sourceSha, targetRef });
214
+ const id = releaseTransactionId({
215
+ repository,
216
+ version,
217
+ sourceSha,
218
+ targetRef,
219
+ });
213
220
  const createdAt = nowIso();
214
221
  return {
215
222
  schema: 1,
@@ -237,6 +244,10 @@ export function createReleaseTransaction({
237
244
  failure: "",
238
245
  artifacts: [],
239
246
  evidence: [],
247
+ sealed_bundle: null,
248
+ publication_state: "prepared",
249
+ milestones: {},
250
+ resume_command: "",
240
251
  created_at: createdAt,
241
252
  updated_at: createdAt,
242
253
  };
@@ -255,10 +266,11 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
255
266
  throw new Error(`cannot transition release transaction from ${currentState} to ${nextState}`);
256
267
  }
257
268
  }
258
- const nextFailure = nextState === "complete"
259
- ? optionalString(metadata.failure ?? "")
260
- : optionalString(metadata.failure ?? record.failure);
261
- return {
269
+ const nextFailure =
270
+ nextState === "complete"
271
+ ? optionalString(metadata.failure ?? "")
272
+ : optionalString(metadata.failure ?? record.failure);
273
+ const nextRecord = {
262
274
  ...record,
263
275
  previous_state: currentState === nextState ? record.previous_state || "" : currentState,
264
276
  state: nextState,
@@ -268,6 +280,88 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
268
280
  failure: nextFailure,
269
281
  updated_at: nowIso(),
270
282
  };
283
+ return {
284
+ ...nextRecord,
285
+ publication_state: releaseTransactionPublicationState(nextRecord),
286
+ };
287
+ }
288
+
289
+ export function releaseTransactionPublicationState(record) {
290
+ const state = record?.state || "prepared";
291
+ if (state === "repair_required") {
292
+ return "repair-required";
293
+ }
294
+ if (["abandoned", "failed_permanently"].includes(state)) {
295
+ return state.replaceAll("_", "-");
296
+ }
297
+ const githubReleaseRequired = record?.sealed_bundle?.completion?.githubReleaseRequired === true;
298
+ const githubReleaseComplete = record?.milestones?.github_release?.status === "complete";
299
+ if (state === "complete" && (!githubReleaseRequired || githubReleaseComplete)) {
300
+ return record?.channel === "alpha" ? "alpha-complete" : "release-complete";
301
+ }
302
+ if (
303
+ ["published", "finalizing", "complete"].includes(state) ||
304
+ record?.milestones?.package_published?.status === "complete"
305
+ ) {
306
+ return "package-published";
307
+ }
308
+ if (state === "sealed" || record?.sealed_bundle?.root) {
309
+ return "sealed";
310
+ }
311
+ if (state === "publishing" || state === "publish_failed") {
312
+ return state.replaceAll("_", "-");
313
+ }
314
+ return "prepared";
315
+ }
316
+
317
+ export function attachReleaseTransactionSealedBundle(record, sealedBundle, { actor = "", runId = "" } = {}) {
318
+ if (!record || typeof record !== "object") {
319
+ throw new Error("release transaction record must be an object");
320
+ }
321
+ const root = assertNonEmptyString(sealedBundle?.root, "sealedBundle.root");
322
+ if (record.sealed_bundle?.root && record.sealed_bundle.root !== root) {
323
+ throw new Error(`sealed bundle root mismatch: expected ${record.sealed_bundle.root}, got ${root}`);
324
+ }
325
+ if (!["prepared", "sealed"].includes(record.state || "prepared")) {
326
+ if (!record.sealed_bundle?.root) {
327
+ throw new Error(`cannot attach a sealed bundle after transaction entered ${record.state}`);
328
+ }
329
+ return record;
330
+ }
331
+ let next = {
332
+ ...record,
333
+ sealed_bundle: sealedBundle,
334
+ resume_command: optionalString(sealedBundle.resumeCommand),
335
+ actor: optionalString(actor || record.actor),
336
+ run_id: optionalString(runId || record.run_id),
337
+ updated_at: nowIso(),
338
+ };
339
+ if ((next.state || "prepared") === "prepared") {
340
+ next = transitionReleaseTransaction(next, "sealed", { actor, runId });
341
+ }
342
+ return {
343
+ ...next,
344
+ publication_state: releaseTransactionPublicationState(next),
345
+ };
346
+ }
347
+
348
+ export function recordReleaseTransactionMilestone(record, milestone, details = {}) {
349
+ const normalizedMilestone = assertNonEmptyString(milestone, "milestone").replaceAll("-", "_");
350
+ const next = {
351
+ ...record,
352
+ milestones: {
353
+ ...(record.milestones || {}),
354
+ [normalizedMilestone]: {
355
+ status: "complete",
356
+ ...details,
357
+ },
358
+ },
359
+ updated_at: nowIso(),
360
+ };
361
+ return {
362
+ ...next,
363
+ publication_state: releaseTransactionPublicationState(next),
364
+ };
271
365
  }
272
366
 
273
367
  export function readReleaseTransaction(filePath) {
@@ -284,8 +378,12 @@ export function writeReleaseTransaction(filePath, record) {
284
378
  return record;
285
379
  }
286
380
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
287
- fs.writeFileSync(filePath, `${JSON.stringify(record, null, 2)}\n`);
288
- return record;
381
+ const normalized = {
382
+ ...record,
383
+ publication_state: releaseTransactionPublicationState(record),
384
+ };
385
+ fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`);
386
+ return normalized;
289
387
  }
290
388
 
291
389
  function artifactIdentity(artifact) {
@@ -424,7 +522,9 @@ export function parsePublishArtifactsJson(value, label = "publish artifacts") {
424
522
  throw new Error(`${label} must be a JSON array`);
425
523
  }
426
524
  return parsed.map((artifact, index) =>
427
- normalizePublishArtifact(artifact, `${label}[${index}]`, { requireDigest: false }),
525
+ normalizePublishArtifact(artifact, `${label}[${index}]`, {
526
+ requireDigest: false,
527
+ }),
428
528
  );
429
529
  }
430
530
 
@@ -470,7 +570,9 @@ export function readPublishEvidence(filePath) {
470
570
 
471
571
  export function planArtifactPublish({ requiredArtifacts = [], existingArtifacts = [] } = {}) {
472
572
  const required = requiredArtifacts.map((artifact, index) =>
473
- normalizePublishArtifact(artifact, `requiredArtifacts[${index}]`, { requireDigest: false }),
573
+ normalizePublishArtifact(artifact, `requiredArtifacts[${index}]`, {
574
+ requireDigest: false,
575
+ }),
474
576
  );
475
577
  const existing = existingArtifacts.map((artifact, index) =>
476
578
  normalizePublishArtifact(artifact, `existingArtifacts[${index}]`),
@@ -487,7 +589,11 @@ export function planArtifactPublish({ requiredArtifacts = [], existingArtifacts
487
589
  const current = candidates.find((candidate) => artifactMatchesRequirement(candidate, artifact));
488
590
  if (!current) {
489
591
  if (candidates.length > 0) {
490
- conflicts.push({ expected: artifact, actual: candidates[0], fields: ["ref"] });
592
+ conflicts.push({
593
+ expected: artifact,
594
+ actual: candidates[0],
595
+ fields: ["ref"],
596
+ });
491
597
  continue;
492
598
  }
493
599
  publish.push(artifact);
@@ -667,7 +773,11 @@ export function planTransactionRecovery({
667
773
  explicitOverride = false,
668
774
  } = {}) {
669
775
  if (!transaction) {
670
- return { action: "prepare", blocked: false, reason: "no existing transaction" };
776
+ return {
777
+ action: "prepare",
778
+ blocked: false,
779
+ reason: "no existing transaction",
780
+ };
671
781
  }
672
782
  const state = transaction.state || "prepared";
673
783
  assertKnownState(state);
@@ -679,19 +789,39 @@ export function planTransactionRecovery({
679
789
  };
680
790
  }
681
791
  if (state === "complete") {
682
- return { action: "inspect", blocked: false, reason: "transaction is complete" };
792
+ return {
793
+ action: "inspect",
794
+ blocked: false,
795
+ reason: "transaction is complete",
796
+ };
683
797
  }
684
798
  if (state === "repair_required" && !explicitOverride) {
685
- return { action: "blocked", blocked: true, reason: "transaction requires explicit repair" };
799
+ return {
800
+ action: "blocked",
801
+ blocked: true,
802
+ reason: "transaction requires explicit repair",
803
+ };
686
804
  }
687
805
  const result = validation || (evidence ? { valid: true } : undefined);
688
806
  if (result?.valid) {
689
- return { action: "finalize", blocked: false, reason: "publish evidence is valid" };
807
+ return {
808
+ action: "finalize",
809
+ blocked: false,
810
+ reason: "publish evidence is valid",
811
+ };
690
812
  }
691
813
  if (state === "published" || state === "finalizing") {
692
- return { action: "repair", blocked: true, reason: "published transaction has invalid or missing evidence" };
814
+ return {
815
+ action: "repair",
816
+ blocked: true,
817
+ reason: "published transaction has invalid or missing evidence",
818
+ };
693
819
  }
694
- return { action: "publish", blocked: false, reason: "publish evidence is missing or incomplete" };
820
+ return {
821
+ action: "publish",
822
+ blocked: false,
823
+ reason: "publish evidence is missing or incomplete",
824
+ };
695
825
  }
696
826
 
697
827
  export function isReleaseTransactionTerminal(record) {