@kungfu-tech/buildchain 2.14.3 → 2.14.4-alpha.0

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 (37) hide show
  1. package/actions/promote-buildchain-ref/README.md +8 -0
  2. package/bin/buildchain.mjs +4 -0
  3. package/dist/site/buildchain-contract.json +39 -25
  4. package/dist/site/buildchain-site.json +26 -26
  5. package/dist/site/capability-registry.json +1 -1
  6. package/dist/site/controller-registry.json +28 -4
  7. package/dist/site/kfd-claims.json +24 -6
  8. package/dist/site/kfd-upstream-aggregate.json +1 -1
  9. package/dist/site/manual-registry.json +6 -6
  10. package/dist/site/node-api-registry.json +20 -7
  11. package/dist/site/page-registry.json +16 -16
  12. package/dist/site/public-surface-audit.json +11 -5
  13. package/dist/site/publication-registry.json +4 -4
  14. package/dist/site/release-passport-check-manifest.json +1 -0
  15. package/dist/site/release-provenance.json +1 -0
  16. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  17. package/dist/site/site-manifest.json +10 -10
  18. package/dist/site/workflow-registry.json +9 -3
  19. package/docs/MAP.md +1 -0
  20. package/docs/cli.md +7 -0
  21. package/docs/lifecycle-protocol.md +14 -0
  22. package/docs/release-governance.md +10 -1
  23. package/docs/release-passport.md +21 -0
  24. package/docs/versioning.md +1 -0
  25. package/package.json +2 -1
  26. package/packages/core/README.md +2 -0
  27. package/packages/core/anchored-version-material.js +281 -0
  28. package/packages/core/buildchain-config.js +68 -0
  29. package/packages/core/buildchain-contract.js +4 -0
  30. package/packages/core/controller-evidence.js +30 -4
  31. package/packages/core/index.js +6 -0
  32. package/packages/core/release-passport-contract.js +2 -0
  33. package/packages/core/release-passport.js +186 -1
  34. package/scripts/anchored-version-material.mjs +42 -0
  35. package/scripts/check-inventory.mjs +6 -0
  36. package/scripts/generate-site-bundle.mjs +1 -0
  37. package/scripts/stable-candidate-patrol.mjs +55 -6
@@ -26,6 +26,7 @@ export const PRODUCT_MECHANISM_CONTRACT = "kungfu-buildchain-product-mechanism";
26
26
  export const RELEASE_CHECK_REPORT_CONTRACT = "kungfu-buildchain-release-check-report";
27
27
  export const KFD2_RELEASE_TRUST_PASSPORT_CONTRACT = "kungfu-buildchain-kfd-2-release-trust-passport-audit";
28
28
  export const KFD2_TRUST_PROOF_CONTRACT = "kungfu-buildchain-kfd-2-trust-proof";
29
+ export const INVARIANT_PASSPORT_GATE_CONTRACT = "buildchain.invariant-passport-gate/v1";
29
30
 
30
31
  const CONTRACTS = new Set([
31
32
  RELEASE_PASSPORT_CONTRACT,
@@ -128,6 +129,80 @@ export function sha256Text(value) {
128
129
  return crypto.createHash("sha256").update(value).digest("hex");
129
130
  }
130
131
 
132
+ function invariantSemanticPreimage(passport) {
133
+ const value = structuredClone(passport);
134
+ delete value.passportRoot;
135
+ delete value.observedAt;
136
+ return value;
137
+ }
138
+
139
+ function normalizeInvariantPassport(meta, index = 0) {
140
+ const value = meta?.value;
141
+ const label = `invariantPassportJsons[${index}]`;
142
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
143
+ throw new Error(`${label} must be a JSON object`);
144
+ }
145
+ for (const field of ["schema", "product", "canonicalization", "passportRoot", "contractRoot", "registryRoot", "verdict"]) {
146
+ nonEmptyString(value[field], `${label}.${field}`);
147
+ }
148
+ if (value.canonicalization !== "stable-json-sha256-v1") {
149
+ throw new Error(`${label}.canonicalization must be stable-json-sha256-v1`);
150
+ }
151
+ if (!/^sha256:[0-9a-f]{64}$/.test(value.passportRoot)) {
152
+ throw new Error(`${label}.passportRoot must be sha256:<64-lowercase-hex>`);
153
+ }
154
+ const expectedRoot = `sha256:${sha256Text(stableJson(invariantSemanticPreimage(value)))}`;
155
+ if (value.passportRoot !== expectedRoot) {
156
+ throw new Error(`${label}.passportRoot mismatch: expected ${expectedRoot}, got ${value.passportRoot}`);
157
+ }
158
+ if (value.verdict !== "verified") {
159
+ throw new Error(`${label}.verdict must be verified, got ${value.verdict}`);
160
+ }
161
+ if (value.coverage?.complete !== true) {
162
+ throw new Error(`${label}.coverage.complete must be true`);
163
+ }
164
+ if (value.source?.dirty !== false) {
165
+ throw new Error(`${label}.source.dirty must be false`);
166
+ }
167
+ if (!/^[0-9a-f]{40}$/.test(optionalString(value.source?.revision))) {
168
+ throw new Error(`${label}.source.revision must be an exact 40-hex revision`);
169
+ }
170
+ const platforms = Array.isArray(value.coverage?.platforms)
171
+ ? [...new Set(value.coverage.platforms.map(String))].sort()
172
+ : [];
173
+ if (platforms.length === 0) throw new Error(`${label}.coverage.platforms must be non-empty`);
174
+ if (!Array.isArray(value.residualRisk)) throw new Error(`${label}.residualRisk must be an array`);
175
+ return {
176
+ path: optionalString(meta.path),
177
+ sha256: optionalString(meta.sha256),
178
+ schema: value.schema,
179
+ product: value.product,
180
+ passportRoot: value.passportRoot,
181
+ contractRoot: value.contractRoot,
182
+ registryRoot: value.registryRoot,
183
+ verdict: value.verdict,
184
+ source: structuredClone(value.source),
185
+ coverage: structuredClone(value.coverage),
186
+ platforms,
187
+ residualRisk: structuredClone(value.residualRisk),
188
+ };
189
+ }
190
+
191
+ export function createInvariantPassportGate(passportMetas = []) {
192
+ const passports = passportMetas.filter((meta) => meta?.value).map(normalizeInvariantPassport);
193
+ if (passports.length === 0) return undefined;
194
+ return {
195
+ contract: INVARIANT_PASSPORT_GATE_CONTRACT,
196
+ result: "passed",
197
+ passports,
198
+ responsibility: {
199
+ invariantSemanticsOwner: "consumer",
200
+ passportVerificationOwner: "consumer",
201
+ releaseAdmissionOwner: "Buildchain",
202
+ },
203
+ };
204
+ }
205
+
131
206
  export function sha256File(filePath) {
132
207
  return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
133
208
  }
@@ -411,7 +486,12 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
411
486
  if (!stdout) {
412
487
  throw new Error(`${label} produced no JSON on stdout`);
413
488
  }
414
- const parsed = JSON.parse(stdout);
489
+ let parsed;
490
+ try {
491
+ parsed = JSON.parse(stdout);
492
+ } catch (error) {
493
+ throw new Error(`${label} output must be valid JSON: ${error.message}`, { cause: error });
494
+ }
415
495
  return {
416
496
  value: parsed,
417
497
  path: "",
@@ -1016,6 +1096,7 @@ export function createReleasePassport({
1016
1096
  assets = [],
1017
1097
  packageSet = undefined,
1018
1098
  anchorManifest = undefined,
1099
+ versionMaterial = undefined,
1019
1100
  publishEvidence = undefined,
1020
1101
  trustedPublishing = undefined,
1021
1102
  transaction = undefined,
@@ -1030,6 +1111,7 @@ export function createReleasePassport({
1030
1111
  kfd1 = undefined,
1031
1112
  kfd2Claims = [],
1032
1113
  kfd3 = undefined,
1114
+ invariantPassports = undefined,
1033
1115
  controllerReceipts = [],
1034
1116
  controllerReceiptReferences = [],
1035
1117
  } = {}) {
@@ -1186,6 +1268,7 @@ export function createReleasePassport({
1186
1268
  ...(normalizedPackageSet ? { packageSet: normalizedPackageSet } : {}),
1187
1269
  ...(normalizedPublishSummary ? { publish: normalizedPublishSummary } : {}),
1188
1270
  ...(anchorManifest ? { anchorManifest } : {}),
1271
+ ...(versionMaterial ? { versionMaterial } : {}),
1189
1272
  ...(normalizedTrustedPublishing ? { trustedPublishing: normalizedTrustedPublishing } : {}),
1190
1273
  ...(normalizedTransaction ? { transaction: normalizedTransaction } : {}),
1191
1274
  ...(normalizedPromotionRouting ? { promotionRouting: normalizedPromotionRouting } : {}),
@@ -1196,6 +1279,7 @@ export function createReleasePassport({
1196
1279
  ...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
1197
1280
  ...(normalizedKfd2 ? { "kfd-2": normalizedKfd2 } : {}),
1198
1281
  ...(normalizedKfd3 ? { [normalizedKfd3.key || "kfd-3"]: normalizedKfd3.passportSection } : {}),
1282
+ ...(invariantPassports ? { invariantPassports } : {}),
1199
1283
  ...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
1200
1284
  versionImpact: normalizedImpact.versionImpact,
1201
1285
  surfaceImpacts: normalizedImpact.surfaceImpacts,
@@ -1238,6 +1322,7 @@ export function createReleasePassport({
1238
1322
  kfd1: normalizedKfd1 ? `${normalizedKfd1.key || kfd1Metadata.key}` : "",
1239
1323
  kfd2: normalizedKfd2 ? "kfd-2" : "",
1240
1324
  kfd3: normalizedKfd3 ? `${normalizedKfd3.key || "kfd-3"}` : "",
1325
+ invariantPassports: invariantPassports ? "invariantPassports" : "",
1241
1326
  impact: impactPath,
1242
1327
  checkReport: checkReportPath,
1243
1328
  agentIndex: agentIndexPath,
@@ -1267,6 +1352,7 @@ export function collectGitHubReleasePassport({
1267
1352
  trustedPublishingJson = "",
1268
1353
  transactionJson = "",
1269
1354
  anchorManifestJson = "",
1355
+ versionMaterialJson = "",
1270
1356
  impactJson = "",
1271
1357
  buildSummaryJson = "",
1272
1358
  buildFactsJsons = [],
@@ -1277,6 +1363,8 @@ export function collectGitHubReleasePassport({
1277
1363
  kfd3PrebuildWitnessJsons = [],
1278
1364
  kfd3ArtifactWitnessJsons = [],
1279
1365
  kfd3ArtifactVerifyCommand = "",
1366
+ invariantPassportJsons = [],
1367
+ invariantPassportCommand = "",
1280
1368
  controllerReceiptReferences = [],
1281
1369
  basePassportJson = "",
1282
1370
  requireBaseKfd = false,
@@ -1292,6 +1380,11 @@ export function collectGitHubReleasePassport({
1292
1380
  const trustedPublishing = parseJsonInput(trustedPublishingJson, undefined, { cwd, label: "trustedPublishingJson" });
1293
1381
  const transactionMeta = parseJsonInputWithMeta(transactionJson, undefined, { cwd, label: "transactionJson" });
1294
1382
  const anchorManifest = normalizeAnchorManifest(parseJsonInputWithMeta(anchorManifestJson, undefined, { cwd, label: "anchorManifestJson" }));
1383
+ const versionMaterial = parseJsonInput(
1384
+ versionMaterialJson,
1385
+ undefined,
1386
+ { cwd, label: "versionMaterialJson" },
1387
+ );
1295
1388
  const impactMeta = parseJsonInputWithMeta(impactJson, undefined, { cwd, label: "impactJson" });
1296
1389
  const buildSummaryMeta = parseJsonInputWithMeta(buildSummaryJson, undefined, { cwd, label: "buildSummaryJson" });
1297
1390
  const buildFactMetas = (buildFactsJsons || [])
@@ -1324,6 +1417,17 @@ export function collectGitHubReleasePassport({
1324
1417
  cwd,
1325
1418
  label: "KFD-3 artifact verify command",
1326
1419
  });
1420
+ const invariantPassportMetas = (invariantPassportJsons || [])
1421
+ .filter(Boolean)
1422
+ .map((passportJson) => parseJsonInputWithMeta(passportJson, undefined, { cwd, label: "invariantPassportJsons entry" }))
1423
+ .filter((meta) => meta.value);
1424
+ const invariantPassportCommandMeta = parseJsonCommandOutput({
1425
+ command: invariantPassportCommand,
1426
+ cwd,
1427
+ label: "invariant passport command",
1428
+ });
1429
+ if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
1430
+ const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
1327
1431
  const kfd3ArtifactWitnesses = [
1328
1432
  ...kfd3ArtifactWitnessMetas.map((meta) => meta.value),
1329
1433
  ...(kfd3ArtifactCommandMeta.value ? [kfd3ArtifactCommandMeta.value] : []),
@@ -1369,6 +1473,7 @@ export function collectGitHubReleasePassport({
1369
1473
  assets,
1370
1474
  packageSet,
1371
1475
  anchorManifest,
1476
+ versionMaterial,
1372
1477
  publishEvidence: publishEvidenceMeta.value,
1373
1478
  trustedPublishing,
1374
1479
  transaction: transactionMeta.value,
@@ -1400,6 +1505,7 @@ export function collectGitHubReleasePassport({
1400
1505
  kfd1,
1401
1506
  kfd2Claims: kfd2ClaimMetas.map((meta) => meta.value),
1402
1507
  kfd3,
1508
+ invariantPassports,
1403
1509
  controllerReceiptReferences,
1404
1510
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
1405
1511
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
@@ -1881,6 +1987,56 @@ export function createReleaseCheckReport({
1881
1987
  issues.push(issue("error", "anchorManifest.fields", "anchorManifest.fields must be an object"));
1882
1988
  }
1883
1989
  }
1990
+ if (passport?.versionMaterial) {
1991
+ if (passport.versionMaterial.contract !== "kungfu-buildchain-anchored-version-material/v1") {
1992
+ issues.push(issue(
1993
+ "error",
1994
+ "versionMaterial.contract",
1995
+ "versionMaterial contract must be kungfu-buildchain-anchored-version-material/v1",
1996
+ ));
1997
+ }
1998
+ if (!passport.versionMaterial.alpha?.tree || !passport.versionMaterial.release?.tree) {
1999
+ issues.push(issue(
2000
+ "error",
2001
+ "versionMaterial.tree",
2002
+ "versionMaterial must record alpha and release tree identities",
2003
+ ));
2004
+ }
2005
+ const allowedPaths = Array.isArray(passport.versionMaterial.allowedPaths)
2006
+ ? passport.versionMaterial.allowedPaths
2007
+ : [];
2008
+ const derivedFiles = Array.isArray(passport.versionMaterial.derivedFiles)
2009
+ ? passport.versionMaterial.derivedFiles
2010
+ : [];
2011
+ for (const [index, file] of derivedFiles.entries()) {
2012
+ if (!file?.path || !file?.sha256 || !allowedPaths.includes(file.path)) {
2013
+ issues.push(issue(
2014
+ "error",
2015
+ `versionMaterial.derivedFiles[${index}]`,
2016
+ "derived version material must have a path, digest, and matching allowed path",
2017
+ ));
2018
+ }
2019
+ }
2020
+ for (const side of ["alpha", "release"]) {
2021
+ const material = Array.isArray(passport.versionMaterial[side]?.material)
2022
+ ? passport.versionMaterial[side].material
2023
+ : [];
2024
+ for (const [index, file] of material.entries()) {
2025
+ if (
2026
+ !file?.path ||
2027
+ !allowedPaths.includes(file.path) ||
2028
+ file.present !== true ||
2029
+ !/^sha256:[0-9a-f]{64}$/.test(file.sha256 || "")
2030
+ ) {
2031
+ issues.push(issue(
2032
+ "error",
2033
+ `versionMaterial.${side}.material[${index}]`,
2034
+ "version material must have an allowed path, present bytes, and sha256 digest",
2035
+ ));
2036
+ }
2037
+ }
2038
+ }
2039
+ }
1884
2040
  if (!passport?.runnerPolicy?.productionDefault) {
1885
2041
  issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
1886
2042
  }
@@ -1917,6 +2073,35 @@ export function createReleaseCheckReport({
1917
2073
  issues.push(issue("error", "kfd-3.metadata", error.message));
1918
2074
  }
1919
2075
  }
2076
+ if (passport?.invariantPassports) {
2077
+ const section = passport.invariantPassports;
2078
+ if (section.contract !== INVARIANT_PASSPORT_GATE_CONTRACT) {
2079
+ issues.push(issue("error", "invariantPassports.contract", `invariantPassports.contract must be ${INVARIANT_PASSPORT_GATE_CONTRACT}`));
2080
+ }
2081
+ if (section.result !== "passed") {
2082
+ issues.push(issue("error", "invariantPassports.result", "invariantPassports.result must be passed"));
2083
+ }
2084
+ if (!Array.isArray(section.passports) || section.passports.length === 0) {
2085
+ issues.push(issue("error", "invariantPassports.empty", "invariantPassports must contain at least one verified passport"));
2086
+ }
2087
+ const acceptedSourceShas = new Set([
2088
+ passport?.release?.sourceSha,
2089
+ passport?.release?.builtSourceSha,
2090
+ passport?.release?.promotionChannelSha,
2091
+ ].filter(Boolean));
2092
+ for (const [index, entry] of (section.passports || []).entries()) {
2093
+ const prefix = `invariantPassports.passports[${index}]`;
2094
+ if (entry.verdict !== "verified") issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified`));
2095
+ if (entry.coverage?.complete !== true) issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage.complete must be true`));
2096
+ if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
2097
+ if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
2098
+ issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
2099
+ }
2100
+ if (!/^sha256:[0-9a-f]{64}$/.test(optionalString(entry.passportRoot))) issues.push(issue("error", `${prefix}.passportRoot`, `${prefix}.passportRoot is invalid`));
2101
+ if (!Array.isArray(entry.platforms) || entry.platforms.length === 0) issues.push(issue("error", `${prefix}.platforms`, `${prefix}.platforms must be non-empty`));
2102
+ if (!Array.isArray(entry.residualRisk)) issues.push(issue("error", `${prefix}.residualRisk`, `${prefix}.residualRisk must be an array`));
2103
+ }
2104
+ }
1920
2105
  if (impactVersion) {
1921
2106
  if (!impactLine) {
1922
2107
  issues.push(issue("error", "impact.release.line", "version-bound impact requires release.line"));
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createAnchoredVersionMaterialEvidence } from "../packages/core/anchored-version-material.js";
5
+
6
+ function env(name, fallback = "") {
7
+ return process.env[name] || fallback;
8
+ }
9
+
10
+ function writeOutput(name, value) {
11
+ if (!process.env.GITHUB_OUTPUT) {
12
+ return;
13
+ }
14
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${String(value)}\n`);
15
+ }
16
+
17
+ try {
18
+ const cwd = path.resolve(env("BUILDCHAIN_ANCHORED_SOURCE_CWD", process.cwd()));
19
+ const outputPath = path.resolve(
20
+ env(
21
+ "BUILDCHAIN_ANCHORED_MATERIAL_OUTPUT",
22
+ ".buildchain/artifacts/anchored-version-material.json",
23
+ ),
24
+ );
25
+ const evidence = createAnchoredVersionMaterialEvidence({
26
+ cwd,
27
+ targetChannel: env("BUILDCHAIN_ANCHORED_TARGET_CHANNEL"),
28
+ targetRef: env("BUILDCHAIN_ANCHORED_TARGET_REF"),
29
+ alphaRef: env("BUILDCHAIN_ANCHORED_ALPHA_REF"),
30
+ releaseRef: env("BUILDCHAIN_ANCHORED_RELEASE_REF", "HEAD"),
31
+ runLifecycle: env("BUILDCHAIN_ANCHORED_RUN_LIFECYCLE", "true") !== "false",
32
+ });
33
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
34
+ fs.writeFileSync(outputPath, `${JSON.stringify(evidence, null, 2)}\n`);
35
+ writeOutput("anchored-version-material-applicable", evidence.applicable === true);
36
+ writeOutput("anchored-version-material-digest", evidence.digest || "");
37
+ writeOutput("anchored-version-material-path", outputPath);
38
+ process.stdout.write(`${JSON.stringify(evidence)}\n`);
39
+ } catch (error) {
40
+ console.error(`anchored version material: ${error.message}`);
41
+ process.exitCode = 1;
42
+ }
@@ -26,6 +26,7 @@ const requiredPaths = [
26
26
  "bin/buildchain.mjs",
27
27
  "packages/core/homebrew.js",
28
28
  "packages/core/artifact-verification-envelope.js",
29
+ "packages/core/anchored-version-material.js",
29
30
  "packages/core/build-facts.js",
30
31
  "packages/core/publication-package.js",
31
32
  "packages/core/release-line-bootstrap.js",
@@ -58,6 +59,7 @@ const requiredPaths = [
58
59
  "scripts/generate-release-candidate-passport.mjs",
59
60
  "scripts/shifu-gate-profile.mjs",
60
61
  "scripts/artifact-relay-s3.mjs",
62
+ "scripts/anchored-version-material.mjs",
61
63
  "scripts/npm-publish-dry-run.mjs",
62
64
  "scripts/npm-publish-transaction.mjs",
63
65
  "scripts/publication-package.mjs",
@@ -876,6 +878,10 @@ for (const requiredSnippet of [
876
878
  "release-passport-kfd-3-artifact-witness-jsons:",
877
879
  "release-passport-kfd-3-artifact-witness-jsons: ${{ inputs.release-passport-kfd-3-artifact-witness-jsons }}",
878
880
  "release-passport-kfd-3-artifact-verify-command:",
881
+ "release-passport-invariant-passport-jsons:",
882
+ "release-passport-invariant-passport-jsons: ${{ inputs.release-passport-invariant-passport-jsons }}",
883
+ "release-passport-invariant-passport-command:",
884
+ "release-passport-invariant-passport-command: ${{ inputs.release-passport-invariant-passport-command }}",
879
885
  "release-passport-buildchain-self-kfd:",
880
886
  "release-passport-buildchain-self-kfd: ${{ inputs.release-passport-buildchain-self-kfd }}",
881
887
  "github-release:",
@@ -499,6 +499,7 @@ function nodeApiMeta(exportName) {
499
499
  "./buildchain-publication-authority": { group: "release-passport-trust", summary: "Buildchain-owned closed-world publication authority descriptor registry." },
500
500
  "./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
501
501
  "./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
502
+ "./anchored-version-material": { group: "reusable-build", summary: "Anchored/manual derived version material preflight, exact-tree binding, and digest evidence APIs." },
502
503
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
503
504
  "./release-passport-contract": { group: "release-passport-trust", summary: "Standalone release passport JSON Schema, ownership/check manifest, and structural validation APIs." },
504
505
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
@@ -223,7 +223,7 @@ export async function runStableCandidatePatrol(optionsInput = {}, clientInput) {
223
223
  `Buildchain Stable Candidate Patrol human release-now for ${selection.candidate.version}`,
224
224
  );
225
225
  }
226
- await client.ensureBranch(refs.sourceRef, selection.candidate.sha);
226
+ await client.ensureBranch(refs.sourceRef, selection.candidate.sha, refs.targetRef);
227
227
  const pullRequest = await client.ensurePromotionPullRequest({
228
228
  head: refs.sourceRef,
229
229
  base: refs.targetRef,
@@ -371,15 +371,64 @@ export function createGitHubStableCandidateClient({ repository: repositoryInput,
371
371
  },
372
372
  });
373
373
  },
374
- async ensureBranch(ref, candidateSha) {
374
+ async ensureBranch(ref, candidateSha, targetRef) {
375
375
  const current = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`, { allow404: true });
376
- if (!current) {
376
+ if (!targetRef) {
377
+ if (!current) {
378
+ return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: candidateSha } });
379
+ }
380
+ if (current.object.sha !== candidateSha) {
381
+ throw new Error(`source-lock branch ${ref} already points to ${current.object.sha}, not ${candidateSha}`);
382
+ }
383
+ return current;
384
+ }
385
+
386
+ const candidateCommit = await api(`/repos/${owner}/${repo}/git/commits/${candidateSha}`);
387
+ let sourceSha = current?.object?.sha || candidateSha;
388
+ if (sourceSha !== candidateSha) {
389
+ const sourceCommit = await api(`/repos/${owner}/${repo}/git/commits/${sourceSha}`);
390
+ if (sourceCommit.tree?.sha !== candidateCommit.tree?.sha) {
391
+ throw new Error(`source-lock branch ${ref} no longer preserves candidate ${candidateSha} tree`);
392
+ }
393
+ const candidateLineage = await api(`/repos/${owner}/${repo}/compare/${candidateSha}...${sourceSha}`);
394
+ if (!["ahead", "identical"].includes(candidateLineage.status)) {
395
+ throw new Error(`source-lock branch ${ref} no longer descends from candidate ${candidateSha}`);
396
+ }
397
+ }
398
+
399
+ const target = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(targetRef)}`);
400
+ const targetSha = target.object?.sha || "";
401
+ const targetLineage = await api(`/repos/${owner}/${repo}/compare/${targetSha}...${sourceSha}`);
402
+ if (["ahead", "identical"].includes(targetLineage.status)) {
403
+ if (current) return current;
377
404
  return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: candidateSha } });
378
405
  }
379
- if (current.object.sha !== candidateSha) {
380
- throw new Error(`source-lock branch ${ref} already points to ${current.object.sha}, not ${candidateSha}`);
406
+
407
+ const identity = { name: "Buildchain Patrol", email: "buildchain-patrol@kungfu.link" };
408
+ const reconciliation = await api(`/repos/${owner}/${repo}/git/commits`, {
409
+ method: "POST",
410
+ body: {
411
+ message: [
412
+ "chore(buildchain): reconcile stable source-lock ancestry",
413
+ "",
414
+ `Preserve the exact candidate tree from ${candidateSha} while admitting ${targetRef} at ${targetSha}.`,
415
+ "",
416
+ "Signed-off-by: Buildchain Patrol <buildchain-patrol@kungfu.link>",
417
+ ].join("\n"),
418
+ tree: candidateCommit.tree.sha,
419
+ parents: [sourceSha, targetSha],
420
+ author: identity,
421
+ committer: identity,
422
+ },
423
+ });
424
+ sourceSha = reconciliation.sha;
425
+ if (!current) {
426
+ return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: sourceSha } });
381
427
  }
382
- return current;
428
+ return api(`/repos/${owner}/${repo}/git/refs/heads/${encodeRef(ref)}`, {
429
+ method: "PATCH",
430
+ body: { sha: sourceSha, force: false },
431
+ });
383
432
  },
384
433
  async ensurePromotionPullRequest({ head, base, title, body }) {
385
434
  const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);