@kungfu-tech/buildchain 4.1.3-alpha.9 → 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.
@@ -26,17 +26,17 @@
26
26
  },
27
27
  "repositoryBudgets": {
28
28
  "baselineRevision": "9569f69c2cb609372b8a3d65ce5dec8fb7b34a84",
29
- "maxHandMaintainedSourceFiles": 1479,
30
- "maxHandMaintainedSourceLines": 218720,
29
+ "maxHandMaintainedSourceFiles": 1481,
30
+ "maxHandMaintainedSourceLines": 218834,
31
31
  "maxWorkflowFiles": 44,
32
32
  "maxWorkflowLines": 8029,
33
- "latestTransition": "Bounded provider readback and safe npm publication failure diagnostics.",
34
- "latestTransitionRationale": "Charge sixteen source lines for bounded read-only reconciliation after one immutable write, plus twenty-eight measured source lines for fixed-allowlist npm failure codes. Keep one sealed npm publish invocation, exclude raw stdout/stderr and credentials from reported errors, and preserve all per-file, function, complexity and exception limits.",
33
+ "latestTransition": "Ignore skipped product placeholders in successful published-entry no-op runs.",
34
+ "latestTransitionRationale": "Charge four measured source lines for excluding skipped product jobs when recognizing successful no-op entry runs. Preserve failure and cancellation blocking, native receipt verification, and all per-file, function, complexity, debt and exception limits.",
35
35
  "rationale": "Bounded modules separate candidate, evidence, native proof and execution, canonical failure verification, closed-set execution transfer, process-boundary verification, Warrant state and qualification, authority state, qualification, Landing acquisition, verified-attempt admission, Landing settlement and fence validation, provider attempt and heartbeat readback, sealed terminal evidence, hosted heartbeat, resume, store, options, command-specific adapters, and CLI orchestration responsibilities. The protected delivery workflow gives candidate and seal jobs no provider write credential, recursively rejects transfer drift, scopes every transfer artifact to the exact provider run attempt, chains every durable heartbeat state and receipt root on a distinct hosted runner domain, and permits qualification or exact failure settlement only after the finalizer verifies live jobs and the latest durable authority state. Public Landing admission and heartbeat always perform non-injectable fresh exact-attempt provider readback, tolerate only a not-yet-materialized seal job while retaining exact native and duplicate-job rejection, and heartbeat-loss coordination never invokes GitHub's run-scoped cancellation API where a successor rerun could race. The hosted process boundary anchors trust in the kernel-resolved versioned Runner.Worker path while accepting an equivalent argv-zero spelling with the same basename; its public boundary marker is allowed only at the one exact non-secret value, so a forged value under the same credential-like name still fails closed. The credentialless transfer seal validates the rooted native reuse decision against the Warrant-bound source, closure, dependency, toolchain, environment and command inputs; qualifiedBase stays in the proof and decision where it is rooted and later independently rechecked, rather than being assumed on the Warrant schema. V2 mutation cannot initialize a second empty authority beside a legacy ref: migration reads and roots the canonical live v1 ref, rechecks it, applies the explicit configured v2 policy, and atomically replaces its bytes through one expected-old non-force update, so old v1 controllers fail closed while the prior commit remains immutable history. Candidate ancestry classifies the GitHub artifact runtime token as credential authority. Every Landing settlement, including an unexpired fence, requires product-owned terminal readback. Terminal cleanup reads the immutable historical attempt, derives workflow and hosted Landing job identity from live run data, reads the exact protected ref head and compares containment against that immutable SHA, and seals merged only when the admitted merge-group head is contained by that protected base; a later different-head merge settles the old attempt as dequeued. Terminal normalization preserves transfer, finalizer boundary, native and seal job, admitted provider attempt, protected-base head, containment result, and terminal readback coordinates. The public reusable workflow truthfully retains its single-flight v1 authority without advertising v2 migration, while bounded v2 remains a separate opt-in CLI, Node API, and schema contract; the tracked self-caller stays on public v4-alpha with matching stable and alpha locks, and transient train selection is dispatch-only. Next-development parity adds one versioned transition authority, one durable controller, one bounded reservation adapter, and pure public-contract recovery evidence while retaining the existing thin floating-channel callers byte-for-byte. The floating-consumer gate reuses the existing contract-world evaluator so SHA drift passes only under the selected lock's compatibility policy, breaking surface drift still fails closed, and external certification reconstructs the receipt from the observed workflow SHA rather than the lock's historical evidence SHA. Tail reseal adds no Stage Capsule effect or general rebuild authority: it verifies the exact retained four-platform bytes, admits one known macOS tail failure through live provider readback, fences signing and finalization to explicit credentials and Warrant identity, and requires standard candidate Passport plus provider and protected readback. Compatibility Facts parity isolates canonical encoding, temporal verification, lifecycle validation, fixture projection, public authority projection, and CLI adaptation into bounded modules; legacy digest and proof arrays are derived only from exact current Fact lineage. Publication Rehearsal adds no production release authority: the content-addressed capsule contract binds source, candidate, manifest, config, canonical data-only provider bindings, provider policy, observations and core version separately from filesystem, runtime and rehearsal-only provider authority; simulate, replay and explicitly authorized provider rehearsal all reuse the release-tail planner, while same-commit local dogfood resolves the new reusable contract and exact current runtime before external callers remain on floating v4-alpha. Cross-runtime candidate recovery preserves original runtime and provider roots and creates resume lineage only after exact public ref, tag, ancestry, npm version and integrity readback. Adopter Delivery restores the reviewed public v3 gate, drivers, Passport binding, N-1 bootstrap and exact archive loader through one public v4 surface while keeping provider and release authority explicit non-claims. Floating Alpha bootstrap uses only the existing exact resolver and protected Alpha private shell to move an otherwise stale floating shell, retains public v4 stable promotion, and removes itself after protected readback. CI lane change-budget enforcement adds a bounded parser, evaluator, guard, and tests without creating another merge authority. Minimal consumer contract adds exactly seven bounded contract modules, one generated-contract gate and six product build/test example sources; the source-file ceiling increases by fourteen for this reviewed feature slice. Existing per-file, function, complexity and debt limits are unchanged. The final readable template sources require exact source-line and total-automation ceilings of 203394 and 224451 respectively; this charges all generated example sources rather than hiding them from measurement. Qualified landing status projection adds 51 source lines and four internal action-contract lines within existing job permissions; per-file, function, complexity and exception budgets are unchanged. Delivery scheduling preserves its exact queued or running provider owner before Warrant acquisition, adding 31 measured source lines within the existing structural budgets.",
36
36
  "governance": "legacy-debt-sunset-2026q4",
37
37
  "maxActionDefinitions": 283,
38
38
  "maxActionDefinitionLines": 12399,
39
- "maxAutomationImplementationLines": 238300
39
+ "maxAutomationImplementationLines": 238414
40
40
  },
41
41
  "sourceBudgets": {
42
42
  "newFileLines": 600,
@@ -188,12 +188,14 @@
188
188
  "packages/core/providers/github/pipeline-run-entry.js",
189
189
  "packages/core/providers/github/pipeline-runs.js",
190
190
  "packages/core/providers/github/pipeline-source.js",
191
+ "packages/core/providers/github/pipeline-stable-baseline.js",
191
192
  "packages/core/providers/github/pipeline-stable-source.js",
192
193
  "packages/core/providers/github/pipeline-version-artifacts.js",
193
194
  "packages/core/providers/github/pipeline-version-readback.js",
194
195
  "packages/core/providers/github/pipeline-version.js",
195
196
  "packages/core/providers/github/pipeline-worker.js",
196
197
  "packages/core/providers/github/release-assets.js",
198
+ "packages/core/providers/github/release-evidence-assets.js",
197
199
  "packages/core/publication/binary/action.js",
198
200
  "packages/core/publication/binary/capability.js",
199
201
  "packages/core/publication/binary/evidence.js",
@@ -397,7 +399,7 @@
397
399
  "packages/core/workflow/pipeline/wake.js",
398
400
  "packages/core/workflow/pipeline/web-status.js"
399
401
  ],
400
- "root": "sha256:b474be097d7b1455785cf78247c966601a2adfb03ff1baee9cc009f26b9c2ac1"
402
+ "root": "sha256:d95f03e3906b482b6ab0486d531865162e62961f2398058f6aecb380500f5314"
401
403
  },
402
404
  "rustWasmAuthority": {
403
405
  "loader": "packages/core/runtime/domain-wasm.js",
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "name": "Buildchain",
6
6
  "package": "@kungfu-tech/buildchain",
7
- "version": "4.1.3-alpha.9",
7
+ "version": "4.1.3",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "majorLine": "v4",
@@ -2356,5 +2356,5 @@
2356
2356
  "compatibilityFactCutRoot": "sha256:58f33e498596505a26b6fc3c7ae5acef847ff699eee6c7e17a7b318a40bdf0d6",
2357
2357
  "compatibilityProofRegistryRoot": "sha256:2f6477405c7891b754b33ad6515d55618bd320e7cdc59330d7cc645e17201e72",
2358
2358
  "compatibilityDigest": "sha256:81e32696723fe82fe29d2b2214cd56cf6c87bbeae5cff1a201de7ee7fd10ee9c",
2359
- "contractDigest": "sha256:1c898441fd7051ee92677001bc465e69b9b5988bba15f45be857f3d0a014a129"
2359
+ "contractDigest": "sha256:41084ce334ddbf03549d209d96310db8f653921cbb25eaf0dce0571565d2cce5"
2360
2360
  }
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "4.1.3-alpha.9",
40
+ "version": "4.1.3",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "source": {
@@ -21,7 +21,7 @@
21
21
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
22
22
  "path": "dist/site/public-surface-audit.json",
23
23
  "status": "passed",
24
- "sha256": "e215e928cf074b0532e8e1892a7b7b7e5855dc3531e0a78cec8b4e3b51d8a65a",
24
+ "sha256": "c5ae763ca6bac9648d2291c1908da6a0ee78785651aecfced739c9988dfcf92f",
25
25
  "summary": {
26
26
  "cliCommandCount": 117,
27
27
  "workflowCount": 44,
@@ -246,7 +246,7 @@
246
246
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
247
247
  "path": "dist/site/public-surface-audit.json",
248
248
  "status": "passed",
249
- "sha256": "e215e928cf074b0532e8e1892a7b7b7e5855dc3531e0a78cec8b4e3b51d8a65a",
249
+ "sha256": "c5ae763ca6bac9648d2291c1908da6a0ee78785651aecfced739c9988dfcf92f",
250
250
  "summary": {
251
251
  "cliCommandCount": 117,
252
252
  "workflowCount": 44,
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "id": "kungfu-tech-buildchain",
6
6
  "name": "@kungfu-tech/buildchain",
7
- "version": "4.1.3-alpha.9",
7
+ "version": "4.1.3",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "source": {
@@ -10814,7 +10814,7 @@
10814
10814
  "workflowRegistryPath": "dist/site/workflow-registry.json",
10815
10815
  "pageRegistryPath": "dist/site/page-registry.json",
10816
10816
  "cliRegistryDigest": "e43fdb0289df88871f3f0febbe04dcb9311a252a2b1df9b3a83a2d10c9d4a70f",
10817
- "workflowRegistryDigest": "071f8c15fed251611c981d66e8961b74476bfdce7bd8a25e0c396e37947e2c47",
10817
+ "workflowRegistryDigest": "b64f87652d2377f6f1cd192bb3e0983f6885af60f3522915649f6a2dc40610fc",
10818
10818
  "pageRegistryDigest": "1e79a71c8f1d094ad1291b2d8b4bc983633245dcc104ae5ef7bae755fb733209"
10819
10819
  },
10820
10820
  "comparison": {
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "4.1.3-alpha.9",
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.9",
40
+ "version": "4.1.3",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -1263,7 +1263,7 @@
1263
1263
  "status": "preview",
1264
1264
  "owner": "buildchain-workflows",
1265
1265
  "maturity": "preview",
1266
- "introducedVersion": "4.1.3-alpha.9",
1266
+ "introducedVersion": "4.1.3",
1267
1267
  "compatibilityPromise": "current-contract-only",
1268
1268
  "deprecationReplacement": "",
1269
1269
  "sunsetCondition": "explicit-contract-change-review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "4.1.3-alpha.9",
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;