@kungfu-tech/buildchain 3.0.0 → 3.0.1-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 (35) hide show
  1. package/README.md +3 -0
  2. package/dist/site/buildchain-contract.json +101 -26
  3. package/dist/site/buildchain-site.json +93 -17
  4. package/dist/site/capability-registry.json +6 -5
  5. package/dist/site/controller-registry.json +23 -3
  6. package/dist/site/kfd-claims.json +92 -11
  7. package/dist/site/kfd-upstream-aggregate.json +1 -1
  8. package/dist/site/manual-registry.json +17 -3
  9. package/dist/site/node-api-registry.json +19 -6
  10. package/dist/site/page-registry.json +77 -9
  11. package/dist/site/public-surface-audit.json +61 -9
  12. package/dist/site/publication-authority-registry.json +19 -1
  13. package/dist/site/publication-registry.json +4 -4
  14. package/dist/site/release-provenance.json +1 -0
  15. package/dist/site/site-manifest.json +15 -7
  16. package/dist/site/workflow-registry.json +52 -5
  17. package/docs/MAP.md +1 -0
  18. package/docs/auditable-demo.md +155 -0
  19. package/docs/github-governance-authority.md +2 -2
  20. package/docs/release-activation-transaction.md +38 -0
  21. package/docs/versioning.md +1 -0
  22. package/package.json +2 -1
  23. package/packages/core/buildchain-config.js +14 -0
  24. package/packages/core/buildchain-contract.js +45 -0
  25. package/packages/core/buildchain-kfd-claims.js +1 -0
  26. package/packages/core/buildchain-publication-authority.js +1 -0
  27. package/packages/core/github-governance-authority.js +3 -3
  28. package/packages/core/index.js +14 -0
  29. package/packages/core/release-activation-transaction.js +421 -0
  30. package/scripts/auditable-demo.mjs +576 -0
  31. package/scripts/check-inventory.mjs +4 -0
  32. package/scripts/generate-site-bundle.mjs +5 -0
  33. package/scripts/installer-publication.mjs +15 -5
  34. package/scripts/publication-commit-evidence.mjs +298 -12
  35. package/scripts/resolve-artifact-coordinates.mjs +174 -0
@@ -61,6 +61,7 @@ repository changes are patch".
61
61
 
62
62
  | Date | Action | Line | Faces | Class | Rationale | PR |
63
63
  | --- | --- | --- | --- | --- | --- | --- |
64
+ | 2026-07-26 | extend-minor | `v3.0` | auditable-demo-workflow, auditable-demo-evidence | additive | Forward-port the consumer-neutral reusable Gate that binds exact same-run GitHub Artifacts to checked-in adapters and immutable renderer evidence, with optional media rendering only from the exact passing Gate bundle. | #1862 |
64
65
  | 2026-07-23 | extend-minor | `v2.14` | credential-island-macos-input, protected-signer-job, macos-signing-evidence, action-subpaths | additive | The reusable build surface can seal an exact source-bound macOS app and hand it to a protected caller environment, where an immutable Buildchain action signs, notarizes, staples, Gatekeeper-assesses, and returns an auditable additional release-candidate platform without exposing credentials to consumer lifecycle jobs. | |
65
66
  | 2026-07-20 | extend-minor | `v2.14` | anchored-derived-version-material, build-controller-evidence, release-passport, package-subpaths, release-propagation-controller | additive | Anchored/manual consumers can declare derived version witnesses that Buildchain regenerates and verifies before heavy builds, binds to exact alpha/release trees and passports, and admits during protected promotion; propagation receipts now model their existing optional consumer stages. | |
66
67
  | 2026-07-17 | extend-minor | `v2.14` | merge-queue-config, release-line-governance-inheritance | additive | Buildchain config can explicitly enable, inherit, or disable exact dev-channel merge queues, and release-line bootstrap reconciles the declared or inherited policy before moving the repository default branch. | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.0",
3
+ "version": "3.0.1-alpha.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -44,6 +44,7 @@
44
44
  "./release-passport": "./packages/core/release-passport.js",
45
45
  "./release-passport-contract": "./packages/core/release-passport-contract.js",
46
46
  "./release-propagation": "./packages/core/release-propagation.js",
47
+ "./release-activation-transaction": "./packages/core/release-activation-transaction.js",
47
48
  "./surface-manifest": "./packages/core/surface-manifest.js",
48
49
  "./buildchain-kfd-claims": "./packages/core/buildchain-kfd-claims.js",
49
50
  "./site/buildchain-site.json": "./dist/site/buildchain-site.json",
@@ -511,6 +511,20 @@ function normalizePublishSection(publish) {
511
511
  ? ""
512
512
  : assertString(publish.main_package, "publish.main_package"),
513
513
  };
514
+ if (publish.shared_alpha_authority_major !== undefined) {
515
+ const sharedAlphaAuthorityMajor = Number(
516
+ publish.shared_alpha_authority_major,
517
+ );
518
+ if (
519
+ !Number.isSafeInteger(sharedAlphaAuthorityMajor) ||
520
+ sharedAlphaAuthorityMajor < 1
521
+ ) {
522
+ throw new Error(
523
+ "publish.shared_alpha_authority_major must be a positive integer",
524
+ );
525
+ }
526
+ normalized.sharedAlphaAuthorityMajor = sharedAlphaAuthorityMajor;
527
+ }
514
528
  if (publish.kind !== undefined) {
515
529
  normalized.kind = kind;
516
530
  }
@@ -301,6 +301,51 @@ export function createBuildchainContractWorld({
301
301
  "channel-aware artifact host facts are checked against the deploy plan before adapter side effects",
302
302
  ],
303
303
  }),
304
+ surface(root, {
305
+ id: "auditable-demo",
306
+ kind: "workflow",
307
+ path: ".github/workflows/.auditable-demo.yml",
308
+ publicRef: `${pkg.repository ? "kungfu-systems/buildchain" : "buildchain"}/.github/workflows/.auditable-demo.yml@${majorLine}`,
309
+ requiredInputs: [
310
+ "source-artifact-name",
311
+ "source-artifact-digest",
312
+ "adapter-path",
313
+ "renderer-image",
314
+ ],
315
+ requiredOutputs: [
316
+ "source-sha",
317
+ "gate-artifact-name",
318
+ "gate-artifact-id",
319
+ "gate-artifact-digest",
320
+ "gate-artifact-url",
321
+ "gate-root",
322
+ "media-artifact-name",
323
+ "media-artifact-id",
324
+ "media-artifact-digest",
325
+ "media-artifact-url",
326
+ "media-root",
327
+ ],
328
+ breakingDefaults: {
329
+ trustedEventRequired: true,
330
+ renderMediaDefault: false,
331
+ artifactRetentionDaysDefault: 14,
332
+ gatePolicy: "required-before-selective-render",
333
+ },
334
+ optionalInputs: [
335
+ "buildchain-repository",
336
+ "source-ref",
337
+ "render-media",
338
+ "artifact-retention-days",
339
+ "require-trusted-event",
340
+ ],
341
+ guarantees: [
342
+ "the source GitHub Artifact is admitted by exact same-run name and archive digest",
343
+ "the checked-in consumer adapter runs from the exact consumer source with a disposable home and reduced environment",
344
+ "every invocation runs an immutable network-disabled renderer smoke before emitting a qualified Gate bundle",
345
+ "complete media rendering is optional and consumes only an exact passing Gate bundle",
346
+ "Gate and media bundles expose both GitHub Artifact archive coordinates and deterministic member roots",
347
+ ],
348
+ }),
304
349
  surface(root, {
305
350
  id: "promote-buildchain-ref-action",
306
351
  kind: "action",
@@ -13,6 +13,7 @@ export const BUILDCHAIN_KFD_COLLABORATION_INTERFACE_CONTRACT = "kungfu-buildchai
13
13
 
14
14
  export const BUILDCHAIN_AGENT_MANUALS = Object.freeze([
15
15
  { id: "map", title: "Buildchain documentation map", path: "docs/MAP.md", plane: "use" },
16
+ { id: "auditable-demo", title: "Auditable demo artifact pipeline", path: "docs/auditable-demo.md", plane: "verify" },
16
17
  { id: "install", title: "Install and verify Buildchain", path: "docs/install.md", plane: "use" },
17
18
  { id: "release-passport", title: "Release Passport protocol", path: "docs/release-passport.md", plane: "verify" },
18
19
  { id: "controller-evidence", title: "Controller evidence contract", path: "docs/controller-evidence.md", plane: "verify" },
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { createPublicationAuthorityRegistry } from "./publication-authority.js";
5
5
 
6
6
  const DESCRIPTORS = Object.freeze([
7
+ [".github/workflows/.auditable-demo.yml", "non-publication-oidc"],
7
8
  [".github/workflows/.build.yml", "non-publication-oidc"],
8
9
  [".github/workflows/.release-candidate-promote.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "caller-bound"],
9
10
  [".github/workflows/.publication-authority.yml", "evidence-publication"],
@@ -46,9 +46,9 @@ const PUBLIC_REPOSITORY_TARGETS = Object.freeze({
46
46
  target("publish-gate/major", [check("check")], true),
47
47
  ],
48
48
  buildchain: [
49
- target("dev/v2/v2.14", [check("check")], false),
50
- target("alpha/v2/v2.14", [check("check"), check("verify")], false),
51
- target("release/v2/v2.14", [check("check")], true),
49
+ target("dev/v3/v3.0", [check("check")], false),
50
+ target("alpha/v3/v3.0", [check("check"), check("verify")], false),
51
+ target("release/v3/v3.0", [check("check")], true),
52
52
  target("publish-gate/major", [check("check")], true),
53
53
  ],
54
54
  "homebrew-tap": [
@@ -521,6 +521,20 @@ export {
521
521
  writeReleasePropagationLock,
522
522
  } from "./release-propagation.js";
523
523
 
524
+ export {
525
+ RELEASE_ACTIVATION_CONTRACT,
526
+ RELEASE_ACTIVATION_PHASES,
527
+ RELEASE_ACTIVATION_RECEIPT_SET_CONTRACT,
528
+ abortReleaseActivationTransaction,
529
+ createReleaseActivationReceiptSet,
530
+ createReleaseActivationTransaction,
531
+ recordReleaseActivationPhase,
532
+ releaseActivationRoot,
533
+ rollbackReleaseActivationTransaction,
534
+ validateReleaseActivationReceiptSet,
535
+ validateReleaseActivationTransaction,
536
+ } from "./release-activation-transaction.js";
537
+
524
538
  export {
525
539
  SURFACE_TIMESTAMP_POLICY_CONTRACT,
526
540
  applySurfaceTimestampPolicy,
@@ -0,0 +1,421 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const RELEASE_ACTIVATION_CONTRACT =
4
+ "kungfu-buildchain-release-activation-transaction/v1";
5
+ export const RELEASE_ACTIVATION_RECEIPT_SET_CONTRACT =
6
+ "kungfu-buildchain-release-activation-receipt-set/v1";
7
+
8
+ export const RELEASE_ACTIVATION_PHASES = Object.freeze([
9
+ "candidate-qualified",
10
+ "artifacts-published",
11
+ "passport-sealed",
12
+ "site-published",
13
+ "public-readback",
14
+ "evidence-synthesized",
15
+ ]);
16
+
17
+ const ROOT = /^sha256:[0-9a-f]{64}$/;
18
+ const SHA = /^[0-9a-f]{40}$/;
19
+ const RECEIPT_KINDS = Object.freeze([
20
+ "artifact-publication",
21
+ "release-passport",
22
+ "site-publication",
23
+ "public-readback",
24
+ "product-qualification",
25
+ ]);
26
+
27
+ function stableJson(value) {
28
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
29
+ if (value && typeof value === "object") {
30
+ return `{${Object.keys(value)
31
+ .sort()
32
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
33
+ .join(",")}}`;
34
+ }
35
+ return JSON.stringify(value);
36
+ }
37
+
38
+ export function releaseActivationRoot(value) {
39
+ const copy = structuredClone(value);
40
+ delete copy.transactionRoot;
41
+ delete copy.receiptSetRoot;
42
+ return `sha256:${crypto.createHash("sha256").update(stableJson(copy)).digest("hex")}`;
43
+ }
44
+
45
+ function requiredString(value, label) {
46
+ const normalized = String(value || "").trim();
47
+ if (!normalized) throw new Error(`${label} must be a non-empty string`);
48
+ return normalized;
49
+ }
50
+
51
+ function exactRoot(value, label) {
52
+ const normalized = requiredString(value, label).toLowerCase();
53
+ if (!ROOT.test(normalized))
54
+ throw new Error(`${label} must be a sha256 content root`);
55
+ return normalized;
56
+ }
57
+
58
+ function exactSha(value, label) {
59
+ const normalized = requiredString(value, label).toLowerCase();
60
+ if (!SHA.test(normalized))
61
+ throw new Error(`${label} must be an exact 40-character Git SHA`);
62
+ return normalized;
63
+ }
64
+
65
+ function normalizeBindings(bindings = {}) {
66
+ const version = requiredString(bindings.version, "bindings.version");
67
+ const tag = requiredString(bindings.tag, "bindings.tag");
68
+ if (tag !== `v${version}`)
69
+ throw new Error("bindings.tag must exactly match bindings.version");
70
+ const channel = requiredString(bindings.channel, "bindings.channel");
71
+ if (!["alpha", "release"].includes(channel)) {
72
+ throw new Error("bindings.channel must be alpha or release");
73
+ }
74
+ const environment = requiredString(
75
+ bindings.environment,
76
+ "bindings.environment",
77
+ );
78
+ if (!["shadow", "production"].includes(environment)) {
79
+ throw new Error("bindings.environment must be shadow or production");
80
+ }
81
+ return {
82
+ sourceSha: exactSha(bindings.sourceSha, "bindings.sourceSha"),
83
+ siteSourceSha: exactSha(bindings.siteSourceSha, "bindings.siteSourceSha"),
84
+ tag,
85
+ channel,
86
+ version,
87
+ environment,
88
+ artifactSetRoot: exactRoot(
89
+ bindings.artifactSetRoot,
90
+ "bindings.artifactSetRoot",
91
+ ),
92
+ };
93
+ }
94
+
95
+ function normalizeOwners(owners = {}) {
96
+ return {
97
+ product: requiredString(owners.product, "owners.product"),
98
+ transaction: requiredString(owners.transaction, "owners.transaction"),
99
+ site: requiredString(owners.site, "owners.site"),
100
+ };
101
+ }
102
+
103
+ export function createReleaseActivationTransaction({
104
+ transactionId,
105
+ mode = "shadow",
106
+ bindings,
107
+ owners,
108
+ } = {}) {
109
+ if (!["shadow", "activation"].includes(mode)) {
110
+ throw new Error("mode must be shadow or activation");
111
+ }
112
+ const normalizedBindings = normalizeBindings(bindings);
113
+ if (mode === "shadow" && normalizedBindings.environment !== "shadow") {
114
+ throw new Error("shadow transactions must use the shadow environment");
115
+ }
116
+ if (
117
+ mode === "activation" &&
118
+ normalizedBindings.environment !== "production"
119
+ ) {
120
+ throw new Error(
121
+ "activation transactions must use the production environment",
122
+ );
123
+ }
124
+ const transaction = {
125
+ schema: RELEASE_ACTIVATION_CONTRACT,
126
+ transactionId: requiredString(transactionId, "transactionId"),
127
+ mode,
128
+ state: "open",
129
+ releasedUseClaim: false,
130
+ bindings: normalizedBindings,
131
+ owners: normalizeOwners(owners),
132
+ phases: RELEASE_ACTIVATION_PHASES.map((id) => ({
133
+ id,
134
+ status: "pending",
135
+ attempts: 0,
136
+ receiptRoots: [],
137
+ })),
138
+ retainedReceiptRoots: [],
139
+ failure: null,
140
+ rollback: null,
141
+ };
142
+ transaction.transactionRoot = releaseActivationRoot(transaction);
143
+ return transaction;
144
+ }
145
+
146
+ export function validateReleaseActivationTransaction(transaction) {
147
+ const issues = [];
148
+ if (transaction?.schema !== RELEASE_ACTIVATION_CONTRACT) {
149
+ issues.push(`schema must be ${RELEASE_ACTIVATION_CONTRACT}`);
150
+ }
151
+ try {
152
+ normalizeBindings(transaction?.bindings);
153
+ normalizeOwners(transaction?.owners);
154
+ } catch (error) {
155
+ issues.push(error.message);
156
+ }
157
+ if (!["shadow", "activation"].includes(transaction?.mode))
158
+ issues.push("mode is invalid");
159
+ if (
160
+ !["open", "complete", "aborted", "rolled-back"].includes(
161
+ transaction?.state,
162
+ )
163
+ ) {
164
+ issues.push("state is invalid");
165
+ }
166
+ if (
167
+ !Array.isArray(transaction?.phases) ||
168
+ transaction.phases.map((phase) => phase.id).join(",") !==
169
+ RELEASE_ACTIVATION_PHASES.join(",")
170
+ ) {
171
+ issues.push("phases must preserve the canonical activation order");
172
+ } else {
173
+ let pendingSeen = false;
174
+ for (const phase of transaction.phases) {
175
+ if (!["pending", "passed", "failed", "rolled-back"].includes(phase.status)) {
176
+ issues.push(`phase ${phase.id} has invalid status`);
177
+ }
178
+ if (phase.status === "pending") pendingSeen = true;
179
+ if (pendingSeen && phase.status === "passed") {
180
+ issues.push(`phase ${phase.id} passed after an incomplete predecessor`);
181
+ }
182
+ if (!Number.isSafeInteger(phase.attempts) || phase.attempts < 0) {
183
+ issues.push(
184
+ `phase ${phase.id} attempts must be a non-negative integer`,
185
+ );
186
+ }
187
+ if (
188
+ !Array.isArray(phase.receiptRoots) ||
189
+ phase.receiptRoots.some((root) => !ROOT.test(root))
190
+ ) {
191
+ issues.push(`phase ${phase.id} receipt roots are invalid`);
192
+ }
193
+ }
194
+ }
195
+ if (
196
+ transaction?.mode === "shadow" &&
197
+ transaction?.releasedUseClaim !== false
198
+ ) {
199
+ issues.push("shadow transactions must never make a released-use claim");
200
+ }
201
+ if (
202
+ transaction?.transactionRoot !== releaseActivationRoot(transaction || {})
203
+ ) {
204
+ issues.push("transactionRoot mismatch");
205
+ }
206
+ return { valid: issues.length === 0, issues };
207
+ }
208
+
209
+ function refresh(transaction) {
210
+ transaction.retainedReceiptRoots = [
211
+ ...new Set(transaction.phases.flatMap((phase) => phase.receiptRoots)),
212
+ ].sort();
213
+ transaction.transactionRoot = releaseActivationRoot(transaction);
214
+ return transaction;
215
+ }
216
+
217
+ export function recordReleaseActivationPhase(
218
+ transaction,
219
+ phaseId,
220
+ { receiptRoots = [], failure = "" } = {},
221
+ ) {
222
+ const validation = validateReleaseActivationTransaction(transaction);
223
+ if (!validation.valid)
224
+ throw new Error(
225
+ `invalid activation transaction: ${validation.issues.join("; ")}`,
226
+ );
227
+ if (transaction.state !== "open") {
228
+ if (transaction.state === "complete" && !failure)
229
+ return structuredClone(transaction);
230
+ throw new Error(
231
+ `cannot record a phase while transaction state is ${transaction.state}`,
232
+ );
233
+ }
234
+ const next = structuredClone(transaction);
235
+ const index = RELEASE_ACTIVATION_PHASES.indexOf(phaseId);
236
+ if (index < 0) throw new Error(`unknown activation phase: ${phaseId}`);
237
+ if (
238
+ next.phases.slice(0, index).some((phase) => phase.status !== "passed")
239
+ ) {
240
+ throw new Error(
241
+ `activation phase ${phaseId} cannot skip an incomplete predecessor`,
242
+ );
243
+ }
244
+ const phase = next.phases[index];
245
+ const normalizedRoots = [
246
+ ...new Set(
247
+ receiptRoots.map((root, receiptIndex) =>
248
+ exactRoot(root, `receiptRoots[${receiptIndex}]`),
249
+ ),
250
+ ),
251
+ ].sort();
252
+ if (phase.status === "passed") {
253
+ if (stableJson(phase.receiptRoots) !== stableJson(normalizedRoots)) {
254
+ throw new Error(
255
+ `activation phase ${phaseId} replay changed receipt roots`,
256
+ );
257
+ }
258
+ return next;
259
+ }
260
+ phase.attempts += 1;
261
+ phase.receiptRoots = normalizedRoots;
262
+ if (failure) {
263
+ phase.status = "failed";
264
+ next.failure = {
265
+ phase: phaseId,
266
+ reason: requiredString(failure, "failure"),
267
+ };
268
+ return refresh(next);
269
+ }
270
+ phase.status = "passed";
271
+ next.failure = null;
272
+ if (index === RELEASE_ACTIVATION_PHASES.length - 1)
273
+ next.state = "complete";
274
+ return refresh(next);
275
+ }
276
+
277
+ export function abortReleaseActivationTransaction(transaction, reason) {
278
+ if (transaction.state === "complete")
279
+ throw new Error("completed activation transactions cannot be aborted");
280
+ const next = structuredClone(transaction);
281
+ next.state = "aborted";
282
+ next.failure = {
283
+ phase:
284
+ next.phases.find((phase) => phase.status !== "passed")?.id || "",
285
+ reason: requiredString(reason, "reason"),
286
+ };
287
+ return refresh(next);
288
+ }
289
+
290
+ export function rollbackReleaseActivationTransaction(
291
+ transaction,
292
+ { toSiteSourceSha, reason } = {},
293
+ ) {
294
+ if (!["complete", "aborted"].includes(transaction.state)) {
295
+ throw new Error(
296
+ "rollback requires a complete or aborted activation transaction",
297
+ );
298
+ }
299
+ const next = structuredClone(transaction);
300
+ next.state = "rolled-back";
301
+ next.rollback = {
302
+ toSiteSourceSha: exactSha(toSiteSourceSha, "toSiteSourceSha"),
303
+ reason: requiredString(reason, "reason"),
304
+ };
305
+ for (const phase of next.phases.slice(3).reverse()) {
306
+ if (phase.status === "passed") phase.status = "rolled-back";
307
+ }
308
+ return refresh(next);
309
+ }
310
+
311
+ export function createReleaseActivationReceiptSet({
312
+ transaction,
313
+ receipts = [],
314
+ } = {}) {
315
+ const validation = validateReleaseActivationTransaction(transaction);
316
+ if (!validation.valid)
317
+ throw new Error(
318
+ `invalid activation transaction: ${validation.issues.join("; ")}`,
319
+ );
320
+ if (transaction.state !== "complete")
321
+ throw new Error(
322
+ "receipt synthesis requires a complete activation transaction",
323
+ );
324
+ const byKind = new Map();
325
+ for (const [index, receipt] of receipts.entries()) {
326
+ const kind = requiredString(receipt?.kind, `receipts[${index}].kind`);
327
+ if (!RECEIPT_KINDS.includes(kind))
328
+ throw new Error(`unsupported activation receipt kind: ${kind}`);
329
+ if (byKind.has(kind))
330
+ throw new Error(`duplicate activation receipt kind: ${kind}`);
331
+ byKind.set(kind, {
332
+ kind,
333
+ root: exactRoot(receipt.root, `receipts[${index}].root`),
334
+ bindingRoot: exactRoot(
335
+ receipt.bindingRoot,
336
+ `receipts[${index}].bindingRoot`,
337
+ ),
338
+ locator: requiredString(
339
+ receipt.locator,
340
+ `receipts[${index}].locator`,
341
+ ),
342
+ });
343
+ }
344
+ for (const kind of RECEIPT_KINDS) {
345
+ if (!byKind.has(kind))
346
+ throw new Error(`activation receipt set is missing ${kind}`);
347
+ }
348
+ const bindingRoot = releaseActivationRoot(transaction.bindings);
349
+ if (
350
+ [...byKind.values()].some(
351
+ (receipt) => receipt.bindingRoot !== bindingRoot,
352
+ )
353
+ ) {
354
+ throw new Error(
355
+ "activation receipts do not bind the exact transaction inputs",
356
+ );
357
+ }
358
+ const receiptSet = {
359
+ schema: RELEASE_ACTIVATION_RECEIPT_SET_CONTRACT,
360
+ transactionId: transaction.transactionId,
361
+ transactionRoot: transaction.transactionRoot,
362
+ mode: transaction.mode,
363
+ releasedUseClaim: transaction.mode === "activation",
364
+ bindings: structuredClone(transaction.bindings),
365
+ receipts: RECEIPT_KINDS.map((kind) => byKind.get(kind)),
366
+ legalBoundary: {
367
+ firstUseDateClaim: null,
368
+ legalConclusion: "not-made",
369
+ registrationStatusClaim: "none",
370
+ },
371
+ };
372
+ receiptSet.receiptSetRoot = releaseActivationRoot(receiptSet);
373
+ return receiptSet;
374
+ }
375
+
376
+ export function validateReleaseActivationReceiptSet(
377
+ receiptSet,
378
+ { allowShadow = true } = {},
379
+ ) {
380
+ const issues = [];
381
+ if (receiptSet?.schema !== RELEASE_ACTIVATION_RECEIPT_SET_CONTRACT) {
382
+ issues.push(`schema must be ${RELEASE_ACTIVATION_RECEIPT_SET_CONTRACT}`);
383
+ }
384
+ try {
385
+ normalizeBindings(receiptSet?.bindings);
386
+ } catch (error) {
387
+ issues.push(error.message);
388
+ }
389
+ if (!allowShadow && receiptSet?.mode !== "activation")
390
+ issues.push("released evidence requires activation mode");
391
+ if (
392
+ receiptSet?.mode === "shadow" &&
393
+ receiptSet?.releasedUseClaim !== false
394
+ ) {
395
+ issues.push("shadow receipt sets must not claim released use");
396
+ }
397
+ const receipts = Array.isArray(receiptSet?.receipts)
398
+ ? receiptSet.receipts
399
+ : [];
400
+ if (
401
+ receipts.map((receipt) => receipt.kind).join(",") !==
402
+ RECEIPT_KINDS.join(",")
403
+ ) {
404
+ issues.push(
405
+ "receipt kinds are missing, duplicated, or out of canonical order",
406
+ );
407
+ }
408
+ const bindingRoot = releaseActivationRoot(receiptSet?.bindings || {});
409
+ for (const receipt of receipts) {
410
+ if (!ROOT.test(receipt?.root || ""))
411
+ issues.push(`${receipt?.kind || "receipt"} root is invalid`);
412
+ if (receipt?.bindingRoot !== bindingRoot)
413
+ issues.push(`${receipt?.kind || "receipt"} binding root mismatch`);
414
+ if (!String(receipt?.locator || "").trim())
415
+ issues.push(`${receipt?.kind || "receipt"} locator is missing`);
416
+ }
417
+ if (receiptSet?.receiptSetRoot !== releaseActivationRoot(receiptSet || {})) {
418
+ issues.push("receiptSetRoot mismatch");
419
+ }
420
+ return { valid: issues.length === 0, issues };
421
+ }