@kungfu-tech/buildchain 2.8.17 → 2.9.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.
@@ -42,6 +42,8 @@ const SUPPORTED_INFRA_ADOPTION_MODES = new Set([
42
42
  "managed-apply",
43
43
  ]);
44
44
  const SUPPORTED_INFRA_APPLY_MODES = new Set(["disabled", "manual-approval", "environment-approval"]);
45
+ const SUPPORTED_FACT_VERSION_SOURCE_TYPES = new Set(["static", "json", "toml", "regex", "command"]);
46
+ const SUPPORTED_FACT_LEGACY_PROJECTIONS = new Set(["kungfu-buildinfo"]);
45
47
 
46
48
  function posixPath(value) {
47
49
  return String(value || "").split(path.sep).join("/");
@@ -154,11 +156,123 @@ export function normalizeBuildchainConfig(config) {
154
156
  if (normalized.diagnostics !== undefined) {
155
157
  normalized.diagnostics = normalizeDiagnosticsSection(normalized.diagnostics);
156
158
  }
159
+ if (normalized.facts !== undefined) {
160
+ normalized.facts = normalizeFactsSection(normalized.facts);
161
+ }
157
162
  validateWebSurfaceConfig(normalized);
158
163
  validateInfraContractConfig(normalized);
159
164
  return normalized;
160
165
  }
161
166
 
167
+ function normalizeFactsSection(facts) {
168
+ assertPlainObject(facts, "facts");
169
+ return {
170
+ outputDir: facts.output_dir === undefined ? ".buildchain/facts" : posixPath(assertString(facts.output_dir, "facts.output_dir")),
171
+ versionSources: normalizeFactVersionSources(facts.version_sources),
172
+ modules: normalizeFactModules(facts.modules),
173
+ products: normalizeFactProducts(facts.products),
174
+ legacyProjections: normalizeFactLegacyProjections(facts.legacy_projections),
175
+ };
176
+ }
177
+
178
+ function normalizeFactVersionSources(sources = []) {
179
+ if (sources === undefined) {
180
+ return [];
181
+ }
182
+ if (!Array.isArray(sources)) {
183
+ throw new Error("facts.version_sources must be an array of tables");
184
+ }
185
+ return sources.map((source, index) => {
186
+ assertPlainObject(source, `facts.version_sources[${index}]`);
187
+ const id = assertString(source.id, `facts.version_sources[${index}].id`);
188
+ const type = source.type === undefined ? "static" : assertString(source.type, `facts.version_sources[${index}].type`);
189
+ if (!SUPPORTED_FACT_VERSION_SOURCE_TYPES.has(type)) {
190
+ throw new Error("facts.version_sources[].type must be one of static, json, toml, regex, or command");
191
+ }
192
+ if (["json", "toml", "regex"].includes(type) && source.path === undefined) {
193
+ throw new Error(`facts.version_sources[${index}].path is required for ${type}`);
194
+ }
195
+ if (["json", "toml"].includes(type) && source.key === undefined) {
196
+ throw new Error(`facts.version_sources[${index}].key is required for ${type}`);
197
+ }
198
+ if (type === "regex" && source.pattern === undefined) {
199
+ throw new Error(`facts.version_sources[${index}].pattern is required for regex`);
200
+ }
201
+ if (type === "command" && source.command === undefined) {
202
+ throw new Error(`facts.version_sources[${index}].command is required for command`);
203
+ }
204
+ return {
205
+ id,
206
+ type,
207
+ value: source.value === undefined ? undefined : String(source.value),
208
+ path: source.path === undefined ? "" : posixPath(assertString(source.path, `facts.version_sources[${index}].path`)),
209
+ key: source.key === undefined ? "" : assertString(source.key, `facts.version_sources[${index}].key`),
210
+ pattern: source.pattern === undefined ? "" : assertString(source.pattern, `facts.version_sources[${index}].pattern`),
211
+ command: source.command === undefined ? "" : assertString(source.command, `facts.version_sources[${index}].command`),
212
+ trust: source.trust === undefined ? "" : assertString(source.trust, `facts.version_sources[${index}].trust`),
213
+ reproducible: optionalBoolean(source.reproducible, type !== "command"),
214
+ };
215
+ });
216
+ }
217
+
218
+ function normalizeFactModules(modules = []) {
219
+ if (modules === undefined) {
220
+ return [];
221
+ }
222
+ if (!Array.isArray(modules)) {
223
+ throw new Error("facts.modules must be an array of tables");
224
+ }
225
+ return modules.map((module, index) => {
226
+ assertPlainObject(module, `facts.modules[${index}]`);
227
+ return {
228
+ id: assertString(module.id, `facts.modules[${index}].id`),
229
+ root: module.root === undefined ? "." : posixPath(assertString(module.root, `facts.modules[${index}].root`)),
230
+ scope: module.scope === undefined ? "" : assertString(module.scope, `facts.modules[${index}].scope`),
231
+ versionSource: module.version_source === undefined ? "" : assertString(module.version_source, `facts.modules[${index}].version_source`),
232
+ lifecycle: module.lifecycle === undefined ? "" : assertString(module.lifecycle, `facts.modules[${index}].lifecycle`),
233
+ outputs: normalizeStringArray(module.outputs, `facts.modules[${index}].outputs`).map(posixPath),
234
+ };
235
+ });
236
+ }
237
+
238
+ function normalizeFactProducts(products = []) {
239
+ if (products === undefined) {
240
+ return [];
241
+ }
242
+ if (!Array.isArray(products)) {
243
+ throw new Error("facts.products must be an array of tables");
244
+ }
245
+ return products.map((product, index) => {
246
+ assertPlainObject(product, `facts.products[${index}]`);
247
+ return {
248
+ id: assertString(product.id, `facts.products[${index}].id`),
249
+ moduleFacts: normalizeStringArray(product.module_facts, `facts.products[${index}].module_facts`).map(posixPath),
250
+ artifacts: normalizeStringArray(product.artifacts, `facts.products[${index}].artifacts`).map(posixPath),
251
+ };
252
+ });
253
+ }
254
+
255
+ function normalizeFactLegacyProjections(projections = []) {
256
+ if (projections === undefined) {
257
+ return [];
258
+ }
259
+ if (!Array.isArray(projections)) {
260
+ throw new Error("facts.legacy_projections must be an array of tables");
261
+ }
262
+ return projections.map((projection, index) => {
263
+ assertPlainObject(projection, `facts.legacy_projections[${index}]`);
264
+ const type = assertString(projection.type, `facts.legacy_projections[${index}].type`);
265
+ if (!SUPPORTED_FACT_LEGACY_PROJECTIONS.has(type)) {
266
+ throw new Error("facts.legacy_projections[].type must be kungfu-buildinfo");
267
+ }
268
+ return {
269
+ type,
270
+ module: projection.module === undefined ? "" : assertString(projection.module, `facts.legacy_projections[${index}].module`),
271
+ path: posixPath(assertString(projection.path, `facts.legacy_projections[${index}].path`)),
272
+ };
273
+ });
274
+ }
275
+
162
276
  function normalizeDiagnosticsSection(diagnostics) {
163
277
  assertPlainObject(diagnostics, "diagnostics");
164
278
  const normalized = { ...diagnostics };
@@ -1062,6 +1176,7 @@ export function validateBuildchainConfig(
1062
1176
  })),
1063
1177
  lifecycleStages,
1064
1178
  publish: loadedConfig.config.publish,
1179
+ facts: loadedConfig.config.facts,
1065
1180
  };
1066
1181
  }
1067
1182
 
@@ -22,6 +22,7 @@ export const BUILDCHAIN_AGENT_MANUALS = Object.freeze([
22
22
  { id: "ownership", title: "Ownership", path: "docs/ownership.md", plane: "why" },
23
23
  { id: "product-mechanism", title: "Product mechanism", path: "docs/product-mechanism.md", plane: "why" },
24
24
  { id: "cli", title: "CLI and npm package", path: "docs/cli.md", plane: "use" },
25
+ { id: "build-facts", title: "Build Facts", path: "docs/build-facts.md", plane: "use" },
25
26
  { id: "lifecycle-protocol", title: "Lifecycle protocol", path: "docs/lifecycle-protocol.md", plane: "use" },
26
27
  { id: "reusable-build-surface", title: "Reusable build surface", path: "docs/reusable-build-surface.md", plane: "use" },
27
28
  { id: "publish-transaction", title: "Publish transaction", path: "docs/publish-transaction.md", plane: "verify" },
@@ -72,6 +73,7 @@ const EXTRA_KFD1_FILES = Object.freeze([
72
73
  "bin/buildchain.mjs",
73
74
  "packages/core/index.js",
74
75
  "packages/core/homebrew.js",
76
+ "packages/core/build-facts.js",
75
77
  "packages/core/release-propagation.js",
76
78
  "scripts/generate-site-bundle.mjs",
77
79
  "scripts/ensure-github-release.mjs",
@@ -86,6 +86,24 @@ export {
86
86
  writeDiagnosticsArtifact,
87
87
  } from "./diagnostics.js";
88
88
 
89
+ export {
90
+ BUILD_FACTS_GIT_CONTRACT,
91
+ BUILD_FACTS_LEGACY_KUNGFU_BUILDINFO_CONTRACT,
92
+ BUILD_FACTS_MODULE_CONTRACT,
93
+ BUILD_FACTS_PRODUCT_CONTRACT,
94
+ BUILD_FACTS_VERIFY_CONTRACT,
95
+ BUILD_FACTS_VERSION_CONTRACT,
96
+ aggregateBuildFacts,
97
+ buildFactsDigest,
98
+ collectGitSourceFacts,
99
+ collectModuleBuildFacts,
100
+ collectVersionSourceFact,
101
+ createKungfuBuildInfoProjection,
102
+ verifyBuildFacts,
103
+ writeBuildFacts,
104
+ writeKungfuBuildInfoProjection,
105
+ } from "./build-facts.js";
106
+
89
107
  export {
90
108
  RELEASE_CANDIDATE_PASSPORT_CONTRACT,
91
109
  createReleaseCandidatePassport,
@@ -194,15 +212,21 @@ export {
194
212
  } from "./issue-reporting.js";
195
213
 
196
214
  export {
215
+ BADGE_BUNDLE_DEFAULT_CLAIMS,
216
+ BADGE_BUNDLE_FACTS_CONTRACT,
197
217
  README_BADGE_BLOCK_END,
198
218
  README_BADGE_BLOCK_START,
199
219
  README_BADGE_FACTS_CONTRACT,
200
220
  README_BADGE_HOSTED_BASE_URL,
221
+ checkBadgeBundleBlock,
201
222
  checkReadmeBadgeBlock,
223
+ collectBadgeBundleFacts,
202
224
  collectReadmeBadgeFacts,
203
225
  createReadmeBadgeEndpointRegistry,
204
226
  readReadme,
227
+ renderBadgeBundleBlock,
205
228
  renderReadmeBadgeBlock,
229
+ updateBadgeBundleBlock,
206
230
  updateReadmeBadgeBlock,
207
231
  } from "./readme-badges.js";
208
232
 
@@ -10,9 +10,16 @@ import {
10
10
  } from "./release-passport.js";
11
11
 
12
12
  export const README_BADGE_FACTS_CONTRACT = "kungfu-buildchain-readme-badge-facts";
13
+ export const BADGE_BUNDLE_FACTS_CONTRACT = "kungfu-buildchain-badge-bundle-facts";
13
14
  export const README_BADGE_BLOCK_START = "<!-- buildchain:badges:start -->";
14
15
  export const README_BADGE_BLOCK_END = "<!-- buildchain:badges:end -->";
15
16
  export const README_BADGE_HOSTED_BASE_URL = "https://buildchain.libkungfu.dev/badges/v1";
17
+ export const BADGE_BUNDLE_DEFAULT_CLAIMS = [
18
+ "kfd-1",
19
+ "kfd-2",
20
+ "kfd-3",
21
+ "release-passport",
22
+ ];
16
23
 
17
24
  const KFD_KEYS = [
18
25
  { key: "kfd-1", id: "kfd1", label: "KFD-1", text: "contract world" },
@@ -47,6 +54,22 @@ const BUILDCHAIN_BADGE_IDS = [
47
54
  "buildchain-release-passport",
48
55
  ];
49
56
 
57
+ const BADGE_BUNDLE_CLAIM_ALIASES = {
58
+ kfd1: "kfd-1",
59
+ "kfd_1": "kfd-1",
60
+ "kfd-1": "kfd-1",
61
+ kfd2: "kfd-2",
62
+ "kfd_2": "kfd-2",
63
+ "kfd-2": "kfd-2",
64
+ kfd3: "kfd-3",
65
+ "kfd_3": "kfd-3",
66
+ "kfd-3": "kfd-3",
67
+ passport: "release-passport",
68
+ "release_passport": "release-passport",
69
+ "release-passport": "release-passport",
70
+ "buildchain-release-passport": "release-passport",
71
+ };
72
+
50
73
  const BUILDCHAIN_BADGE_LOGO_PLACEHOLDER = {
51
74
  contract: "kungfu-buildchain-badge-logo-policy",
52
75
  mode: "hosted-placeholder",
@@ -83,6 +106,10 @@ function sha256File(filePath) {
83
106
  return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
84
107
  }
85
108
 
109
+ function hashText(value) {
110
+ return crypto.createHash("sha256").update(String(value || "")).digest("hex");
111
+ }
112
+
86
113
  function posixPath(value) {
87
114
  return String(value || "").split(path.sep).join("/");
88
115
  }
@@ -174,6 +201,51 @@ function normalizeStringArray(value) {
174
201
  .filter(Boolean);
175
202
  }
176
203
 
204
+ function splitClaimList(value) {
205
+ if (Array.isArray(value)) {
206
+ return value;
207
+ }
208
+ if (typeof value === "string") {
209
+ return value.split(",");
210
+ }
211
+ return [];
212
+ }
213
+
214
+ function normalizeBadgeBundleClaims(value, fallback = BADGE_BUNDLE_DEFAULT_CLAIMS) {
215
+ const selected = [];
216
+ const unknown = [];
217
+ for (const claim of splitClaimList(value)) {
218
+ const key = String(claim || "").trim().toLowerCase();
219
+ if (!key) {
220
+ continue;
221
+ }
222
+ const normalized = BADGE_BUNDLE_CLAIM_ALIASES[key];
223
+ if (!normalized) {
224
+ unknown.push(key);
225
+ continue;
226
+ }
227
+ if (!selected.includes(normalized)) {
228
+ selected.push(normalized);
229
+ }
230
+ }
231
+ if (unknown.length > 0) {
232
+ throw new Error(`unsupported badge bundle claim(s): ${unknown.join(", ")}`);
233
+ }
234
+ return selected.length > 0 ? selected : [...fallback];
235
+ }
236
+
237
+ function badgeBundleConfig(badgeConfig = {}) {
238
+ const configured = badgeConfig.bundle && typeof badgeConfig.bundle === "object"
239
+ ? badgeConfig.bundle
240
+ : {};
241
+ return {
242
+ enabled: configured.enabled === undefined ? true : Boolean(configured.enabled),
243
+ claims: normalizeBadgeBundleClaims(configured.claims || badgeConfig.bundle_claims || badgeConfig.bundleClaims),
244
+ mode: String(configured.mode || "readme"),
245
+ hosted: configured.hosted === undefined ? true : Boolean(configured.hosted),
246
+ };
247
+ }
248
+
177
249
  function resolveLocalFactPath({ cwd, location }) {
178
250
  const value = String(location || "").trim();
179
251
  if (!value || /^https?:\/\//.test(value)) {
@@ -531,6 +603,46 @@ function buildBadgeEntries(facts, { badgeConfig = {} } = {}) {
531
603
  return entries;
532
604
  }
533
605
 
606
+ function buildBadgeBundleEntries(readmeFacts, bundle) {
607
+ const selected = new Set(bundle.claims);
608
+ const entries = [];
609
+ for (const kfd of readmeFacts.kfd) {
610
+ if (!selected.has(kfd.key)) {
611
+ continue;
612
+ }
613
+ entries.push({
614
+ id: kfd.key,
615
+ claim: kfd.key,
616
+ alt: `${kfd.label}: ${kfd.state}`,
617
+ image: readmeFacts.badges.find((badge) => badge.id === kfd.key)?.image || "",
618
+ link: kfd.url,
619
+ state: kfd.state,
620
+ source: kfd.source,
621
+ standard: {
622
+ title: kfd.title,
623
+ documentUrl: kfd.standardDocumentUrl,
624
+ documentSha256: kfd.standardDocumentSha256,
625
+ interfaceContract: kfd.interfaceContract,
626
+ schemaId: kfd.schemaId,
627
+ },
628
+ });
629
+ }
630
+ if (selected.has("release-passport")) {
631
+ const badge = readmeFacts.badges.find((entry) => entry.id === "buildchain-release-passport");
632
+ entries.push({
633
+ id: "buildchain-release-passport",
634
+ claim: "release-passport",
635
+ alt: `Buildchain Release Passport: ${readmeFacts.releasePassport.state}`,
636
+ image: badge?.image || "",
637
+ link: readmeFacts.releasePassport.url,
638
+ state: readmeFacts.releasePassport.state,
639
+ source: readmeFacts.releasePassport.verified ? "release-passport" : "declaration",
640
+ releasePassport: readmeFacts.releasePassport,
641
+ });
642
+ }
643
+ return entries;
644
+ }
645
+
534
646
  export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
535
647
  const resolvedCwd = path.resolve(cwd);
536
648
  const loadedConfig = loadBuildchainConfig(resolvedCwd);
@@ -610,13 +722,46 @@ export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
610
722
  hostedBadgeIds: BUILDCHAIN_BADGE_IDS,
611
723
  logoPolicy: BUILDCHAIN_BADGE_LOGO_PLACEHOLDER,
612
724
  },
725
+ badgeBundle: badgeBundleConfig(badgeConfig),
613
726
  badges: [],
614
727
  };
615
728
  facts.badges = buildBadgeEntries(facts, { badgeConfig });
616
729
  return facts;
617
730
  }
618
731
 
619
- export function renderReadmeBadgeBlock(facts) {
732
+ export async function collectBadgeBundleFacts({ cwd = process.cwd(), claims = undefined } = {}) {
733
+ const readmeFacts = await collectReadmeBadgeFacts({ cwd });
734
+ const config = {
735
+ ...readmeFacts.badgeBundle,
736
+ claims: normalizeBadgeBundleClaims(claims, readmeFacts.badgeBundle.claims),
737
+ };
738
+ const badges = buildBadgeBundleEntries(readmeFacts, config);
739
+ return {
740
+ schemaVersion: 1,
741
+ contract: BADGE_BUNDLE_FACTS_CONTRACT,
742
+ cwd: readmeFacts.cwd,
743
+ repository: readmeFacts.repository,
744
+ package: readmeFacts.package,
745
+ sourceFactsContract: readmeFacts.contract,
746
+ sourceFactsSha256: hashText(JSON.stringify(readmeFacts)),
747
+ policy: {
748
+ defaultClaims: [...BADGE_BUNDLE_DEFAULT_CLAIMS],
749
+ claims: config.claims,
750
+ hosted: config.hosted,
751
+ mode: config.mode,
752
+ passedRequiresRepositoryPassport: true,
753
+ consumerActionForLogoChange: "none",
754
+ },
755
+ releasePassport: readmeFacts.releasePassport,
756
+ kfdStandards: readmeFacts.kfdStandards,
757
+ kfdClaimRegistry: readmeFacts.kfdClaimRegistry,
758
+ productMechanism: readmeFacts.productMechanism,
759
+ badgeRuntime: readmeFacts.badgeRuntime,
760
+ badges,
761
+ };
762
+ }
763
+
764
+ function renderBadgeMarkdownBlock(facts) {
620
765
  const lines = [
621
766
  README_BADGE_BLOCK_START,
622
767
  ...facts.badges.map((badge) => (
@@ -629,12 +774,20 @@ export function renderReadmeBadgeBlock(facts) {
629
774
  return `${lines.join("\n")}\n`;
630
775
  }
631
776
 
777
+ export function renderReadmeBadgeBlock(facts) {
778
+ return renderBadgeMarkdownBlock(facts);
779
+ }
780
+
781
+ export function renderBadgeBundleBlock(facts) {
782
+ return renderBadgeMarkdownBlock(facts);
783
+ }
784
+
632
785
  function badgeBlockRegex() {
633
786
  return new RegExp(`${README_BADGE_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${README_BADGE_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
634
787
  }
635
788
 
636
789
  export function checkReadmeBadgeBlock({ readmeText, facts } = {}) {
637
- const expected = renderReadmeBadgeBlock(facts);
790
+ const expected = renderBadgeMarkdownBlock(facts);
638
791
  const match = String(readmeText || "").match(badgeBlockRegex());
639
792
  const actual = match ? match[0] : "";
640
793
  const normalizedActual = actual.endsWith("\n") ? actual : `${actual}\n`;
@@ -652,6 +805,13 @@ export function checkReadmeBadgeBlock({ readmeText, facts } = {}) {
652
805
  };
653
806
  }
654
807
 
808
+ export function checkBadgeBundleBlock({ readmeText, facts } = {}) {
809
+ return {
810
+ ...checkReadmeBadgeBlock({ readmeText, facts }),
811
+ contract: "kungfu-buildchain-badge-bundle-check",
812
+ };
813
+ }
814
+
655
815
  export function updateReadmeBadgeBlock({ readmeText, facts } = {}) {
656
816
  const source = String(readmeText || "");
657
817
  const block = renderReadmeBadgeBlock(facts);
@@ -665,6 +825,10 @@ export function updateReadmeBadgeBlock({ readmeText, facts } = {}) {
665
825
  return `${block}\n${source}`;
666
826
  }
667
827
 
828
+ export function updateBadgeBundleBlock({ readmeText, facts } = {}) {
829
+ return updateReadmeBadgeBlock({ readmeText, facts });
830
+ }
831
+
668
832
  export function readReadme({ cwd = process.cwd(), readmePath = "README.md" } = {}) {
669
833
  return readTextIfExists(path.resolve(cwd, readmePath));
670
834
  }
@@ -930,6 +930,7 @@ export function createReleasePassport({
930
930
  trustedPublishing = undefined,
931
931
  transaction = undefined,
932
932
  buildSummary = undefined,
933
+ buildFacts = [],
933
934
  platformArtifactManifests = [],
934
935
  distTagPromotionEvidence = undefined,
935
936
  release = {},
@@ -947,6 +948,9 @@ export function createReleasePassport({
947
948
  const normalizedTrustedPublishing = normalizeTrustedPublishing(trustedPublishing, { workflow, publish });
948
949
  const normalizedTransaction = normalizeTransaction(transaction);
949
950
  const normalizedBuildSummary = buildSummary ? normalizeEvidenceDocument(buildSummary, "buildSummary") : undefined;
951
+ const normalizedBuildFacts = (buildFacts || [])
952
+ .map((fact, index) => normalizeEvidenceDocument(fact, `buildFacts[${index}]`))
953
+ .filter(Boolean);
950
954
  const normalizedPlatformArtifactManifests = (platformArtifactManifests || [])
951
955
  .map((manifest, index) => normalizePlatformArtifactManifest(manifest, index))
952
956
  .filter(Boolean);
@@ -1078,6 +1082,7 @@ export function createReleasePassport({
1078
1082
  ...(normalizedTrustedPublishing ? { trustedPublishing: normalizedTrustedPublishing } : {}),
1079
1083
  ...(normalizedTransaction ? { transaction: normalizedTransaction } : {}),
1080
1084
  ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
1085
+ ...(normalizedBuildFacts.length > 0 ? { buildFacts: normalizedBuildFacts } : {}),
1081
1086
  ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
1082
1087
  ...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
1083
1088
  ...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
@@ -1111,6 +1116,13 @@ export function createReleasePassport({
1111
1116
  publishEvidence: publishEvidencePath,
1112
1117
  transactionState: transactionStatePath,
1113
1118
  buildSummary: normalizedBuildSummary?.path || "",
1119
+ buildFacts: normalizedBuildFacts.map((fact) => ({
1120
+ path: fact.path || "",
1121
+ sha256: fact.sha256 || "",
1122
+ contract: fact.fields?.contract || "",
1123
+ id: fact.fields?.id || "",
1124
+ digest: fact.fields?.digest || "",
1125
+ })),
1114
1126
  platformArtifactManifests: normalizedPlatformArtifactManifests.map((manifest) => ({
1115
1127
  path: manifest.path,
1116
1128
  sha256: manifest.sha256,
@@ -1152,6 +1164,7 @@ export function collectGitHubReleasePassport({
1152
1164
  anchorManifestJson = "",
1153
1165
  impactJson = "",
1154
1166
  buildSummaryJson = "",
1167
+ buildFactsJsons = [],
1155
1168
  platformManifestJsons = [],
1156
1169
  distTagEvidenceJson = "",
1157
1170
  kfd1WitnessJsons = [],
@@ -1175,6 +1188,10 @@ export function collectGitHubReleasePassport({
1175
1188
  const anchorManifest = normalizeAnchorManifest(parseJsonInputWithMeta(anchorManifestJson, undefined));
1176
1189
  const impactMeta = parseJsonInputWithMeta(impactJson, undefined);
1177
1190
  const buildSummaryMeta = parseJsonInputWithMeta(buildSummaryJson, undefined);
1191
+ const buildFactMetas = (buildFactsJsons || [])
1192
+ .filter(Boolean)
1193
+ .map((buildFactsJson) => parseJsonInputWithMeta(buildFactsJson, undefined))
1194
+ .filter((meta) => meta.value);
1178
1195
  const platformManifestMetas = (platformManifestJsons || [])
1179
1196
  .filter(Boolean)
1180
1197
  .map((manifestJson) => parseJsonInputWithMeta(manifestJson, undefined));
@@ -1255,6 +1272,10 @@ export function collectGitHubReleasePassport({
1255
1272
  path: buildSummaryMeta.path ? path.relative(resolvedOutputDir, buildSummaryMeta.path).split(path.sep).join("/") : "",
1256
1273
  }
1257
1274
  : undefined,
1275
+ buildFacts: buildFactMetas.map((meta) => ({
1276
+ ...meta,
1277
+ path: meta.path ? path.relative(resolvedOutputDir, meta.path).split(path.sep).join("/") : "",
1278
+ })),
1258
1279
  platformArtifactManifests: platformManifestMetas
1259
1280
  .filter((meta) => meta.value)
1260
1281
  .map((meta) => ({
@@ -15,7 +15,9 @@ const requiredPaths = [
15
15
  ".github/pull_request_template.md",
16
16
  "bin/buildchain.mjs",
17
17
  "packages/core/homebrew.js",
18
+ "packages/core/build-facts.js",
18
19
  "docs/MAP.md",
20
+ "docs/build-facts.md",
19
21
  "docs/binary-distribution.md",
20
22
  "docs/cli.md",
21
23
  "docs/consumer-issue-reporting.md",
@@ -120,6 +122,12 @@ if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-report
120
122
  if (rootPackage.exports?.["./readme-badges"] !== "./packages/core/readme-badges.js") {
121
123
  throw new Error("root package must export @kungfu-tech/buildchain/readme-badges");
122
124
  }
125
+ if (rootPackage.exports?.["./badges"] !== "./packages/core/badges.js") {
126
+ throw new Error("root package must export @kungfu-tech/buildchain/badges");
127
+ }
128
+ if (rootPackage.exports?.["./build-facts"] !== "./packages/core/build-facts.js") {
129
+ throw new Error("root package must export @kungfu-tech/buildchain/build-facts");
130
+ }
123
131
  if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
124
132
  throw new Error("root package must export @kungfu-tech/buildchain/logging");
125
133
  }
@@ -210,6 +218,20 @@ if (!coreIndexSource.includes("createSurfaceTimestampPolicy")) {
210
218
  throw new Error("packages/core/index.js must export surface manifest timestamp policy APIs");
211
219
  }
212
220
  for (const requiredSnippet of [
221
+ "collectModuleBuildFacts",
222
+ "aggregateBuildFacts",
223
+ "verifyBuildFacts",
224
+ "writeKungfuBuildInfoProjection",
225
+ ]) {
226
+ if (!coreIndexSource.includes(requiredSnippet)) {
227
+ throw new Error(`packages/core/index.js must export Build Facts API: ${requiredSnippet}`);
228
+ }
229
+ }
230
+ for (const requiredSnippet of [
231
+ "collectBadgeBundleFacts",
232
+ "renderBadgeBundleBlock",
233
+ "checkBadgeBundleBlock",
234
+ "updateBadgeBundleBlock",
213
235
  "collectReadmeBadgeFacts",
214
236
  "renderReadmeBadgeBlock",
215
237
  "checkReadmeBadgeBlock",
@@ -387,6 +409,8 @@ for (const requiredSnippet of [
387
409
  "Verification fails closed",
388
410
  "KFD-2 release trust passport audit",
389
411
  "Unbound public claims fail",
412
+ "buildFacts[]",
413
+ "--build-facts-json",
390
414
  "Floating Buildchain contract lock",
391
415
  "buildchain.contract-lock.json",
392
416
  "--kfd-3-prebuild-witness-json",
@@ -403,6 +427,7 @@ for (const requiredSnippet of [
403
427
  "KFD-1 / KFD-2 / KFD-3",
404
428
  "floating `@v2`",
405
429
  "npm publish transactions",
430
+ "Git/source/version/module/product build facts",
406
431
  "GitHub Release",
407
432
  "release propagation",
408
433
  "Homebrew tap distribution indexes",
@@ -416,6 +441,11 @@ for (const requiredSnippet of [
416
441
  }
417
442
  const readmeBadgesDoc = fs.readFileSync(path.join(root, "docs/readme-badges.md"), "utf8");
418
443
  for (const requiredSnippet of [
444
+ "buildchain badges bundle --check",
445
+ "collectBadgeBundleFacts",
446
+ "kungfu-buildchain-badge-bundle-facts",
447
+ "@kungfu-tech/buildchain/badges",
448
+ "[badges.bundle]",
419
449
  "buildchain badges readme --check",
420
450
  "collectReadmeBadgeFacts",
421
451
  "kungfu-buildchain-readme-badge-facts",
@@ -472,8 +502,11 @@ for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs
472
502
  }
473
503
  }
474
504
  for (const requiredSnippet of [
505
+ "buildchain badges bundle --check",
506
+ "buildchain badges bundle --write",
475
507
  "buildchain badges readme --check",
476
508
  "buildchain badges readme --write",
509
+ "@kungfu-tech/buildchain/badges",
477
510
  "@kungfu-tech/buildchain/readme-badges",
478
511
  "buildchain homebrew update-formula",
479
512
  "buildchain homebrew check",
@@ -553,12 +586,10 @@ for (const requiredSnippet of [
553
586
  "release-passport-buildchain-self-kfd: true",
554
587
  "publish-required-artifacts-json: \"[]\"",
555
588
  "release-passport-impact-json: >-",
556
- "Buildchain v2.8 promotes KFD self-verification",
557
- "kfd-1-contract-world-release-gate",
558
- "kfd-2-release-trust-passport-audit",
559
- "kfd-3-collaboration-interface-trust-proof",
560
- "publish-source-lock-enforcement",
561
- "required-check-protection",
589
+ "Buildchain v2.9 adds Build Facts as a public contract",
590
+ "build-facts-contract",
591
+ "trust-badge-bundle",
592
+ "web-surface-directory-index-aliases",
562
593
  "\"surfaceImpacts\":[",
563
594
  ]) {
564
595
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -294,6 +294,7 @@ function buildSiteBundle() {
294
294
  { id: "verify-infra-contract-evidence-bundle", usage: "buildchain verify infra-contract-evidence-bundle <file>", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
295
295
  { id: "logging", usage: "buildchain log|mark|span|verify observability-log", purpose: "Emit timestamped build events, summarize logs, and enforce required phases." },
296
296
  { id: "diagnostics-summary", usage: "buildchain diagnostics summary <diagnostics.json>...", purpose: "Summarize small diagnostics artifacts into JSON and a cross-platform lifecycle timing table." },
297
+ { id: "build-facts", usage: "buildchain facts module|aggregate|verify", purpose: "Collect and verify Git source, version, module output, product artifact, and legacy Kungfu buildinfo facts." },
297
298
  { id: "npm-dry-run", usage: "buildchain npm dry-run --json", purpose: "Verify npm publish shape before a release transaction." },
298
299
  { id: "infra-contract", usage: "buildchain infra-contract --mode validate|ci|plan|contract|propagation-plan|propagation-apply|apply|evidence-bundle", purpose: "Validate and publish provider-neutral infrastructure contract evidence with a mutation-free CI evidence chain, provider command plans, configured provider command execution, saved-plan apply gates, dry-run-first propagation, and lifecycle evidence bundles." },
299
300
  { id: "homebrew", usage: "buildchain homebrew update-formula|check", purpose: "Generate and verify Homebrew tap Formula metadata as a distribution-index projection of upstream release passport evidence." },
@@ -316,6 +317,7 @@ function buildSiteBundle() {
316
317
  "docs/MAP.md",
317
318
  "docs/install.md",
318
319
  "docs/cli.md",
320
+ "docs/build-facts.md",
319
321
  "docs/reusable-build-surface.md",
320
322
  "docs/release-candidate.md",
321
323
  "docs/release-governance.md",
@@ -361,6 +363,7 @@ function buildSiteBundle() {
361
363
  })),
362
364
  docs: [
363
365
  { id: "cli-and-node-package", path: "docs/cli.md", digest: sha256File("docs/cli.md") },
366
+ { id: "build-facts", path: "docs/build-facts.md", digest: sha256File("docs/build-facts.md") },
364
367
  { id: "readme-badges", path: "docs/readme-badges.md", digest: sha256File("docs/readme-badges.md") },
365
368
  { id: "homebrew", path: "docs/homebrew.md", digest: sha256File("docs/homebrew.md") },
366
369
  { id: "site-bundle-contract", path: "docs/site-bundle-contract.md", digest: sha256File("docs/site-bundle-contract.md") },