@kungfu-tech/buildchain 2.8.16-alpha.1 → 2.8.16

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,414 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readJsonFromLocation, sha256Text, verifyReleasePassport } from "./release-passport.js";
4
+
5
+ export const HOMEBREW_TAP_FACTS_CONTRACT = "kungfu-buildchain-homebrew-tap-facts";
6
+ export const HOMEBREW_TAP_CHECK_CONTRACT = "kungfu-buildchain-homebrew-tap-check";
7
+ export const HOMEBREW_TAP_MANIFEST_CONTRACT = "kungfu-buildchain-homebrew-tap-manifest";
8
+
9
+ const DEFAULT_MANIFEST_PATH = "tap-manifest.json";
10
+ const DEFAULT_FORMULA_PATH = "Formula/buildchain.rb";
11
+ const KFD_KEYS = ["kfd-1", "kfd-2", "kfd-3"];
12
+ const SUPPORTED_FORMULA_PLATFORMS = new Set(["darwin-arm64", "linux-x64"]);
13
+
14
+ function readJsonFile(filePath, fallback = undefined) {
15
+ if (!fs.existsSync(filePath)) {
16
+ return fallback;
17
+ }
18
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
19
+ }
20
+
21
+ function writeJsonFile(filePath, value) {
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
24
+ }
25
+
26
+ function writeTextFile(filePath, value) {
27
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
+ fs.writeFileSync(filePath, value.endsWith("\n") ? value : `${value}\n`);
29
+ }
30
+
31
+ function optionalString(value) {
32
+ return value === undefined || value === null ? "" : String(value);
33
+ }
34
+
35
+ function nonEmptyString(value, label) {
36
+ const normalized = optionalString(value).trim();
37
+ if (!normalized) {
38
+ throw new Error(`${label} must be a non-empty string`);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function digestWithoutPrefix(value) {
44
+ return optionalString(value).replace(/^sha256:/, "");
45
+ }
46
+
47
+ function stableJson(value) {
48
+ if (Array.isArray(value)) {
49
+ return `[${value.map(stableJson).join(",")}]`;
50
+ }
51
+ if (value && typeof value === "object") {
52
+ return `{${Object.keys(value)
53
+ .sort()
54
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
55
+ .join(",")}}`;
56
+ }
57
+ return JSON.stringify(value);
58
+ }
59
+
60
+ function formulaClassName(name) {
61
+ return nonEmptyString(name, "package name")
62
+ .replace(/^@[^/]+\//, "")
63
+ .split(/[^A-Za-z0-9]+/)
64
+ .filter(Boolean)
65
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
66
+ .join("");
67
+ }
68
+
69
+ function rubyString(value) {
70
+ return optionalString(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
71
+ }
72
+
73
+ function inferPlatformFromName(name) {
74
+ const lower = optionalString(name).toLowerCase();
75
+ if (lower.includes("apple-darwin") || lower.includes("darwin") || lower.includes("macos")) {
76
+ return lower.includes("aarch64") || lower.includes("arm64") ? "darwin-arm64" : "darwin-x64";
77
+ }
78
+ if (lower.includes("windows") || lower.includes("pc-windows") || lower.endsWith(".zip")) {
79
+ return "windows-x64";
80
+ }
81
+ if (lower.includes("linux") || lower.includes("unknown-linux")) {
82
+ return lower.includes("aarch64") || lower.includes("arm64") ? "linux-arm64" : "linux-x64";
83
+ }
84
+ return "";
85
+ }
86
+
87
+ function normalizeRepository(value = "") {
88
+ const raw = optionalString(value).trim();
89
+ if (!raw) return "";
90
+ const githubMatch = raw.match(/github\.com[:/]([^/\s]+\/[^/\s#]+?)(?:\.git)?(?:[#/?].*)?$/);
91
+ if (githubMatch) return githubMatch[1].replace(/\.git$/, "");
92
+ if (/^[^/\s]+\/[^/\s]+$/.test(raw)) return raw.replace(/\.git$/, "");
93
+ return raw;
94
+ }
95
+
96
+ function releaseDirectoryUrl(releasePassportLocation = "") {
97
+ if (!/^https?:\/\//.test(releasePassportLocation)) return "";
98
+ const url = new URL(releasePassportLocation);
99
+ url.pathname = url.pathname.replace(/\/[^/]*$/, "/");
100
+ return url.toString();
101
+ }
102
+
103
+ function githubAssetUrl(repository, tag, name) {
104
+ const repo = normalizeRepository(repository);
105
+ if (!repo || !tag || !name) return "";
106
+ return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`;
107
+ }
108
+
109
+ function siblingAssetUrl(releasePassportLocation, name) {
110
+ const base = releaseDirectoryUrl(releasePassportLocation);
111
+ return base ? new URL(encodeURIComponent(name), base).toString() : "";
112
+ }
113
+
114
+ function normalizeArtifact(artifact, { releasePassportLocation = "", repository = "", tag = "" } = {}) {
115
+ const name = nonEmptyString(artifact.name || artifact.filename, "artifact.name");
116
+ const platform = optionalString(artifact.platform || inferPlatformFromName(name));
117
+ return {
118
+ name,
119
+ platform,
120
+ url: optionalString(artifact.url || artifact.browser_download_url || artifact.downloadUrl)
121
+ || siblingAssetUrl(releasePassportLocation, name)
122
+ || githubAssetUrl(repository, tag, name),
123
+ sha256: digestWithoutPrefix(artifact.sha256 || artifact.digest || artifact.checksum),
124
+ };
125
+ }
126
+
127
+ function formulaArchiveArtifacts(passport, context) {
128
+ return (passport.artifacts || [])
129
+ .map((artifact) => normalizeArtifact(artifact, context))
130
+ .filter((artifact) => SUPPORTED_FORMULA_PLATFORMS.has(artifact.platform))
131
+ .filter((artifact) => /\.(?:tar\.gz|tgz)$/i.test(artifact.name))
132
+ .sort((left, right) => left.platform.localeCompare(right.platform));
133
+ }
134
+
135
+ function readTapManifest(cwd, manifestPath = DEFAULT_MANIFEST_PATH) {
136
+ const filePath = path.join(cwd, manifestPath);
137
+ const manifest = readJsonFile(filePath, {
138
+ schema: 1,
139
+ contract: HOMEBREW_TAP_MANIFEST_CONTRACT,
140
+ name: "",
141
+ kind: "homebrew-tap",
142
+ entries: [],
143
+ });
144
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
145
+ throw new Error(`${manifestPath} must be a JSON object`);
146
+ }
147
+ if (manifest.schema !== undefined && manifest.schema !== 1) {
148
+ throw new Error(`${manifestPath} schema must be 1`);
149
+ }
150
+ if (manifest.schemaVersion !== undefined && manifest.schemaVersion !== 1) {
151
+ throw new Error(`${manifestPath} schemaVersion must be 1`);
152
+ }
153
+ if (manifest.entries !== undefined && !Array.isArray(manifest.entries)) {
154
+ throw new Error(`${manifestPath} entries must be an array`);
155
+ }
156
+ return manifest;
157
+ }
158
+
159
+ function findManifestEntry(manifest, packageName) {
160
+ return (manifest.entries || []).find((entry) => entry?.type === "formula" && entry.name === packageName);
161
+ }
162
+
163
+ async function verifyPassportLocation(location) {
164
+ try {
165
+ const report = await verifyReleasePassport({ passportLocation: location });
166
+ return { ok: report.ok === true, report, error: "" };
167
+ } catch (error) {
168
+ return { ok: false, report: undefined, error: error.message };
169
+ }
170
+ }
171
+
172
+ function createKfdProjection(passport, verified) {
173
+ return Object.fromEntries(KFD_KEYS.map((key) => [
174
+ key,
175
+ verified && passport?.[key]?.status === "passed" ? "passed" : "unverified",
176
+ ]));
177
+ }
178
+
179
+ function createManifestProjection({
180
+ manifest,
181
+ entry,
182
+ packageName,
183
+ formulaPath,
184
+ releasePassportLocation,
185
+ passport,
186
+ artifacts,
187
+ kfd,
188
+ repository,
189
+ } = {}) {
190
+ const tag = nonEmptyString(passport.release?.tag, "release.tag");
191
+ const version = nonEmptyString(passport.release?.publishedVersion || passport.release?.versionLabel, "release.publishedVersion");
192
+ const repo = normalizeRepository(repository || entry?.upstream?.repository || passport.product?.repository);
193
+ const latestReleasePassportUrl = optionalString(entry?.upstream?.latestReleasePassportUrl)
194
+ || (repo ? `https://github.com/${repo}/releases/latest/download/buildchain.release.json` : "");
195
+ return {
196
+ schema: 1,
197
+ contract: HOMEBREW_TAP_MANIFEST_CONTRACT,
198
+ name: optionalString(manifest.name || (repo ? `${repo.replace(/\/[^/]+$/, "")}/homebrew-tap` : "")),
199
+ kind: "homebrew-tap",
200
+ entries: [
201
+ {
202
+ type: "formula",
203
+ name: packageName,
204
+ path: formulaPath,
205
+ upstream: {
206
+ repository: repo,
207
+ tag,
208
+ releasePassportUrl: releasePassportLocation,
209
+ latestReleasePassportUrl,
210
+ },
211
+ version,
212
+ kfd,
213
+ artifacts: artifacts.map((artifact) => ({
214
+ platform: artifact.platform,
215
+ url: artifact.url,
216
+ sha256: artifact.sha256,
217
+ })),
218
+ },
219
+ ],
220
+ };
221
+ }
222
+
223
+ export async function collectHomebrewTapFacts({
224
+ cwd = process.cwd(),
225
+ packageName = "buildchain",
226
+ releasePassport = "",
227
+ manifestPath = DEFAULT_MANIFEST_PATH,
228
+ formulaPath = "",
229
+ } = {}) {
230
+ const resolvedCwd = path.resolve(cwd);
231
+ const manifest = readTapManifest(resolvedCwd, manifestPath);
232
+ const entry = findManifestEntry(manifest, packageName);
233
+ const selectedFormulaPath = formulaPath || entry?.path || (packageName === "buildchain" ? DEFAULT_FORMULA_PATH : `Formula/${packageName}.rb`);
234
+ const releasePassportLocation = nonEmptyString(
235
+ releasePassport || entry?.upstream?.releasePassportUrl || entry?.releasePassport,
236
+ "release passport",
237
+ );
238
+ const passport = await readJsonFromLocation(releasePassportLocation);
239
+ const verification = await verifyPassportLocation(releasePassportLocation);
240
+ const repository = normalizeRepository(entry?.upstream?.repository || passport.product?.repository);
241
+ const artifacts = formulaArchiveArtifacts(passport, {
242
+ releasePassportLocation,
243
+ repository,
244
+ tag: passport.release?.tag,
245
+ });
246
+ const kfd = createKfdProjection(passport, verification.ok);
247
+ const manifestProjection = createManifestProjection({
248
+ manifest,
249
+ entry,
250
+ packageName,
251
+ formulaPath: selectedFormulaPath,
252
+ releasePassportLocation,
253
+ passport,
254
+ artifacts,
255
+ kfd,
256
+ repository,
257
+ });
258
+ return {
259
+ schemaVersion: 1,
260
+ contract: HOMEBREW_TAP_FACTS_CONTRACT,
261
+ cwd: resolvedCwd,
262
+ package: {
263
+ name: packageName,
264
+ formulaClass: formulaClassName(packageName),
265
+ desc: optionalString(entry?.formula?.desc || "Release passport and build evidence toolkit"),
266
+ homepage: optionalString(entry?.formula?.homepage || passport.product?.homepage || "https://buildchain.libkungfu.dev"),
267
+ license: optionalString(entry?.formula?.license || "Apache-2.0"),
268
+ },
269
+ releasePassport: {
270
+ location: releasePassportLocation,
271
+ verified: verification.ok,
272
+ verificationError: verification.error,
273
+ report: verification.report,
274
+ },
275
+ release: {
276
+ tag: passport.release?.tag || "",
277
+ version: passport.release?.publishedVersion || passport.release?.versionLabel || "",
278
+ repository,
279
+ },
280
+ formula: {
281
+ path: selectedFormulaPath,
282
+ artifacts,
283
+ },
284
+ kfd,
285
+ manifestPath,
286
+ manifestProjection,
287
+ };
288
+ }
289
+
290
+ export function renderHomebrewFormula(facts) {
291
+ if (!facts || facts.contract !== HOMEBREW_TAP_FACTS_CONTRACT) {
292
+ throw new Error("facts must be collected with collectHomebrewTapFacts");
293
+ }
294
+ const artifacts = facts.formula?.artifacts || [];
295
+ const darwinArm64 = artifacts.find((artifact) => artifact.platform === "darwin-arm64");
296
+ const linuxX64 = artifacts.find((artifact) => artifact.platform === "linux-x64");
297
+ if (!darwinArm64 || !linuxX64) {
298
+ throw new Error("Homebrew formula requires darwin-arm64 and linux-x64 tar.gz artifacts");
299
+ }
300
+ return `class ${facts.package.formulaClass} < Formula
301
+ desc "${rubyString(facts.package.desc)}"
302
+ homepage "${rubyString(facts.package.homepage)}"
303
+ version "${rubyString(facts.release.version)}"
304
+ license "${rubyString(facts.package.license)}"
305
+
306
+ if OS.mac? && Hardware::CPU.arm?
307
+ url "${rubyString(darwinArm64.url)}"
308
+ sha256 "${rubyString(darwinArm64.sha256)}"
309
+ elsif OS.linux? && Hardware::CPU.intel?
310
+ url "${rubyString(linuxX64.url)}"
311
+ sha256 "${rubyString(linuxX64.sha256)}"
312
+ else
313
+ odie "${facts.package.formulaClass} Homebrew formula currently supports macOS arm64 and Linux x86_64 binary archives."
314
+ end
315
+
316
+ def install
317
+ bin.install "${rubyString(facts.package.name)}"
318
+ end
319
+
320
+ test do
321
+ assert_match version.to_s, shell_output("#{bin}/${rubyString(facts.package.name)} version")
322
+ end
323
+ end
324
+ `;
325
+ }
326
+
327
+ function checkStatus(ok, id, message, details = {}) {
328
+ return { id, status: ok ? "pass" : "fail", message, details };
329
+ }
330
+
331
+ function sameJson(left, right) {
332
+ return stableJson(left) === stableJson(right);
333
+ }
334
+
335
+ export async function checkHomebrewTap({
336
+ cwd = process.cwd(),
337
+ packageName = "buildchain",
338
+ releasePassport = "",
339
+ manifestPath = DEFAULT_MANIFEST_PATH,
340
+ formulaPath = "",
341
+ } = {}) {
342
+ const facts = await collectHomebrewTapFacts({ cwd, packageName, releasePassport, manifestPath, formulaPath });
343
+ const expectedFormula = renderHomebrewFormula(facts);
344
+ const expectedManifest = facts.manifestProjection;
345
+ const formulaFilePath = path.join(facts.cwd, facts.formula.path);
346
+ const manifestFilePath = path.join(facts.cwd, facts.manifestPath);
347
+ const currentFormula = fs.existsSync(formulaFilePath) ? fs.readFileSync(formulaFilePath, "utf8") : "";
348
+ const currentManifest = readJsonFile(manifestFilePath, undefined);
349
+ const formulaCurrent = currentFormula === expectedFormula;
350
+ const manifestCurrent = currentManifest !== undefined && sameJson(currentManifest, expectedManifest);
351
+ const checks = [
352
+ checkStatus(facts.releasePassport.verified, "upstream-passport.verified", "upstream release passport verifies", {
353
+ error: facts.releasePassport.verificationError,
354
+ }),
355
+ checkStatus((facts.formula.artifacts || []).length >= 2, "formula.artifacts", "formula has required platform artifacts", {
356
+ platforms: (facts.formula.artifacts || []).map((artifact) => artifact.platform),
357
+ }),
358
+ checkStatus(formulaCurrent, "formula.current", "Formula is current with upstream passport projection", {
359
+ path: facts.formula.path,
360
+ expectedSha256: sha256Text(expectedFormula),
361
+ actualSha256: currentFormula ? sha256Text(currentFormula) : "",
362
+ }),
363
+ checkStatus(manifestCurrent, "tap-manifest.current", "tap manifest is current with upstream passport projection", {
364
+ path: facts.manifestPath,
365
+ expectedSha256: sha256Text(`${JSON.stringify(expectedManifest, null, 2)}\n`),
366
+ actualSha256: currentManifest ? sha256Text(`${JSON.stringify(currentManifest, null, 2)}\n`) : "",
367
+ }),
368
+ ...KFD_KEYS.map((key) => checkStatus(
369
+ facts.kfd[key] === "passed",
370
+ `kfd.${key}`,
371
+ `${key} passed is backed by verified upstream release passport`,
372
+ { status: facts.kfd[key] },
373
+ )),
374
+ ];
375
+ return {
376
+ schemaVersion: 1,
377
+ contract: HOMEBREW_TAP_CHECK_CONTRACT,
378
+ cwd: facts.cwd,
379
+ ok: checks.every((check) => check.status === "pass"),
380
+ package: packageName,
381
+ checks,
382
+ facts,
383
+ expected: {
384
+ formula: expectedFormula,
385
+ manifest: expectedManifest,
386
+ },
387
+ };
388
+ }
389
+
390
+ export async function updateHomebrewTap({
391
+ cwd = process.cwd(),
392
+ packageName = "buildchain",
393
+ releasePassport = "",
394
+ manifestPath = DEFAULT_MANIFEST_PATH,
395
+ formulaPath = "",
396
+ write = true,
397
+ } = {}) {
398
+ const facts = await collectHomebrewTapFacts({ cwd, packageName, releasePassport, manifestPath, formulaPath });
399
+ const formula = renderHomebrewFormula(facts);
400
+ const manifest = facts.manifestProjection;
401
+ if (write) {
402
+ writeTextFile(path.join(facts.cwd, facts.formula.path), formula);
403
+ writeJsonFile(path.join(facts.cwd, facts.manifestPath), manifest);
404
+ }
405
+ return {
406
+ schemaVersion: 1,
407
+ contract: "kungfu-buildchain-homebrew-tap-update",
408
+ ok: true,
409
+ written: write ? [facts.formula.path, facts.manifestPath] : [],
410
+ facts,
411
+ formula,
412
+ manifest,
413
+ };
414
+ }
@@ -203,6 +203,16 @@ export {
203
203
  updateReadmeBadgeBlock,
204
204
  } from "./readme-badges.js";
205
205
 
206
+ export {
207
+ HOMEBREW_TAP_CHECK_CONTRACT,
208
+ HOMEBREW_TAP_FACTS_CONTRACT,
209
+ HOMEBREW_TAP_MANIFEST_CONTRACT,
210
+ checkHomebrewTap,
211
+ collectHomebrewTapFacts,
212
+ renderHomebrewFormula,
213
+ updateHomebrewTap,
214
+ } from "./homebrew.js";
215
+
206
216
  export {
207
217
  RELEASE_PROPAGATION_GRAPH_CONTRACT,
208
218
  RELEASE_PROPAGATION_LOCK_CONTRACT,
@@ -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;
@@ -1838,27 +1844,27 @@ export async function verifyReleasePassport({
1838
1844
  productMechanismLocation = "",
1839
1845
  } = {}) {
1840
1846
  const passport = await readJsonFromLocation(passportLocation);
1841
- const basePath = /^https?:\/\//.test(passportLocation) ? "" : path.resolve(passportLocation);
1847
+ const basePath = /^https?:\/\//.test(passportLocation) ? passportLocation : path.resolve(passportLocation);
1842
1848
  const artifactEvidence =
1843
1849
  artifactEvidenceLocation
1844
1850
  ? await readJsonFromLocation(artifactEvidenceLocation)
1845
- : resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
1851
+ : await resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
1846
1852
  const publishEvidence =
1847
1853
  publishEvidenceLocation
1848
1854
  ? await readJsonFromLocation(publishEvidenceLocation)
1849
- : resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
1855
+ : await resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
1850
1856
  const impact =
1851
1857
  impactLocation
1852
1858
  ? await readJsonFromLocation(impactLocation)
1853
- : resolveSiblingJson(basePath, passport.evidence?.impact) || {};
1859
+ : await resolveSiblingJson(basePath, passport.evidence?.impact) || {};
1854
1860
  const agentIndex =
1855
1861
  agentIndexLocation
1856
1862
  ? await readJsonFromLocation(agentIndexLocation)
1857
- : resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
1863
+ : await resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
1858
1864
  const productMechanism =
1859
1865
  productMechanismLocation
1860
1866
  ? await readJsonFromLocation(productMechanismLocation)
1861
- : resolveSiblingJson(basePath, passport.product?.mechanism) || {};
1867
+ : await resolveSiblingJson(basePath, passport.product?.mechanism) || {};
1862
1868
  return createReleaseCheckReport({
1863
1869
  passport,
1864
1870
  artifactEvidence,
@@ -14,10 +14,12 @@ 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",
23
25
  "docs/readme-badges.md",
@@ -106,6 +108,9 @@ if (rootPackage.exports?.["."] !== "./packages/core/index.js") {
106
108
  if (rootPackage.exports?.["./diagnostics"] !== "./packages/core/diagnostics.js") {
107
109
  throw new Error("root package must export @kungfu-tech/buildchain/diagnostics");
108
110
  }
111
+ if (rootPackage.exports?.["./homebrew"] !== "./packages/core/homebrew.js") {
112
+ throw new Error("root package must export @kungfu-tech/buildchain/homebrew");
113
+ }
109
114
  if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildchain-contract.js") {
110
115
  throw new Error("root package must export @kungfu-tech/buildchain/buildchain-contract");
111
116
  }
@@ -213,6 +218,16 @@ for (const requiredSnippet of [
213
218
  throw new Error(`packages/core/index.js must export README badge API: ${requiredSnippet}`);
214
219
  }
215
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
+ }
216
231
  if (siteBundle.contract !== "kungfu-buildchain-site-bundle") {
217
232
  throw new Error("buildchain-site.json must expose the Buildchain site bundle contract");
218
233
  }
@@ -388,6 +403,7 @@ for (const requiredSnippet of [
388
403
  "npm publish transactions",
389
404
  "GitHub Release",
390
405
  "release propagation",
406
+ "Homebrew tap distribution indexes",
391
407
  "manual-registry.json",
392
408
  "node-api-registry.json",
393
409
  "README badge",
@@ -409,6 +425,19 @@ for (const requiredSnippet of [
409
425
  throw new Error(`README badges doc missing required snippet: ${requiredSnippet}`);
410
426
  }
411
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
+ }
412
441
  const reusableBuildSurfaceDoc = fs.readFileSync(path.join(root, "docs/reusable-build-surface.md"), "utf8");
413
442
  for (const requiredSnippet of [
414
443
  "Floating Ref Contract Lock",
@@ -435,6 +464,9 @@ for (const requiredSnippet of [
435
464
  "buildchain badges readme --check",
436
465
  "buildchain badges readme --write",
437
466
  "@kungfu-tech/buildchain/readme-badges",
467
+ "buildchain homebrew update-formula",
468
+ "buildchain homebrew check",
469
+ "@kungfu-tech/buildchain/homebrew",
438
470
  ]) {
439
471
  if (!cliDoc.includes(requiredSnippet)) {
440
472
  throw new Error(`CLI doc missing README badge command snippet: ${requiredSnippet}`);
@@ -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",
@@ -357,6 +359,7 @@ function buildSiteBundle() {
357
359
  docs: [
358
360
  { id: "cli-and-node-package", path: "docs/cli.md", digest: sha256File("docs/cli.md") },
359
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") },
360
363
  { id: "site-bundle-contract", path: "docs/site-bundle-contract.md", digest: sha256File("docs/site-bundle-contract.md") },
361
364
  ],
362
365
  guidance: "These are the public Node import surfaces shipped by the npm package. Agents should prefer these exports over internal file paths.",
@@ -418,6 +421,14 @@ function buildSiteBundle() {
418
421
  ],
419
422
  owner: "promote-buildchain-ref",
420
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
+ },
421
432
  };
422
433
 
423
434
  const artifactSchemas = {