@kungfu-tech/buildchain 2.8.15 → 2.8.16-alpha.2

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.
@@ -0,0 +1,450 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ import { loadBuildchainConfig } from "./buildchain-config.js";
6
+ import {
7
+ readJsonFromLocation,
8
+ verifyReleasePassport,
9
+ } from "./release-passport.js";
10
+
11
+ export const README_BADGE_FACTS_CONTRACT = "kungfu-buildchain-readme-badge-facts";
12
+ export const README_BADGE_BLOCK_START = "<!-- buildchain:badges:start -->";
13
+ export const README_BADGE_BLOCK_END = "<!-- buildchain:badges:end -->";
14
+
15
+ const KFD_KEYS = [
16
+ { key: "kfd-1", id: "kfd1", label: "KFD-1", text: "contract world" },
17
+ { key: "kfd-2", id: "kfd2", label: "KFD-2", text: "trust passport" },
18
+ { key: "kfd-3", id: "kfd3", label: "KFD-3", text: "collaboration interface" },
19
+ ];
20
+
21
+ const STATE_COLORS = {
22
+ passed: "2ea44f",
23
+ aligned: "0969da",
24
+ declared: "6e7781",
25
+ planned: "bf8700",
26
+ draft: "8250df",
27
+ downgraded: "bf8700",
28
+ failed: "cf222e",
29
+ missing: "6e7781",
30
+ unknown: "6e7781",
31
+ };
32
+
33
+ function readJsonIfExists(filePath) {
34
+ if (!filePath || !fs.existsSync(filePath)) {
35
+ return undefined;
36
+ }
37
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
38
+ }
39
+
40
+ function readTextIfExists(filePath) {
41
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
42
+ }
43
+
44
+ function sha256File(filePath) {
45
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
46
+ }
47
+
48
+ function posixPath(value) {
49
+ return String(value || "").split(path.sep).join("/");
50
+ }
51
+
52
+ function encodeBadge(value) {
53
+ return encodeURIComponent(String(value || ""))
54
+ .replace(/-/g, "--")
55
+ .replace(/_/g, "__");
56
+ }
57
+
58
+ function badgeUrl({ label, message, color }) {
59
+ return `https://img.shields.io/badge/${encodeBadge(label)}-${encodeBadge(message)}-${color || "6e7781"}.svg`;
60
+ }
61
+
62
+ function normalizeState(value, fallback = "planned") {
63
+ const normalized = String(value || fallback).trim().toLowerCase();
64
+ if (["passed", "aligned", "declared", "planned", "draft", "downgraded", "failed", "missing"].includes(normalized)) {
65
+ return normalized;
66
+ }
67
+ return fallback;
68
+ }
69
+
70
+ function normalizeDeclarationState(value, fallback = "planned") {
71
+ const normalized = normalizeState(value, fallback);
72
+ return normalized === "passed" ? "declared" : normalized;
73
+ }
74
+
75
+ function normalizeStringArray(value) {
76
+ if (!Array.isArray(value)) {
77
+ return [];
78
+ }
79
+ return value
80
+ .map((entry) => String(entry || "").trim())
81
+ .filter(Boolean);
82
+ }
83
+
84
+ function readRepositoryFromGit(cwd) {
85
+ const remote = spawnSync("git", ["config", "--get", "remote.origin.url"], {
86
+ cwd,
87
+ encoding: "utf8",
88
+ });
89
+ const value = remote.status === 0 ? remote.stdout.trim() : "";
90
+ const match = value.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?$/);
91
+ if (!match) {
92
+ return { fullName: "", owner: "", name: "", url: "" };
93
+ }
94
+ const fullName = `${match[1]}/${match[2]}`;
95
+ return {
96
+ fullName,
97
+ owner: match[1],
98
+ name: match[2],
99
+ url: `https://github.com/${fullName}`,
100
+ };
101
+ }
102
+
103
+ function normalizePackageRepository(repository) {
104
+ const value = typeof repository === "string" ? repository : repository?.url || "";
105
+ const match = String(value).match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?/);
106
+ if (!match) {
107
+ return undefined;
108
+ }
109
+ const fullName = `${match[1]}/${match[2]}`;
110
+ return {
111
+ fullName,
112
+ owner: match[1],
113
+ name: match[2],
114
+ url: `https://github.com/${fullName}`,
115
+ };
116
+ }
117
+
118
+ function discoverWorkflowFacts({ cwd, repository, badgeConfig = {} }) {
119
+ const workflowDir = path.join(cwd, ".github", "workflows");
120
+ const configured = normalizeStringArray(badgeConfig.workflows);
121
+ const defaults = ["verify.yml", "build.yml", "buildchain-ref-promotion.yml"];
122
+ const names = configured.length > 0 ? configured : defaults;
123
+ if (!fs.existsSync(workflowDir)) {
124
+ return [];
125
+ }
126
+ return names
127
+ .map((file) => {
128
+ const workflowPath = path.join(workflowDir, file);
129
+ if (!fs.existsSync(workflowPath)) {
130
+ return undefined;
131
+ }
132
+ const source = fs.readFileSync(workflowPath, "utf8");
133
+ const nameMatch = source.match(/^name:\s*["']?(.+?)["']?\s*$/m);
134
+ const name = String(nameMatch?.[1] || path.basename(file, path.extname(file))).trim();
135
+ return {
136
+ file,
137
+ name,
138
+ badgeUrl: repository.fullName
139
+ ? `https://github.com/${repository.fullName}/actions/workflows/${file}/badge.svg`
140
+ : "",
141
+ url: repository.fullName
142
+ ? `https://github.com/${repository.fullName}/actions/workflows/${file}`
143
+ : "",
144
+ };
145
+ })
146
+ .filter(Boolean);
147
+ }
148
+
149
+ function defaultReleasePassportLocation({ cwd, repository, badgeConfig = {} }) {
150
+ const configured = badgeConfig.release_passport_url || badgeConfig.releasePassportUrl || badgeConfig.release_passport || "";
151
+ if (configured) {
152
+ return String(configured);
153
+ }
154
+ const localCandidates = [
155
+ "buildchain.release.json",
156
+ ".buildchain/release-passport/buildchain.release.json",
157
+ ];
158
+ for (const candidate of localCandidates) {
159
+ const filePath = path.join(cwd, candidate);
160
+ if (fs.existsSync(filePath)) {
161
+ return candidate;
162
+ }
163
+ }
164
+ return repository.fullName
165
+ ? `https://github.com/${repository.fullName}/releases/latest/download/buildchain.release.json`
166
+ : "";
167
+ }
168
+
169
+ function siblingUrl(location, filename) {
170
+ if (!/^https?:\/\//.test(location)) {
171
+ return "";
172
+ }
173
+ return `${location.replace(/\/[^/]*$/, "")}/${filename}`;
174
+ }
175
+
176
+ async function readPassportAndReport({ cwd, location }) {
177
+ if (!location) {
178
+ return { passport: undefined, report: undefined, error: "" };
179
+ }
180
+ const resolvedLocation = /^https?:\/\//.test(location) ? location : path.resolve(cwd, location);
181
+ try {
182
+ const passport = await readJsonFromLocation(resolvedLocation);
183
+ const report = await verifyReleasePassport({
184
+ passportLocation: resolvedLocation,
185
+ artifactEvidenceLocation: siblingUrl(resolvedLocation, "artifact-evidence.json"),
186
+ impactLocation: siblingUrl(resolvedLocation, "impact.json"),
187
+ agentIndexLocation: siblingUrl(resolvedLocation, "agent-index.json"),
188
+ productMechanismLocation: siblingUrl(resolvedLocation, "product-mechanism.json"),
189
+ });
190
+ return { passport, report, error: "" };
191
+ } catch (error) {
192
+ return { passport: undefined, report: undefined, error: error.message };
193
+ }
194
+ }
195
+
196
+ function kfdSectionPassed({ passport, report, key }) {
197
+ if (!passport || !report?.ok) {
198
+ return false;
199
+ }
200
+ const section = passport[key] || (key === "kfd-1" ? passport.kfd1 : undefined) || (key === "kfd-3" ? passport.kfd3 : undefined);
201
+ return section?.status === "passed";
202
+ }
203
+
204
+ function declaredKfdState({ badgeConfig = {}, key, id }) {
205
+ const kfdConfig = badgeConfig.kfd && typeof badgeConfig.kfd === "object" ? badgeConfig.kfd : {};
206
+ return normalizeDeclarationState(
207
+ badgeConfig[key] ||
208
+ badgeConfig[id] ||
209
+ badgeConfig[key.replace("-", "_")] ||
210
+ kfdConfig[key] ||
211
+ kfdConfig[id] ||
212
+ kfdConfig[key.replace("kfd-", "")],
213
+ "planned",
214
+ );
215
+ }
216
+
217
+ function releasePassportState({ report, error, badgeConfig = {} }) {
218
+ if (report?.ok) {
219
+ return "passed";
220
+ }
221
+ if (error) {
222
+ return normalizeDeclarationState(badgeConfig.release_passport_state || badgeConfig.releasePassportState, "declared");
223
+ }
224
+ return normalizeDeclarationState(badgeConfig.release_passport_state || badgeConfig.releasePassportState, "planned");
225
+ }
226
+
227
+ function licenseFromFiles(cwd, packageJson) {
228
+ if (packageJson.license) {
229
+ return String(packageJson.license);
230
+ }
231
+ const licenseFile = fs.readdirSync(cwd).find((entry) => /^licen[sc]e($|\.)/i.test(entry));
232
+ return licenseFile ? licenseFile.replace(/^LICENSE[.-]?/i, "") || "present" : "";
233
+ }
234
+
235
+ function collectPlatformFacts({ badgeConfig = {}, passport = undefined }) {
236
+ const configured = normalizeStringArray(badgeConfig.platforms);
237
+ if (configured.length > 0) {
238
+ return configured;
239
+ }
240
+ const assets = Array.isArray(passport?.artifacts) ? passport.artifacts : [];
241
+ const platforms = new Set();
242
+ for (const asset of assets) {
243
+ const platform = String(asset.platform || "").trim();
244
+ if (platform) {
245
+ platforms.add(platform);
246
+ }
247
+ }
248
+ return [...platforms].sort();
249
+ }
250
+
251
+ function localFactFileSummary(cwd, candidates = []) {
252
+ for (const relPath of candidates) {
253
+ const filePath = path.join(cwd, relPath);
254
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
255
+ continue;
256
+ }
257
+ try {
258
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
259
+ return {
260
+ location: relPath,
261
+ sha256: sha256File(filePath),
262
+ contract: value.contract || "",
263
+ claimCount: Array.isArray(value.publicClaims) ? value.publicClaims.length : undefined,
264
+ category: value.category || "",
265
+ };
266
+ } catch (error) {
267
+ return {
268
+ location: relPath,
269
+ error: error.message,
270
+ };
271
+ }
272
+ }
273
+ return undefined;
274
+ }
275
+
276
+ function buildBadgeEntries(facts) {
277
+ const entries = [];
278
+ for (const kfd of facts.kfd) {
279
+ entries.push({
280
+ id: kfd.key,
281
+ alt: `${kfd.label}: ${kfd.state}`,
282
+ image: badgeUrl({
283
+ label: kfd.label,
284
+ message: `${kfd.text} ${kfd.state}`,
285
+ color: STATE_COLORS[kfd.state] || STATE_COLORS.unknown,
286
+ }),
287
+ link: kfd.url,
288
+ });
289
+ }
290
+ entries.push({
291
+ id: "release-passport",
292
+ alt: `Release Passport: ${facts.releasePassport.state}`,
293
+ image: badgeUrl({
294
+ label: "release passport",
295
+ message: facts.releasePassport.state,
296
+ color: STATE_COLORS[facts.releasePassport.state] || STATE_COLORS.unknown,
297
+ }),
298
+ link: facts.releasePassport.url,
299
+ });
300
+ if (facts.package.license) {
301
+ entries.push({
302
+ id: "license",
303
+ alt: `License: ${facts.package.license}`,
304
+ image: badgeUrl({ label: "license", message: facts.package.license, color: "0969da" }),
305
+ link: facts.repository.url ? `${facts.repository.url}/blob/HEAD/LICENSE` : "",
306
+ });
307
+ }
308
+ if (facts.platforms.length > 0) {
309
+ entries.push({
310
+ id: "platform",
311
+ alt: `Platform: ${facts.platforms.join(" | ")}`,
312
+ image: badgeUrl({ label: "platform", message: facts.platforms.join(" | "), color: "6e7781" }),
313
+ link: facts.releasePassport.url,
314
+ });
315
+ }
316
+ for (const workflow of facts.workflows) {
317
+ entries.push({
318
+ id: `workflow:${workflow.file}`,
319
+ alt: workflow.name,
320
+ image: workflow.badgeUrl,
321
+ link: workflow.url,
322
+ });
323
+ }
324
+ return entries;
325
+ }
326
+
327
+ export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
328
+ const resolvedCwd = path.resolve(cwd);
329
+ const loadedConfig = loadBuildchainConfig(resolvedCwd);
330
+ const badgeConfig = loadedConfig?.config?.badges && typeof loadedConfig.config.badges === "object"
331
+ ? loadedConfig.config.badges
332
+ : {};
333
+ const packageJson = readJsonIfExists(path.join(resolvedCwd, "package.json")) || {};
334
+ const packageRepository = normalizePackageRepository(packageJson.repository);
335
+ const repository = packageRepository || readRepositoryFromGit(resolvedCwd);
336
+ const releasePassportLocation = defaultReleasePassportLocation({ cwd: resolvedCwd, repository, badgeConfig });
337
+ const { passport, report, error } = await readPassportAndReport({
338
+ cwd: resolvedCwd,
339
+ location: releasePassportLocation,
340
+ });
341
+ const releasePassportUrl = /^https?:\/\//.test(releasePassportLocation)
342
+ ? releasePassportLocation
343
+ : repository.fullName && releasePassportLocation
344
+ ? `${repository.url}/blob/HEAD/${posixPath(releasePassportLocation)}`
345
+ : releasePassportLocation;
346
+ const releaseState = releasePassportState({ report, error, badgeConfig });
347
+ const kfd = KFD_KEYS.map((entry) => {
348
+ const passed = kfdSectionPassed({ passport, report, key: entry.key });
349
+ const state = passed ? "passed" : declaredKfdState({ badgeConfig, key: entry.key, id: entry.id });
350
+ return {
351
+ key: entry.key,
352
+ label: entry.label,
353
+ text: entry.text,
354
+ state,
355
+ source: passed ? "release-passport" : "declaration",
356
+ url: releasePassportUrl,
357
+ };
358
+ });
359
+ const facts = {
360
+ schemaVersion: 1,
361
+ contract: README_BADGE_FACTS_CONTRACT,
362
+ cwd: resolvedCwd,
363
+ repository,
364
+ package: {
365
+ name: packageJson.name || "",
366
+ version: packageJson.version || "",
367
+ license: licenseFromFiles(resolvedCwd, packageJson),
368
+ },
369
+ releasePassport: {
370
+ location: releasePassportLocation,
371
+ url: releasePassportUrl,
372
+ state: releaseState,
373
+ verified: Boolean(report?.ok),
374
+ error,
375
+ reportSummary: report ? {
376
+ ok: report.ok,
377
+ trust: report.trust,
378
+ issueCount: Array.isArray(report.issues) ? report.issues.length : 0,
379
+ } : undefined,
380
+ },
381
+ kfdClaimRegistry: localFactFileSummary(resolvedCwd, [
382
+ "dist/site/kfd-claims.json",
383
+ ".buildchain/kfd-claims.json",
384
+ ]),
385
+ productMechanism: localFactFileSummary(resolvedCwd, [
386
+ "dist/site/product-mechanism.json",
387
+ "product-mechanism.json",
388
+ ".buildchain/product-mechanism.json",
389
+ ]),
390
+ kfd,
391
+ platforms: collectPlatformFacts({ badgeConfig, passport }),
392
+ workflows: discoverWorkflowFacts({ cwd: resolvedCwd, repository, badgeConfig }),
393
+ badges: [],
394
+ };
395
+ facts.badges = buildBadgeEntries(facts);
396
+ return facts;
397
+ }
398
+
399
+ export function renderReadmeBadgeBlock(facts) {
400
+ const lines = [
401
+ README_BADGE_BLOCK_START,
402
+ ...facts.badges.map((badge) => (
403
+ badge.link
404
+ ? `[![${badge.alt}](${badge.image})](${badge.link})`
405
+ : `![${badge.alt}](${badge.image})`
406
+ )),
407
+ README_BADGE_BLOCK_END,
408
+ ];
409
+ return `${lines.join("\n")}\n`;
410
+ }
411
+
412
+ function badgeBlockRegex() {
413
+ return new RegExp(`${README_BADGE_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${README_BADGE_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
414
+ }
415
+
416
+ export function checkReadmeBadgeBlock({ readmeText, facts } = {}) {
417
+ const expected = renderReadmeBadgeBlock(facts);
418
+ const match = String(readmeText || "").match(badgeBlockRegex());
419
+ const actual = match ? match[0] : "";
420
+ const normalizedActual = actual.endsWith("\n") ? actual : `${actual}\n`;
421
+ const ok = normalizedActual === expected;
422
+ return {
423
+ schemaVersion: 1,
424
+ contract: "kungfu-buildchain-readme-badge-check",
425
+ ok,
426
+ missing: !match,
427
+ stale: Boolean(match) && !ok,
428
+ expected,
429
+ actual,
430
+ facts,
431
+ message: ok ? "README badge block is current" : (match ? "README badge block is stale" : "README badge block is missing"),
432
+ };
433
+ }
434
+
435
+ export function updateReadmeBadgeBlock({ readmeText, facts } = {}) {
436
+ const source = String(readmeText || "");
437
+ const block = renderReadmeBadgeBlock(facts);
438
+ if (badgeBlockRegex().test(source)) {
439
+ return source.replace(badgeBlockRegex(), block);
440
+ }
441
+ const h1 = source.match(/^# .+\n+/);
442
+ if (h1) {
443
+ return `${h1[0]}${block}\n${source.slice(h1[0].length)}`;
444
+ }
445
+ return `${block}\n${source}`;
446
+ }
447
+
448
+ export function readReadme({ cwd = process.cwd(), readmePath = "README.md" } = {}) {
449
+ return readTextIfExists(path.resolve(cwd, readmePath));
450
+ }
@@ -1465,10 +1465,16 @@ function validateKfd2ReleaseTrustPassportAudit(section, issues) {
1465
1465
  }
1466
1466
  }
1467
1467
 
1468
- function resolveSiblingJson(basePath, relativePath) {
1469
- if (!basePath || !relativePath || /^https?:\/\//.test(relativePath)) {
1468
+ async function resolveSiblingJson(basePath, relativePath) {
1469
+ if (!basePath || !relativePath) {
1470
1470
  return undefined;
1471
1471
  }
1472
+ if (/^https?:\/\//.test(relativePath)) {
1473
+ return readJsonFromLocation(relativePath);
1474
+ }
1475
+ if (/^https?:\/\//.test(basePath)) {
1476
+ return readJsonFromLocation(new URL(relativePath, basePath).toString());
1477
+ }
1472
1478
  const candidate = path.resolve(path.dirname(basePath), relativePath);
1473
1479
  if (!fs.existsSync(candidate)) {
1474
1480
  return undefined;
@@ -1789,13 +1795,22 @@ export function createReleaseCheckReport({
1789
1795
  };
1790
1796
  }
1791
1797
 
1792
- export async function readJsonFromLocation(location) {
1798
+ export async function readJsonFromLocation(location, redirectCount = 0) {
1793
1799
  const input = nonEmptyString(location, "location");
1800
+ if (redirectCount > 5) {
1801
+ throw new Error(`too many redirects while reading ${input}`);
1802
+ }
1794
1803
  if (/^https?:\/\//.test(input)) {
1795
1804
  const client = input.startsWith("https:") ? https : http;
1796
1805
  return new Promise((resolve, reject) => {
1797
1806
  client
1798
1807
  .get(input, (response) => {
1808
+ if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
1809
+ const nextLocation = new URL(response.headers.location, input).toString();
1810
+ response.resume();
1811
+ readJsonFromLocation(nextLocation, redirectCount + 1).then(resolve, reject);
1812
+ return;
1813
+ }
1799
1814
  if (response.statusCode < 200 || response.statusCode >= 300) {
1800
1815
  reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
1801
1816
  response.resume();
@@ -1829,27 +1844,27 @@ export async function verifyReleasePassport({
1829
1844
  productMechanismLocation = "",
1830
1845
  } = {}) {
1831
1846
  const passport = await readJsonFromLocation(passportLocation);
1832
- const basePath = /^https?:\/\//.test(passportLocation) ? "" : path.resolve(passportLocation);
1847
+ const basePath = /^https?:\/\//.test(passportLocation) ? passportLocation : path.resolve(passportLocation);
1833
1848
  const artifactEvidence =
1834
1849
  artifactEvidenceLocation
1835
1850
  ? await readJsonFromLocation(artifactEvidenceLocation)
1836
- : resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
1851
+ : await resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
1837
1852
  const publishEvidence =
1838
1853
  publishEvidenceLocation
1839
1854
  ? await readJsonFromLocation(publishEvidenceLocation)
1840
- : resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
1855
+ : await resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
1841
1856
  const impact =
1842
1857
  impactLocation
1843
1858
  ? await readJsonFromLocation(impactLocation)
1844
- : resolveSiblingJson(basePath, passport.evidence?.impact) || {};
1859
+ : await resolveSiblingJson(basePath, passport.evidence?.impact) || {};
1845
1860
  const agentIndex =
1846
1861
  agentIndexLocation
1847
1862
  ? await readJsonFromLocation(agentIndexLocation)
1848
- : resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
1863
+ : await resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
1849
1864
  const productMechanism =
1850
1865
  productMechanismLocation
1851
1866
  ? await readJsonFromLocation(productMechanismLocation)
1852
- : resolveSiblingJson(basePath, passport.product?.mechanism) || {};
1867
+ : await resolveSiblingJson(basePath, passport.product?.mechanism) || {};
1853
1868
  return createReleaseCheckReport({
1854
1869
  passport,
1855
1870
  artifactEvidence,
@@ -14,12 +14,15 @@ const requiredPaths = [
14
14
  ".github/ISSUE_TEMPLATE/config.yml",
15
15
  ".github/pull_request_template.md",
16
16
  "bin/buildchain.mjs",
17
+ "packages/core/homebrew.js",
17
18
  "docs/MAP.md",
18
19
  "docs/binary-distribution.md",
19
20
  "docs/cli.md",
20
21
  "docs/consumer-issue-reporting.md",
22
+ "docs/homebrew.md",
21
23
  "docs/install.md",
22
24
  "docs/product-mechanism.md",
25
+ "docs/readme-badges.md",
23
26
  "docs/release-passport.md",
24
27
  "docs/release-propagation.md",
25
28
  "docs/site-bundle-contract.md",
@@ -105,12 +108,18 @@ if (rootPackage.exports?.["."] !== "./packages/core/index.js") {
105
108
  if (rootPackage.exports?.["./diagnostics"] !== "./packages/core/diagnostics.js") {
106
109
  throw new Error("root package must export @kungfu-tech/buildchain/diagnostics");
107
110
  }
111
+ if (rootPackage.exports?.["./homebrew"] !== "./packages/core/homebrew.js") {
112
+ throw new Error("root package must export @kungfu-tech/buildchain/homebrew");
113
+ }
108
114
  if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildchain-contract.js") {
109
115
  throw new Error("root package must export @kungfu-tech/buildchain/buildchain-contract");
110
116
  }
111
117
  if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
112
118
  throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
113
119
  }
120
+ if (rootPackage.exports?.["./readme-badges"] !== "./packages/core/readme-badges.js") {
121
+ throw new Error("root package must export @kungfu-tech/buildchain/readme-badges");
122
+ }
114
123
  if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
115
124
  throw new Error("root package must export @kungfu-tech/buildchain/logging");
116
125
  }
@@ -199,6 +208,26 @@ if (!coreIndexSource.includes("KFD2_TRUST_PROOF_CONTRACT")) {
199
208
  if (!coreIndexSource.includes("createSurfaceTimestampPolicy")) {
200
209
  throw new Error("packages/core/index.js must export surface manifest timestamp policy APIs");
201
210
  }
211
+ for (const requiredSnippet of [
212
+ "collectReadmeBadgeFacts",
213
+ "renderReadmeBadgeBlock",
214
+ "checkReadmeBadgeBlock",
215
+ "updateReadmeBadgeBlock",
216
+ ]) {
217
+ if (!coreIndexSource.includes(requiredSnippet)) {
218
+ throw new Error(`packages/core/index.js must export README badge API: ${requiredSnippet}`);
219
+ }
220
+ }
221
+ for (const requiredSnippet of [
222
+ "collectHomebrewTapFacts",
223
+ "renderHomebrewFormula",
224
+ "checkHomebrewTap",
225
+ "updateHomebrewTap",
226
+ ]) {
227
+ if (!coreIndexSource.includes(requiredSnippet)) {
228
+ throw new Error(`packages/core/index.js must export Homebrew API: ${requiredSnippet}`);
229
+ }
230
+ }
202
231
  if (siteBundle.contract !== "kungfu-buildchain-site-bundle") {
203
232
  throw new Error("buildchain-site.json must expose the Buildchain site bundle contract");
204
233
  }
@@ -374,13 +403,41 @@ for (const requiredSnippet of [
374
403
  "npm publish transactions",
375
404
  "GitHub Release",
376
405
  "release propagation",
406
+ "Homebrew tap distribution indexes",
377
407
  "manual-registry.json",
378
408
  "node-api-registry.json",
409
+ "README badge",
379
410
  ]) {
380
411
  if (!docsMap.includes(requiredSnippet)) {
381
412
  throw new Error(`documentation map missing capability coverage snippet: ${requiredSnippet}`);
382
413
  }
383
414
  }
415
+ const readmeBadgesDoc = fs.readFileSync(path.join(root, "docs/readme-badges.md"), "utf8");
416
+ for (const requiredSnippet of [
417
+ "buildchain badges readme --check",
418
+ "collectReadmeBadgeFacts",
419
+ "kungfu-buildchain-readme-badge-facts",
420
+ "KFD passed",
421
+ "release passport",
422
+ "<!-- buildchain:badges:start -->",
423
+ ]) {
424
+ if (!readmeBadgesDoc.includes(requiredSnippet)) {
425
+ throw new Error(`README badges doc missing required snippet: ${requiredSnippet}`);
426
+ }
427
+ }
428
+ const homebrewDoc = fs.readFileSync(path.join(root, "docs/homebrew.md"), "utf8");
429
+ for (const requiredSnippet of [
430
+ "project.type = \"distribution-index\"",
431
+ "buildchain homebrew update-formula",
432
+ "buildchain homebrew check",
433
+ "collectHomebrewTapFacts",
434
+ "kungfu-buildchain-homebrew-tap-manifest",
435
+ "KFD passed",
436
+ ]) {
437
+ if (!homebrewDoc.includes(requiredSnippet)) {
438
+ throw new Error(`Homebrew doc missing required snippet: ${requiredSnippet}`);
439
+ }
440
+ }
384
441
  const reusableBuildSurfaceDoc = fs.readFileSync(path.join(root, "docs/reusable-build-surface.md"), "utf8");
385
442
  for (const requiredSnippet of [
386
443
  "Floating Ref Contract Lock",
@@ -403,6 +460,18 @@ for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs
403
460
  }
404
461
  }
405
462
  }
463
+ for (const requiredSnippet of [
464
+ "buildchain badges readme --check",
465
+ "buildchain badges readme --write",
466
+ "@kungfu-tech/buildchain/readme-badges",
467
+ "buildchain homebrew update-formula",
468
+ "buildchain homebrew check",
469
+ "@kungfu-tech/buildchain/homebrew",
470
+ ]) {
471
+ if (!cliDoc.includes(requiredSnippet)) {
472
+ throw new Error(`CLI doc missing README badge command snippet: ${requiredSnippet}`);
473
+ }
474
+ }
406
475
  const releaseLineDryRunScript = fs.readFileSync(path.join(root, "scripts/release-line-dry-run.mjs"), "utf8");
407
476
  const standaloneBinaryScript = fs.readFileSync(path.join(root, "scripts/build-standalone-binary.mjs"), "utf8");
408
477
  for (const requiredSnippet of [
@@ -293,6 +293,7 @@ function buildSiteBundle() {
293
293
  { id: "diagnostics-summary", usage: "buildchain diagnostics summary <diagnostics.json>...", purpose: "Summarize small diagnostics artifacts into JSON and a cross-platform lifecycle timing table." },
294
294
  { id: "npm-dry-run", usage: "buildchain npm dry-run --json", purpose: "Verify npm publish shape before a release transaction." },
295
295
  { 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." },
296
+ { 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." },
296
297
  ],
297
298
  };
298
299
 
@@ -317,6 +318,7 @@ function buildSiteBundle() {
317
318
  "docs/release-governance.md",
318
319
  "docs/release-passport.md",
319
320
  "docs/publish-transaction.md",
321
+ "docs/homebrew.md",
320
322
  "docs/site-bundle-contract.md",
321
323
  "docs/runtime-train-validation.md",
322
324
  "docs/consumer-issue-reporting.md",
@@ -356,6 +358,8 @@ function buildSiteBundle() {
356
358
  })),
357
359
  docs: [
358
360
  { id: "cli-and-node-package", path: "docs/cli.md", digest: sha256File("docs/cli.md") },
361
+ { id: "readme-badges", path: "docs/readme-badges.md", digest: sha256File("docs/readme-badges.md") },
362
+ { id: "homebrew", path: "docs/homebrew.md", digest: sha256File("docs/homebrew.md") },
359
363
  { id: "site-bundle-contract", path: "docs/site-bundle-contract.md", digest: sha256File("docs/site-bundle-contract.md") },
360
364
  ],
361
365
  guidance: "These are the public Node import surfaces shipped by the npm package. Agents should prefer these exports over internal file paths.",
@@ -417,6 +421,14 @@ function buildSiteBundle() {
417
421
  ],
418
422
  owner: "promote-buildchain-ref",
419
423
  },
424
+ distributionIndexes: {
425
+ homebrewTap: {
426
+ projectType: "distribution-index",
427
+ manifest: "tap-manifest.json",
428
+ command: "buildchain homebrew check",
429
+ sourceOfTruth: "upstream release passport and sibling evidence",
430
+ },
431
+ },
420
432
  };
421
433
 
422
434
  const artifactSchemas = {