@kungfu-tech/buildchain 4.0.1-alpha.6 → 4.0.1-alpha.9

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 (50) hide show
  1. package/architecture/ci-lane-change-budget.json +187 -0
  2. package/architecture/maintainability-policy.json +13 -13
  3. package/architecture/v4-floating-consumer-policy.json +6 -0
  4. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  5. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  6. package/dist/site/buildchain-contract.json +9 -9
  7. package/dist/site/buildchain-site.json +19 -9
  8. package/dist/site/capability-registry.json +2 -2
  9. package/dist/site/kfd-claims.json +31 -5
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +1 -1
  12. package/dist/site/node-api-registry.json +154 -73
  13. package/dist/site/page-registry.json +14 -4
  14. package/dist/site/public-surface-audit.json +14 -3
  15. package/dist/site/publication-authority-registry.json +23 -5
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +1 -0
  18. package/dist/site/site-manifest.json +5 -5
  19. package/dist/site/workflow-registry.json +25 -4
  20. package/docs/dev-delivery-warrant.md +22 -1
  21. package/docs/node-api-reference.md +161 -148
  22. package/package.json +3 -2
  23. package/packages/core/buildchain-publication-authority.js +1 -0
  24. package/packages/core/ci-lane-change-budget.js +247 -0
  25. package/packages/core/dev-delivery-native-proof.js +43 -27
  26. package/packages/core/dev-delivery-warrant-state.js +31 -8
  27. package/packages/core/dev-delivery-warrant.js +8 -3
  28. package/packages/core/github-governance-authority.js +1 -0
  29. package/packages/core/index.js +5 -0
  30. package/packages/core/release-candidate-recovery.js +66 -11
  31. package/packages/core/release-candidate.js +2 -2
  32. package/packages/core/release-passport.js +10 -1
  33. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  34. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  35. package/packages/core/v4-floating-consumer-policy.js +138 -35
  36. package/packages/core/v4-floating-consumer-release-passport.js +30 -3
  37. package/packages/core/workflow-yaml-contract.js +24 -2
  38. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  39. package/scripts/check-inventory.mjs +2 -2
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  41. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  42. package/scripts/dev-delivery-warrant.mjs +5 -1
  43. package/scripts/dev-pr-delivery-warrant.mjs +1 -0
  44. package/scripts/dev-qualification-patrol.mjs +9 -9
  45. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  46. package/scripts/promotion-channel-router.mjs +4 -4
  47. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  48. package/scripts/resume-from-candidate-run.mjs +11 -14
  49. package/scripts/site-capability-metadata.mjs +1 -0
  50. package/scripts/v4-consumer-policy.mjs +4 -0
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { execFileSync } from "node:child_process";
7
+ import { evaluateCiLaneChangeBudget } from "../packages/core/ci-lane-change-budget.js";
8
+
9
+ const root = process.cwd();
10
+ const workflowRoot = path.join(root, ".github", "workflows");
11
+ const policyPath = path.join(
12
+ root,
13
+ "architecture",
14
+ "ci-lane-change-budget.json",
15
+ );
16
+
17
+ const workflows = fs
18
+ .readdirSync(workflowRoot, { withFileTypes: true })
19
+ .filter((entry) => entry.isFile() && /\.(?:yaml|yml)$/u.test(entry.name))
20
+ .map((entry) => {
21
+ const workflowPath = path.posix.join(".github/workflows", entry.name);
22
+ return {
23
+ path: workflowPath,
24
+ text: fs.readFileSync(path.join(workflowRoot, entry.name), "utf8"),
25
+ };
26
+ })
27
+ .sort((left, right) => left.path.localeCompare(right.path));
28
+
29
+ const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
30
+ const baselineRevision = String(policy.baseline?.gitRevision || "").trim();
31
+ if (!/^[0-9a-f]{40}$/u.test(baselineRevision)) {
32
+ throw new Error("baseline.gitRevision must be an exact Git commit");
33
+ }
34
+ const embeddedBaselineLanes = policy.baseline?.lanes;
35
+ if (
36
+ !Array.isArray(embeddedBaselineLanes) ||
37
+ embeddedBaselineLanes.length === 0
38
+ ) {
39
+ throw new Error("baseline.lanes must contain the exact offline lane set");
40
+ }
41
+ const embeddedLaneSetRoot = `sha256:${crypto
42
+ .createHash("sha256")
43
+ .update(`${JSON.stringify(embeddedBaselineLanes)}\n`)
44
+ .digest("hex")}`;
45
+ if (embeddedLaneSetRoot !== policy.baseline?.laneSetRoot) {
46
+ throw new Error("baseline laneSetRoot does not match baseline.lanes");
47
+ }
48
+
49
+ let baselineWorkflows = null;
50
+ try {
51
+ execFileSync("git", ["cat-file", "-e", `${baselineRevision}^{commit}`], {
52
+ cwd: root,
53
+ stdio: "ignore",
54
+ });
55
+ const baselinePaths = execFileSync(
56
+ "git",
57
+ [
58
+ "ls-tree",
59
+ "-r",
60
+ "--name-only",
61
+ baselineRevision,
62
+ "--",
63
+ ".github/workflows",
64
+ ],
65
+ { cwd: root, encoding: "utf8" },
66
+ )
67
+ .split(/\r?\n/u)
68
+ .filter((value) => /\.(?:yaml|yml)$/u.test(value))
69
+ .sort();
70
+ baselineWorkflows = baselinePaths.map((workflowPath) => ({
71
+ path: workflowPath,
72
+ text: execFileSync("git", ["show", `${baselineRevision}:${workflowPath}`], {
73
+ cwd: root,
74
+ encoding: "utf8",
75
+ }),
76
+ }));
77
+ } catch {
78
+ // Shallow and exported source cuts may not contain the pinned commit object.
79
+ // The content-rooted embedded lane set remains the exact offline authority.
80
+ }
81
+
82
+ const embeddedEvaluation = evaluateCiLaneChangeBudget({
83
+ policy,
84
+ workflows,
85
+ });
86
+ const evaluation = baselineWorkflows
87
+ ? evaluateCiLaneChangeBudget({ policy, workflows, baselineWorkflows })
88
+ : embeddedEvaluation;
89
+ if (
90
+ baselineWorkflows &&
91
+ (evaluation.baselineLaneCount !== embeddedEvaluation.baselineLaneCount ||
92
+ JSON.stringify(evaluation.newLanes) !==
93
+ JSON.stringify(embeddedEvaluation.newLanes))
94
+ ) {
95
+ throw new Error(
96
+ "embedded baseline lanes do not match the pinned baseline revision",
97
+ );
98
+ }
99
+ process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
100
+ if (!evaluation.ok) process.exitCode = 1;
@@ -1021,7 +1021,7 @@ for (const forbiddenSnippet of [
1021
1021
  for (const requiredSnippet of [
1022
1022
  "id-token: write",
1023
1023
  "actions: write",
1024
- "uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@alpha/v4/v4.0",
1024
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4-alpha",
1025
1025
  "github.event.workflow_run.event == 'push'",
1026
1026
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
1027
1027
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
@@ -1030,7 +1030,7 @@ for (const requiredSnippet of [
1030
1030
  "resume-buildchain-runtime-sha:",
1031
1031
  "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
1032
1032
  "release-passport-buildchain-self-kfd: true",
1033
- "node .buildchain/router/scripts/promotion-identity-resolver.mjs",
1033
+ "buildchain-channel: alpha",
1034
1034
  "release-passport-impact-json: .buildchain/release-impact.json",
1035
1035
  ]) {
1036
1036
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -52,8 +52,13 @@ function assertOrdered(relative, markers) {
52
52
 
53
53
  export function assertPromotionCertificationWiring(source) {
54
54
  for (const marker of [
55
- ".buildchain/runtime/promotion-shell/scripts/v4-consumer-policy.mjs certify",
56
- '--caller-root "${{ github.workspace }}"',
55
+ "path: .buildchain/runtime/candidate-policy-runtime",
56
+ "BUILDCHAIN_EXPECTED_RUNTIME_SHA: ${{ steps.v4-policy-caller.outputs.runtime-sha }}",
57
+ "policy_runtime=.buildchain/runtime",
58
+ 'node "${policy_runtime}/scripts/v4-consumer-policy.mjs" certify',
59
+ "passport.consumerPolicy?.receipt?.caller?.sourceSha",
60
+ "path: .buildchain/runtime/policy-caller",
61
+ '--caller-root "${{ github.workspace }}/.buildchain/runtime/policy-caller"',
57
62
  '--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
58
63
  '--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
59
64
  "release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
@@ -78,13 +83,22 @@ function assertPersistedSelectors() {
78
83
  );
79
84
  if (!match) continue;
80
85
  const selector = match[2];
86
+ const protectedBootstrap =
87
+ relative ===
88
+ ".github/workflows/buildchain-ref-promotion-recovery.yml" &&
89
+ match[1] === ".github/workflows/.release-candidate-promote.yml" &&
90
+ selector === "alpha/v4/v4.0";
81
91
  const isV4 =
82
92
  selector === "v4" ||
83
93
  selector === "v4-alpha" ||
84
94
  /^v4(?:[./-]|$)/u.test(selector) ||
85
95
  /^[0-9a-f]{40}$/iu.test(selector) ||
86
96
  selector.includes("${{");
87
- if (isV4 && !["v4", "v4-alpha"].includes(selector)) {
97
+ if (
98
+ (isV4 || protectedBootstrap) &&
99
+ !["v4", "v4-alpha"].includes(selector) &&
100
+ !protectedBootstrap
101
+ ) {
88
102
  offenders.push(`${relative}:${node.line} @${selector}`);
89
103
  }
90
104
  }
@@ -112,7 +126,12 @@ export function checkV4FloatingConsumerPolicyContract() {
112
126
  "Enforce v4 floating consumer policy",
113
127
  "Validate consumer package manager contract",
114
128
  ]);
115
- if (!read(".github/workflows/.build.yml").includes("BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: ${{ inputs.buildchain-expected-channel }}")) fail("channel builds must disambiguate dual-channel caller invocations");
129
+ if (
130
+ !read(".github/workflows/.build.yml").includes(
131
+ "BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: ${{ inputs.buildchain-expected-channel }}",
132
+ )
133
+ )
134
+ fail("channel builds must disambiguate dual-channel caller invocations");
116
135
  assertTrustGatedJobs(read(".github/workflows/.build.yml"), [
117
136
  "resolve-source",
118
137
  "resolve-contract",
@@ -46,7 +46,7 @@ function decodeBlob(blob) {
46
46
  ).toString("utf8");
47
47
  }
48
48
 
49
- function validateStoredState(queue) {
49
+ function validateStoredState(queue, { allowLegacyV3Readback = false } = {}) {
50
50
  const body = structuredClone(queue || {});
51
51
  const embeddedStateRoot = body.stateRoot;
52
52
  delete body.stateRoot;
@@ -59,7 +59,7 @@ function validateStoredState(queue) {
59
59
  ) {
60
60
  normalizeDevDeliveryAuthorityState(queue);
61
61
  } else {
62
- normalizeDevDeliveryQueue(queue);
62
+ normalizeDevDeliveryQueue(queue, { allowLegacyV3Readback });
63
63
  }
64
64
  return queue;
65
65
  }
@@ -108,7 +108,7 @@ export class GitHubDevDeliveryStore {
108
108
  return data;
109
109
  }
110
110
 
111
- async read({ stateRef, protectedBase, now }) {
111
+ async read({ stateRef, protectedBase, now, allowLegacyV3Readback = false }) {
112
112
  let ref;
113
113
  try {
114
114
  ref = await this.request(
@@ -128,11 +128,13 @@ export class GitHubDevDeliveryStore {
128
128
  };
129
129
  }
130
130
  const commitSha = exactSha(ref?.object?.sha, "state ref commit");
131
- const readback = await this.readCommit(commitSha);
131
+ const readback = await this.readCommit(commitSha, {
132
+ allowLegacyV3Readback,
133
+ });
132
134
  return { exists: true, ...readback };
133
135
  }
134
136
 
135
- async readCommit(commitShaInput) {
137
+ async readCommit(commitShaInput, { allowLegacyV3Readback = false } = {}) {
136
138
  const commitSha = exactSha(commitShaInput, "state commit");
137
139
  const commit = await this.request(
138
140
  "GET",
@@ -152,7 +154,9 @@ export class GitHubDevDeliveryStore {
152
154
  `/repos/${this.repository.fullName}/git/blobs/${entry.sha}`,
153
155
  );
154
156
  const bytes = decodeBlob(blob);
155
- const queue = validateStoredState(JSON.parse(bytes));
157
+ const queue = validateStoredState(JSON.parse(bytes), {
158
+ allowLegacyV3Readback,
159
+ });
156
160
  return { commitSha, queue, bytes };
157
161
  }
158
162
 
@@ -295,7 +295,10 @@ async function observeQueue(loaded, options) {
295
295
  const readMode = text(options.readMode || "v3").toLowerCase();
296
296
  if (!["v3", "v4"].includes(readMode))
297
297
  throw new Error("readMode must be v3 or v4");
298
- let observation = observeDevDeliveryQueue(loaded.queue, { now: options.now });
298
+ let observation = observeDevDeliveryQueue(loaded.queue, {
299
+ now: options.now,
300
+ allowLegacyV3Readback: true,
301
+ });
299
302
  let readCandidate;
300
303
  if (readMode === "v4") {
301
304
  const qualification =
@@ -374,6 +377,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
374
377
  stateRef: options.stateRef,
375
378
  protectedBase: options.branch,
376
379
  now: options.now,
380
+ allowLegacyV3Readback: options.command === "observe",
377
381
  });
378
382
  if (
379
383
  options.expectedOldStateRoot &&
@@ -143,6 +143,7 @@ export async function readCurrentDeliveryQueueState(client, repository, targetBr
143
143
  const { queue } = await store.read({
144
144
  stateRef: defaultDevDeliveryStateRef(targetBranch),
145
145
  protectedBase: targetBranch,
146
+ allowLegacyV3Readback: true,
146
147
  });
147
148
  return queue;
148
149
  }
@@ -501,16 +501,16 @@ export function createGitHubDevQualificationClient({
501
501
  sourceBranch,
502
502
  { activeOnly = false } = {},
503
503
  ) {
504
- const query = sourceBranch
505
- ? `?branch=${encodeURIComponent(sourceBranch)}`
506
- : "";
507
- const runs = await paged(
508
- `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflow)}/runs${query}`,
509
- "workflow_runs",
504
+ const query = sourceBranch ? `branch=${encodeRef(sourceBranch)}&` : "";
505
+ const runs = await Promise.all(
506
+ [...(activeOnly ? ACTIVE_STATUSES : [""])].map((status) =>
507
+ paged(
508
+ `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflow)}/runs?${query}${status ? `status=${status}` : ""}`,
509
+ "workflow_runs",
510
+ ),
511
+ ),
510
512
  );
511
- return activeOnly
512
- ? runs.filter((run) => ACTIVE_STATUSES.has(text(run.status)))
513
- : runs;
513
+ return runs.flat();
514
514
  },
515
515
  async listRunJobs(runId, runAttempt) {
516
516
  return paged(
@@ -159,8 +159,8 @@ function validateWorkflowRoute(name, route, expectedLogicalRef) {
159
159
  if (!/^\.github\/workflows\/[.a-z0-9-]+\.ya?ml$/.test(route.workflowPath || "")) {
160
160
  throw new Error(`promotion shell ${name} workflowPath must name a reusable workflow`);
161
161
  }
162
- if (!/^(?:v[0-9]+(?:-alpha)?|train\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
163
- throw new Error(`promotion shell ${name} callRef must be an official channel ref, trusted train, or exact SHA`);
162
+ if (!/^(?:v[0-9]+(?:-alpha)?|(?:train|alpha|release)\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
163
+ throw new Error(`promotion shell ${name} callRef must be an official tag, protected channel ref, trusted train, or exact SHA`);
164
164
  }
165
165
  if (typeof route.forwardInternalInputs !== "boolean") {
166
166
  throw new Error(`promotion shell ${name} forwardInternalInputs must be boolean`);
@@ -66,8 +66,8 @@ export function resolvePromotionChannel({
66
66
  }
67
67
  const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
68
68
  return {
69
- targetRef: intent.targetRef,
70
- publicationChannel: intent.publicationChannel,
69
+ targetRef: intent.targetRef, publicationChannel: intent.publicationChannel,
70
+ routerRef: normalized(routerRef), routerSha: normalized(routerSha).toLowerCase(),
71
71
  channel: intent.shellChannel,
72
72
  major: selected.major,
73
73
  shellRef,
@@ -99,8 +99,8 @@ function readPackageVersion(cwd) {
99
99
 
100
100
  function writeOutputs(file, result) {
101
101
  const outputs = {
102
- "target-ref": result.targetRef,
103
- "publication-channel": result.publicationChannel,
102
+ "target-ref": result.targetRef, "publication-channel": result.publicationChannel,
103
+ "router-ref": result.routerRef, "router-sha": result.routerSha,
104
104
  channel: result.channel,
105
105
  major: String(result.major),
106
106
  "shell-ref": result.shellRef,
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+
3
+ export function resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId = "" } = {}) {
4
+ if (Number(passport?.pullRequest?.number || 0)) return undefined;
5
+ if (passport?.target?.channel !== "anchor" || passport?.target?.ref !== "publish-gate/anchor") {
6
+ throw new Error("Release Candidate Passport has no PR identity");
7
+ }
8
+ let releaseManifest;
9
+ try {
10
+ releaseManifest = JSON.parse(buildSummary?.publishSource?.releaseManifest || "");
11
+ } catch {
12
+ throw new Error("anchored candidate release manifest is invalid");
13
+ }
14
+ const request = releaseManifest?.anchorRequest;
15
+ if (
16
+ releaseManifest?.sourceRef !== "publish-gate/anchor" ||
17
+ releaseManifest?.sourceSha !== passport.source?.headSha ||
18
+ request?.schema !== 1 ||
19
+ request?.contract !== "kungfu-buildchain-explicit-publish-anchor-request/v1" ||
20
+ !request.assignmentId ||
21
+ !/^native:[0-9a-f-]+$/u.test(String(request.attemptId || "")) ||
22
+ request.transactionId !== transactionId ||
23
+ request.source?.sha !== passport.source?.headSha ||
24
+ request.source?.tree !== passport.source?.treeHash ||
25
+ !/^\d+$/u.test(String(request.supersededCandidate?.workflowRunId || "")) ||
26
+ !/^[0-9a-f]{40}$/u.test(String(request.supersededCandidate?.sha || "")) ||
27
+ !/^sha256:[0-9a-f]{64}$/u.test(String(request.supersededCandidate?.root || "")) ||
28
+ request.runtime?.sha !== passport.buildchain?.sha
29
+ ) {
30
+ throw new Error("anchored candidate request does not bind the sealed recovery identity");
31
+ }
32
+ return request;
33
+ }
34
+
35
+ function normalizeRun(run, repository) {
36
+ return {
37
+ id: String(run.id), repository,
38
+ headRepository: run.head_repository?.full_name || "",
39
+ status: run.status, conclusion: run.conclusion,
40
+ event: run.event, path: run.path, name: run.name,
41
+ headSha: run.head_sha || "", headBranch: run.head_branch || "",
42
+ pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
43
+ };
44
+ }
45
+
46
+ export function normalizeCandidateRun(run, repository) {
47
+ return normalizeRun(run, repository);
48
+ }
49
+
50
+ export function normalizeAnchorProvenance(anchorProvenance, repository) {
51
+ if (!anchorProvenance) return undefined;
52
+ return {
53
+ request: anchorProvenance.request,
54
+ run: normalizeRun(anchorProvenance.run, repository),
55
+ workflow: {
56
+ path: anchorProvenance.workflow.path,
57
+ name: anchorProvenance.workflow.name,
58
+ state: anchorProvenance.workflow.state,
59
+ },
60
+ passport: anchorProvenance.passport,
61
+ buildSummary: anchorProvenance.buildSummary,
62
+ };
63
+ }
64
+
65
+ export async function recoverCandidateProvenance({
66
+ passport, buildSummary, transactionId, repoInfo, artifactName,
67
+ expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
68
+ fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
69
+ downloadArtifact, readOnlyJson,
70
+ }) {
71
+ const request = resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId });
72
+ if (!request) {
73
+ return { anchorProvenance: undefined, provenancePassport: passport };
74
+ }
75
+ const runId = String(request.supersededCandidate.workflowRunId);
76
+ const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
77
+ const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
78
+ const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
79
+ const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
80
+ if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("superseded candidate run has more than 100 artifacts; complete pagination is required before recovery");
81
+ const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
82
+ const provenanceRoot = path.join(bundleRoot, "anchored-provenance");
83
+ const passportDownload = await downloadArtifact({ artifact: selected.passport, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
84
+ const summaryDownload = await downloadArtifact({ artifact: selected.summary, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
85
+ const recoveredPassport = readOnlyJson(passportDownload.files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "superseded release-candidate-passport.json");
86
+ const recoveredBuildSummary = readOnlyJson(summaryDownload.files.filter((file) => path.basename(file.path) === "build-summary.json"), "superseded build-summary.json");
87
+ if (
88
+ String(run.id) !== runId ||
89
+ recoveredPassport.workflow?.runId !== runId ||
90
+ recoveredPassport.workflow?.name !== expectedWorkflowName ||
91
+ String(workflow.path || run.path || "").split("@")[0].replace(/^\.github\/workflows\//u, "") !== expectedWorkflowFile.replace(/^\.github\/workflows\//u, "") ||
92
+ recoveredPassport.target?.channel !== channel ||
93
+ recoveredPassport.source?.headSha !== request.supersededCandidate.sha ||
94
+ recoveredPassport.source?.treeHash !== request.source.tree ||
95
+ `sha256:${recoveredPassport.candidateHash}` !== request.supersededCandidate.root
96
+ ) throw new Error("superseded candidate provenance does not match the anchored request");
97
+ const anchorProvenance = { request, run, workflow, passport: recoveredPassport, buildSummary: recoveredBuildSummary };
98
+ return { anchorProvenance, provenancePassport: recoveredPassport };
99
+ }
@@ -7,6 +7,8 @@ import { execFileSync } from "node:child_process";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
9
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
+ import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
11
+ export { resolveAnchorRecoveryRequest } from "./release-candidate-anchor-provenance.mjs";
10
12
  import {
11
13
  generatePublishRequiredArtifacts,
12
14
  githubDownload,
@@ -878,7 +880,13 @@ export async function resumeFromCandidateRun({
878
880
  repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
879
881
  outputDir, apiUrl, token, fetchImpl, archiveDir,
880
882
  });
881
- const prNumber = Number(passport.pullRequest?.number || 0);
883
+ const { anchorProvenance, provenancePassport } = await recoverCandidateProvenance({
884
+ passport, buildSummary, transactionId, repoInfo, artifactName,
885
+ expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
886
+ fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
887
+ downloadArtifact, readOnlyJson,
888
+ });
889
+ const prNumber = Number(provenancePassport.pullRequest?.number || 0);
882
890
  if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
883
891
  const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
884
892
  const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
@@ -944,20 +952,9 @@ export async function resumeFromCandidateRun({
944
952
  expectedRuntimeSha: candidateRuntimeSha,
945
953
  expectedTransactionId: transactionId,
946
954
  existingTransaction,
947
- run: {
948
- id: String(run.id),
949
- repository: repoInfo.fullName,
950
- headRepository: run.head_repository?.full_name || "",
951
- status: run.status,
952
- conclusion: run.conclusion,
953
- event: run.event,
954
- path: run.path,
955
- name: run.name,
956
- headSha: run.head_sha || "",
957
- headBranch: run.head_branch || "",
958
- pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
959
- },
955
+ run: normalizeCandidateRun(run, repoInfo.fullName),
960
956
  workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
957
+ anchorProvenance: normalizeAnchorProvenance(anchorProvenance, repoInfo.fullName),
961
958
  pullRequest: {
962
959
  number: pullRequest.number,
963
960
  merged: pullRequest.merged === true,
@@ -195,6 +195,7 @@ export function nodeApiMeta(exportName) {
195
195
  "./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
196
196
  "./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
197
197
  "./candidate-timeline": { group: "observability-diagnostics", summary: "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs." },
198
+ "./ci-lane-change-budget": { group: "governance-versioning", summary: "Exact-baseline CI lane admission requiring authority, trigger, concurrency, runner, cancellation, and merge-critical SLO budgets." },
198
199
  "./channel-candidate": { group: "governance-versioning", summary: "Exact-source channel candidate decisions, same-SHA workflow evidence validation, and deterministic source-lock reference APIs." },
199
200
  "./dev-delivery-authority": { group: "governance-versioning", summary: "Opt-in bounded Qualification Lease, exclusive Landing Warrant, provider-attempt admission and heartbeat readback, terminal sealing, and expired-Landing verification APIs." },
200
201
  "./dev-delivery-warrant": { group: "governance-versioning", summary: "Durable fair Dev Delivery Warrant queue, fenced lease, and split source and integration proof APIs." },
@@ -41,6 +41,9 @@ export function scanCommand(options = {}) {
41
41
  const root = path.resolve(
42
42
  options.root || env("BUILDCHAIN_CONSUMER_ROOT", process.cwd()),
43
43
  );
44
+ const invocationRoot = path.resolve(
45
+ options.invocationRoot || env("BUILDCHAIN_INVOCATION_SOURCE_ROOT", root),
46
+ );
44
47
  const output = path.resolve(
45
48
  root,
46
49
  options.output ||
@@ -54,6 +57,7 @@ export function scanCommand(options = {}) {
54
57
  );
55
58
  const result = scanV4FloatingConsumerPolicy({
56
59
  root,
60
+ invocationRoot,
57
61
  repository: options.repository || env("GITHUB_REPOSITORY"),
58
62
  sourceSha: options.sourceSha || env("GITHUB_SHA"),
59
63
  invokedWorkflow: