@kungfu-tech/buildchain 4.1.3-alpha.8 → 4.1.3

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.
@@ -10814,8 +10814,8 @@
10814
10814
  "workflowRegistryPath": "dist/site/workflow-registry.json",
10815
10815
  "pageRegistryPath": "dist/site/page-registry.json",
10816
10816
  "cliRegistryDigest": "e43fdb0289df88871f3f0febbe04dcb9311a252a2b1df9b3a83a2d10c9d4a70f",
10817
- "workflowRegistryDigest": "380c2eb00655d9a8928765aa836dd37b99543d492daadc50fb9c8faf94bb9469",
10818
- "pageRegistryDigest": "80d9a72bc3833c1f9ec4ad39f3dbab4ebce9b51c795d390a3526acf904054eb7"
10817
+ "workflowRegistryDigest": "b64f87652d2377f6f1cd192bb3e0983f6885af60f3522915649f6a2dc40610fc",
10818
+ "pageRegistryDigest": "1e79a71c8f1d094ad1291b2d8b4bc983633245dcc104ae5ef7bae755fb733209"
10819
10819
  },
10820
10820
  "comparison": {
10821
10821
  "missingCliRegistry": [],
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "4.1.3-alpha.8",
35
+ "version": "4.1.3",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "4.1.3-alpha.8",
40
+ "version": "4.1.3",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -253,7 +253,7 @@
253
253
  "path": "docs/node-api-reference.md",
254
254
  "plane": "use",
255
255
  "exists": true,
256
- "digest": "sha256:176c523f579e4b2221f2e323c35d4a5a803d522b8e4f9eeb252a25481688c09a"
256
+ "digest": "sha256:5c00176bff01621ea48e2d35fbea8c34810963eda6a7413a1819ca3fae529f6c"
257
257
  },
258
258
  {
259
259
  "id": "build-facts",
@@ -1263,7 +1263,7 @@
1263
1263
  "status": "preview",
1264
1264
  "owner": "buildchain-workflows",
1265
1265
  "maturity": "preview",
1266
- "introducedVersion": "4.1.3-alpha.8",
1266
+ "introducedVersion": "4.1.3",
1267
1267
  "compatibilityPromise": "current-contract-only",
1268
1268
  "deprecationReplacement": "",
1269
1269
  "sunsetCondition": "explicit-contract-change-review",
@@ -2351,18 +2351,18 @@ Target: `./packages/core/release/next-development-transition.js`. Public symbols
2351
2351
 
2352
2352
  | Symbol | Kind and signature | Parameters | Return | Errors | Side effects | Example | Source |
2353
2353
  | --- | --- | --- | --- | --- | --- | --- | --- |
2354
- | `advanceNextDevelopmentTransition` | function: function advanceNextDevelopmentTransition(record, input = {}) | record, input = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { advanceNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:343` |
2355
- | `bindNextDevelopmentAnchor` | function: function bindNextDevelopmentAnchor(record, input = {}) | record, input = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { bindNextDevelopmentAnchor } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:304` |
2356
- | `createNextDevelopmentTransition` | function: function createNextDevelopmentTransition(input = {}) | input = {} | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { createNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:252` |
2357
- | `materializeNextDevelopmentTransition` | function: function materializeNextDevelopmentTransition({ cwd = process.cwd(), request, write = false, } = {}) | { cwd = process.cwd(), request, write = false, } = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | local-filesystem-write | `import { materializeNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:471` |
2358
- | `NEXT_DEVELOPMENT_ADR` | constant: const NEXT_DEVELOPMENT_ADR | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_ADR } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:20` |
2359
- | `NEXT_DEVELOPMENT_INVARIANT` | constant: const NEXT_DEVELOPMENT_INVARIANT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_INVARIANT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:22` |
2360
- | `NEXT_DEVELOPMENT_REQUEST_CONTRACT` | constant: const NEXT_DEVELOPMENT_REQUEST_CONTRACT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_REQUEST_CONTRACT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:18` |
2361
- | `NEXT_DEVELOPMENT_STATES` | constant: const NEXT_DEVELOPMENT_STATES | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_STATES } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:24` |
2362
- | `NEXT_DEVELOPMENT_TRANSITION_CONTRACT` | constant: const NEXT_DEVELOPMENT_TRANSITION_CONTRACT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_TRANSITION_CONTRACT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:16` |
2363
- | `NEXT_DEVELOPMENT_VERSION_MODELS` | constant: const NEXT_DEVELOPMENT_VERSION_MODELS | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_VERSION_MODELS } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:32` |
2364
- | `nextDevelopmentRoot` | function: function nextDevelopmentRoot(value) | value | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { nextDevelopmentRoot } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:63` |
2365
- | `recordNextDevelopmentMaterialization` | function: function recordNextDevelopmentMaterialization(record, { materialization, evidenceRoot, recordedAt } = {}) | record, { materialization, evidenceRoot, recordedAt } = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | may-write-or-invoke-external-actions | `import { recordNextDevelopmentMaterialization } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:415` |
2354
+ | `advanceNextDevelopmentTransition` | function: function advanceNextDevelopmentTransition(record, input = {}) | record, input = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { advanceNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:344` |
2355
+ | `bindNextDevelopmentAnchor` | function: function bindNextDevelopmentAnchor(record, input = {}) | record, input = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { bindNextDevelopmentAnchor } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:305` |
2356
+ | `createNextDevelopmentTransition` | function: function createNextDevelopmentTransition(input = {}) | input = {} | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { createNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:253` |
2357
+ | `materializeNextDevelopmentTransition` | function: function materializeNextDevelopmentTransition({ cwd = process.cwd(), request, write = false, } = {}) | { cwd = process.cwd(), request, write = false, } = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | local-filesystem-write | `import { materializeNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:472` |
2358
+ | `NEXT_DEVELOPMENT_ADR` | constant: const NEXT_DEVELOPMENT_ADR | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_ADR } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:21` |
2359
+ | `NEXT_DEVELOPMENT_INVARIANT` | constant: const NEXT_DEVELOPMENT_INVARIANT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_INVARIANT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:23` |
2360
+ | `NEXT_DEVELOPMENT_REQUEST_CONTRACT` | constant: const NEXT_DEVELOPMENT_REQUEST_CONTRACT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_REQUEST_CONTRACT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:19` |
2361
+ | `NEXT_DEVELOPMENT_STATES` | constant: const NEXT_DEVELOPMENT_STATES | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_STATES } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:25` |
2362
+ | `NEXT_DEVELOPMENT_TRANSITION_CONTRACT` | constant: const NEXT_DEVELOPMENT_TRANSITION_CONTRACT | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_TRANSITION_CONTRACT } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:17` |
2363
+ | `NEXT_DEVELOPMENT_VERSION_MODELS` | constant: const NEXT_DEVELOPMENT_VERSION_MODELS | none | value | Import does not declare a throw contract. | none-on-import | `import { NEXT_DEVELOPMENT_VERSION_MODELS } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:33` |
2364
+ | `nextDevelopmentRoot` | function: function nextDevelopmentRoot(value) | value | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { nextDevelopmentRoot } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:64` |
2365
+ | `recordNextDevelopmentMaterialization` | function: function recordNextDevelopmentMaterialization(record, { materialization, evidenceRoot, recordedAt } = {}) | record, { materialization, evidenceRoot, recordedAt } = {} | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | may-write-or-invoke-external-actions | `import { recordNextDevelopmentMaterialization } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:416` |
2366
2366
  | `validateNextDevelopmentTransition` | function: function validateNextDevelopmentTransition(record) | record | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { validateNextDevelopmentTransition } from "@kungfu-tech/buildchain/next-development-transition";` | `packages/core/release/next-development-transition.js:728` |
2367
2367
 
2368
2368
  ## `@kungfu-tech/buildchain/next-development-controller`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "4.1.3-alpha.8",
3
+ "version": "4.1.3",
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",
@@ -1,5 +1,4 @@
1
- import { createHash } from "node:crypto";
2
- import { recordDigest } from "../../release/discussion/envelope.js";
1
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
3
2
 
4
3
  const NAMES = [
5
4
  "plan",
@@ -9,103 +8,23 @@ const NAMES = [
9
8
  "attestation",
10
9
  "release",
11
10
  ];
12
- const LIMIT = 8 * 1024 * 1024;
13
-
14
- async function inventory(request, prefix) {
15
- const assets = [],
16
- ids = new Set(),
17
- names = new Set();
18
- for (let page = 1; page <= 20; page++) {
19
- const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
20
- if (!Array.isArray(values) || values.length > 100)
21
- throw new Error("Published evidence asset inventory is incomplete");
22
- for (const value of values) {
23
- if (
24
- !Number.isSafeInteger(value.id) ||
25
- value.id < 1 ||
26
- typeof value.name !== "string" ||
27
- !value.name ||
28
- ids.has(value.id) ||
29
- names.has(value.name)
30
- )
31
- throw new Error("Published evidence asset inventory is ambiguous");
32
- ids.add(value.id);
33
- names.add(value.name);
34
- assets.push(value);
35
- }
36
- if (values.length < 100) return assets;
37
- }
38
- throw new Error(
39
- "Published evidence inventory exceeds its complete-read bound",
40
- );
41
- }
42
-
43
- function selectedAssets(assets) {
44
- return NAMES.map((id) => {
45
- const name = `buildchain.${id}.json`;
46
- const asset = assets.find((value) => value.name === name);
47
- if (!asset) return { name, missing: true };
48
- if (
49
- asset.state !== "uploaded" ||
50
- !Number.isSafeInteger(asset.size) ||
51
- asset.size < 1 ||
52
- asset.size > LIMIT ||
53
- !/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
54
- )
55
- throw new Error("Published evidence asset lacks bounded immutable bytes");
56
- return { id: asset.id, name, size: asset.size, digest: asset.digest };
57
- });
58
- }
59
-
60
- // Download only fixed evidence names from the exact already-observed release.
61
- // There is no caller-supplied URL, artifact selector or provider write here.
62
11
  export async function readPipelineReleaseEvidence(host, candidate) {
63
- const repository = host.repository;
64
- if (
65
- !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
66
- !Number.isSafeInteger(candidate.id) ||
67
- candidate.id < 1
68
- )
69
- throw new Error("Published evidence requires exact release coordinates");
70
- const prefix = `/repos/${repository}/releases/${candidate.id}`;
71
- const selected = selectedAssets(await inventory(host.request, prefix));
72
- const missing = selected
73
- .filter((value) => value.missing)
74
- .map(({ name }) => name);
75
- if (missing.length) return { status: "missing", missing };
76
- const [owner, repo] = repository.split("/");
12
+ const result = await readReleaseEvidenceAssets(
13
+ host,
14
+ candidate,
15
+ NAMES.map((id) => `buildchain.${id}.json`),
16
+ );
17
+ if (result.status !== "present") return result;
77
18
  const values = {};
78
- let bundle;
79
- for (const asset of selected) {
80
- const result = await host.github.rest.repos.getReleaseAsset({
81
- owner,
82
- repo,
83
- asset_id: asset.id,
84
- headers: { accept: "application/octet-stream" },
85
- });
86
- const bytes = Buffer.from(result.data);
87
- if (
88
- bytes.length !== asset.size ||
89
- bytes.length > LIMIT ||
90
- `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
91
- asset.digest
92
- )
93
- throw new Error(
94
- "Published evidence download differs from provider bytes",
95
- );
96
- const id = asset.name.slice("buildchain.".length, -".json".length);
97
- if (id === "attestation") bundle = bytes;
98
- else
99
- values[id] = JSON.parse(
100
- new TextDecoder("utf-8", { fatal: true }).decode(bytes),
101
- );
102
- }
103
- if (
104
- recordDigest(selectedAssets(await inventory(host.request, prefix))) !==
105
- recordDigest(selected)
106
- )
107
- throw new Error(
108
- "Published evidence assets changed during provider readback",
19
+ for (const id of NAMES.filter((name) => name !== "attestation"))
20
+ values[id] = JSON.parse(
21
+ new TextDecoder("utf-8", { fatal: true }).decode(
22
+ result.values[`buildchain.${id}.json`],
23
+ ),
109
24
  );
110
- return { status: "present", values, bundle, assets: selected };
25
+ return {
26
+ ...result,
27
+ values,
28
+ bundle: result.values["buildchain.attestation.json"],
29
+ };
111
30
  }
@@ -0,0 +1,86 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ SETTLEMENT_ASSET,
4
+ verifyPublicationSettlement,
5
+ } from "../../publication/settlement/transaction.js";
6
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
7
+
8
+ // Earlier publications materialized the floating entry after the exact tag.
9
+ // Only their completed immutable settlement can bind those two commits.
10
+ export async function readPipelineStableBaseline(plan, host, release, prior) {
11
+ if ((prior?.commit || null) === plan.previousChannelCommit) return null;
12
+ if (!release || !prior || !plan.previousChannelCommit)
13
+ throw new Error(
14
+ "Stable comparison differs from the retained published channel",
15
+ );
16
+ const evidence = await readReleaseEvidenceAssets(host, release, [
17
+ SETTLEMENT_ASSET,
18
+ "buildchain.release.json",
19
+ ]);
20
+ if (evidence.status !== "present")
21
+ throw new Error(
22
+ "Stable channel differs without a completed publication settlement",
23
+ );
24
+ const read = (name) =>
25
+ JSON.parse(
26
+ new TextDecoder("utf-8", { fatal: true }).decode(evidence.values[name]),
27
+ );
28
+ const settlement = read(SETTLEMENT_ASSET),
29
+ publicPassport = read("buildchain.release.json"),
30
+ repository = plan.source.repository,
31
+ tag = release.tag,
32
+ sourceSha = prior.commit;
33
+ if (
34
+ settlement.schemaVersion !== 1 ||
35
+ settlement.contract !== "buildchain-v4-publication-settlement/v1" ||
36
+ settlement.release?.tag !== tag ||
37
+ settlement.release?.sourceSha !== sourceSha ||
38
+ settlement.release?.channel !== "stable"
39
+ )
40
+ throw new Error(
41
+ "Stable baseline settlement has different release coordinates",
42
+ );
43
+ const verified = verifyPublicationSettlement(settlement.documents, {
44
+ repository,
45
+ tag,
46
+ sourceSha,
47
+ publicPassport,
48
+ });
49
+ const { product, providerState, productState, invocation, passport } =
50
+ settlement.documents;
51
+ const ref = `refs/tags/v${plan.version.split(".")[0]}`,
52
+ updates = product.updates.filter((value) => value.ref === ref);
53
+ const subject = {
54
+ repository,
55
+ sourceSha,
56
+ tag,
57
+ version: tag.slice(1),
58
+ channel: "stable",
59
+ };
60
+ if (
61
+ invocation.target.channel !== "stable" ||
62
+ passport.release.channel !== "stable" ||
63
+ product.promotedSha !== plan.previousChannelCommit ||
64
+ updates.length !== 1 ||
65
+ updates[0].action !== "converged-release-tag" ||
66
+ updates[0].sha !== plan.previousChannelCommit ||
67
+ recordDigest(productState.subject) !== recordDigest(subject) ||
68
+ recordDigest(providerState.subject) !==
69
+ recordDigest({
70
+ ...subject,
71
+ sourceSha: plan.previousChannelCommit,
72
+ })
73
+ )
74
+ throw new Error(
75
+ "Stable baseline settlement does not bind the retained floating channel",
76
+ );
77
+ return {
78
+ releaseId: release.id,
79
+ tag,
80
+ sourceSha,
81
+ channelRef: ref,
82
+ channelCommit: plan.previousChannelCommit,
83
+ receiptRoot: verified.receiptRoot,
84
+ assets: evidence.assets,
85
+ };
86
+ }
@@ -5,6 +5,7 @@ import { compareDevelopmentVersions } from "../../publication/publication-develo
5
5
  import { githubPipelineSource } from "./pipeline-source.js";
6
6
  import { githubPipelineVersion } from "./pipeline-version.js";
7
7
  import { normalInputs } from "../../consumer/contract/entries.js";
8
+ import { readPipelineStableBaseline } from "./pipeline-stable-baseline.js";
8
9
 
9
10
  const SHA = /^[0-9a-f]{40}$/u;
10
11
  const STABLE = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
@@ -295,10 +296,12 @@ export async function readPipelineStableSource(plan, host) {
295
296
  const prior = selected.comparison
296
297
  ? await exactTag(get, selected.comparison.tag)
297
298
  : null;
298
- if ((prior?.commit || null) !== plan.previousChannelCommit)
299
- throw new Error(
300
- "Stable comparison differs from the retained published channel",
301
- );
299
+ const baseline = await readPipelineStableBaseline(
300
+ plan,
301
+ { ...host, request },
302
+ selected.comparison,
303
+ prior,
304
+ );
302
305
  const current = await treeFiles(get, pointer.commit);
303
306
  if (current.tree !== candidate.identity.tree)
304
307
  throw new Error(
@@ -335,6 +338,20 @@ export async function readPipelineStableSource(plan, host) {
335
338
  prior,
336
339
  latest,
337
340
  });
341
+ if (
342
+ baseline &&
343
+ recordDigest(
344
+ await readPipelineStableBaseline(
345
+ plan,
346
+ { ...host, request },
347
+ selected.comparison,
348
+ prior,
349
+ ),
350
+ ) !== recordDigest(baseline)
351
+ )
352
+ throw new Error(
353
+ "Stable baseline evidence changed during provider readback",
354
+ );
338
355
  const body = {
339
356
  schema: "buildchain.pipeline-stable-source/v1",
340
357
  planRoot: plan.root,
@@ -347,6 +364,7 @@ export async function readPipelineStableSource(plan, host) {
347
364
  comparisonStable: selected.comparison
348
365
  ? { ...selected.comparison, sha: prior.commit, tree: previous.tree }
349
366
  : null,
367
+ ...(baseline ? { comparisonChannel: baseline } : {}),
350
368
  impact,
351
369
  changedPaths: changedPaths(previous.files, current.files),
352
370
  };
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ const LIMIT = 8 * 1024 * 1024;
5
+
6
+ async function inventory(request, prefix) {
7
+ const assets = [],
8
+ ids = new Set(),
9
+ names = new Set();
10
+ for (let page = 1; page <= 20; page++) {
11
+ const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
12
+ if (!Array.isArray(values) || values.length > 100)
13
+ throw new Error("Published evidence asset inventory is incomplete");
14
+ for (const value of values) {
15
+ if (
16
+ !Number.isSafeInteger(value.id) ||
17
+ value.id < 1 ||
18
+ typeof value.name !== "string" ||
19
+ !value.name ||
20
+ ids.has(value.id) ||
21
+ names.has(value.name)
22
+ )
23
+ throw new Error("Published evidence asset inventory is ambiguous");
24
+ ids.add(value.id);
25
+ names.add(value.name);
26
+ assets.push(value);
27
+ }
28
+ if (values.length < 100) return assets;
29
+ }
30
+ throw new Error(
31
+ "Published evidence inventory exceeds its complete-read bound",
32
+ );
33
+ }
34
+
35
+ function selectedAssets(assets, names) {
36
+ return names.map((name) => {
37
+ const asset = assets.find((value) => value.name === name);
38
+ if (!asset) return { name, missing: true };
39
+ if (
40
+ asset.state !== "uploaded" ||
41
+ !Number.isSafeInteger(asset.size) ||
42
+ asset.size < 1 ||
43
+ asset.size > LIMIT ||
44
+ !/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
45
+ )
46
+ throw new Error("Published evidence asset lacks bounded immutable bytes");
47
+ return { id: asset.id, name, size: asset.size, digest: asset.digest };
48
+ });
49
+ }
50
+
51
+ // Download only fixed evidence names from the exact already-observed release.
52
+ // There is no caller-supplied URL, artifact selector or provider write here.
53
+ export async function readReleaseEvidenceAssets(host, candidate, names) {
54
+ const repository = host.repository;
55
+ if (
56
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
57
+ !Number.isSafeInteger(candidate.id) ||
58
+ candidate.id < 1
59
+ )
60
+ throw new Error("Published evidence requires exact release coordinates");
61
+ const prefix = `/repos/${repository}/releases/${candidate.id}`;
62
+ const selected = selectedAssets(await inventory(host.request, prefix), names);
63
+ const missing = selected
64
+ .filter((value) => value.missing)
65
+ .map(({ name }) => name);
66
+ if (missing.length) return { status: "missing", missing };
67
+ const [owner, repo] = repository.split("/");
68
+ const values = {};
69
+ for (const asset of selected) {
70
+ const result = await host.github.rest.repos.getReleaseAsset({
71
+ owner,
72
+ repo,
73
+ asset_id: asset.id,
74
+ headers: { accept: "application/octet-stream" },
75
+ });
76
+ const bytes = Buffer.from(result.data);
77
+ if (
78
+ bytes.length !== asset.size ||
79
+ bytes.length > LIMIT ||
80
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
81
+ asset.digest
82
+ )
83
+ throw new Error(
84
+ "Published evidence download differs from provider bytes",
85
+ );
86
+ values[asset.name] = bytes;
87
+ }
88
+ if (
89
+ recordDigest(
90
+ selectedAssets(await inventory(host.request, prefix), names),
91
+ ) !== recordDigest(selected)
92
+ )
93
+ throw new Error(
94
+ "Published evidence assets changed during provider readback",
95
+ );
96
+ return { status: "present", values, assets: selected };
97
+ }
@@ -106,5 +106,17 @@ export function pipelinePublicationJournal(session, host) {
106
106
  });
107
107
  return value;
108
108
  }
109
- return { fence, materials, record };
109
+ async function recordImport(value, phase) {
110
+ publicationImportedValues(value);
111
+ const observed = await fence();
112
+ const id = "publication/recovery-import";
113
+ const eventKey = `publication:${id}:${recordDigest(value)}`;
114
+ const prior = observed.history
115
+ .at(-1)
116
+ .events.find((event) => event.payload.eventKey === eventKey);
117
+ // Import is one atomic append, not a new result for each resumed phase.
118
+ // Replay still verifies the original event, state and retained bytes.
119
+ return record(id, value, { phase: prior?.node || phase });
120
+ }
121
+ return { fence, materials, record, recordImport };
110
122
  }
@@ -76,5 +76,5 @@ export async function importRecoveredPublication(
76
76
  publicationImportedValues(bundle);
77
77
  // One journal append publishes the complete import and derived source/plan.
78
78
  // An interruption cannot expose a new plan beside an old materialization.
79
- await journal.record("publication/recovery-import", bundle, { phase });
79
+ await journal.recordImport(bundle, phase);
80
80
  }
@@ -114,7 +114,11 @@ async function inspectEntryRun(listed, plan, source, host) {
114
114
  const consumer = await pipelineEntryRun(run, plan, source, host);
115
115
  if (!consumer) return null;
116
116
  if (
117
- !jobs.some((job) => /(?:^| \/ )Build product \([^)]+\)$/u.test(job.name)) &&
117
+ !jobs.some(
118
+ (job) =>
119
+ job.conclusion !== "skipped" &&
120
+ /(?:^| \/ )Build product \([^)]+\)$/u.test(job.name),
121
+ ) &&
118
122
  run.conclusion === "success"
119
123
  )
120
124
  return null;
@@ -3,6 +3,7 @@
3
3
  import crypto from "node:crypto";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import { isDeepStrictEqual } from "node:util";
6
7
 
7
8
  import {
8
9
  discoverConfiguredDerivedVersionMaterial,
@@ -606,7 +607,7 @@ function assertStaticTransition(record, recreated) {
606
607
  throw new Error(`next-development ${field} drifted`);
607
608
  }
608
609
  for (const field of ["adapter", "declaredPaths", "effectBounds", "target"]) {
609
- if (JSON.stringify(record[field]) !== JSON.stringify(recreated[field])) {
610
+ if (!isDeepStrictEqual(record[field], recreated[field])) {
610
611
  const label = field === "effectBounds" ? "effect bounds" : field;
611
612
  throw new Error(`next-development ${label} drifted`);
612
613
  }
@@ -638,20 +639,19 @@ function assertStaticTransition(record, recreated) {
638
639
  }
639
640
 
640
641
  function assertMaterialization(record) {
641
- if (record.materialization !== null) {
642
- const { materializationRoot, ...body } = record.materialization;
643
- if (materializationRoot !== nextDevelopmentRoot(body)) {
644
- throw new Error("next-development materialization root drifted");
645
- }
646
- const materializedPaths = (record.materialization.paths || []).map(
647
- (entry) => entry.path,
648
- );
649
- if (
650
- JSON.stringify(materializedPaths) !==
651
- JSON.stringify(record.adapter.sourcePaths)
652
- ) {
653
- throw new Error("next-development materialization escaped source paths");
654
- }
642
+ if (record.materialization === null) return;
643
+ const { materializationRoot, ...body } = record.materialization;
644
+ if (materializationRoot !== nextDevelopmentRoot(body))
645
+ throw new Error("next-development materialization root drifted");
646
+ const { paths = [] } = record.materialization;
647
+ const materializedPaths = paths.map(({ path }) => path);
648
+ // Reference adapters cover sources; transaction adapters cover all outputs.
649
+ if (
650
+ ![record.adapter.sourcePaths, record.adapter.allowedChangePaths].some(
651
+ (expected) => isDeepStrictEqual(materializedPaths, expected),
652
+ )
653
+ ) {
654
+ throw new Error("next-development materialization paths drifted");
655
655
  }
656
656
  }
657
657