@kungfu-tech/buildchain 3.0.3-alpha.1 → 3.0.3-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.
@@ -1,12 +1,8 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import crypto from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
- import {
7
- loadBuildchainConfig,
8
- validateBuildchainConfig,
9
- } from "./buildchain-config.js";
5
+ import { validateBuildchainConfig } from "./buildchain-config.js";
10
6
  import {
11
7
  createBuildchainContractLock,
12
8
  createBuildchainContractWorld,
@@ -22,6 +18,37 @@ import {
22
18
  PUBLICATION_SEALED_BUNDLE_CONTRACT,
23
19
  verifyPublicationSealedBundle,
24
20
  } from "./publication-sealed-bundle.js";
21
+ import {
22
+ PAPER_PATHS,
23
+ commandResult,
24
+ gitResult,
25
+ gitValue,
26
+ normalizeRepository,
27
+ paperConfig,
28
+ parsePaperVersion,
29
+ readJson,
30
+ resolvePaperRepository,
31
+ sha256Text,
32
+ stableJson,
33
+ } from "./paper-repository.js";
34
+
35
+ export { PAPER_PATHS, resolvePaperRepository } from "./paper-repository.js";
36
+ export {
37
+ PAPER_WORK_START_PLAN_CONTRACT,
38
+ PAPER_WORK_SUBMIT_PLAN_CONTRACT,
39
+ createPaperWorkStartPlan,
40
+ createPaperWorkSubmitPlan,
41
+ executePaperWorkStart,
42
+ executePaperWorkSubmitPush,
43
+ } from "./paper-work.js";
44
+ export {
45
+ PAPER_FLEET_AUDIT_CONTRACT,
46
+ PAPER_FLEET_UPDATE_PLAN_CONTRACT,
47
+ collectPaperFleetAudit,
48
+ discoverPaperFleet,
49
+ planPaperFleetUpdate,
50
+ writePaperFleetUpdate,
51
+ } from "./paper-fleet.js";
25
52
 
26
53
  export const PAPER_SCAFFOLD_CONTRACT = "kungfu-buildchain-paper-scaffold";
27
54
  export const PAPER_MIGRATION_CONTRACT = "kungfu-buildchain-paper-migration";
@@ -50,22 +77,6 @@ export const PAPER_STATE_ORDER = Object.freeze([
50
77
  "production-visible",
51
78
  ]);
52
79
 
53
- export const PAPER_PATHS = Object.freeze({
54
- config: ".buildchain/buildchain.toml",
55
- versionPin: ".buildchain-version",
56
- contractLock: ".buildchain/contract-lock.json",
57
- buildWorkflow: ".github/workflows/build.yml",
58
- releaseWorkflow: ".github/workflows/paper-release.yml",
59
- reproducibilityReceipt:
60
- ".buildchain/publication/reproducibility-receipt.json",
61
- sealedBundle: ".buildchain/admitted/sealed-bundle.json",
62
- admission: ".buildchain/admitted/publication-admission.json",
63
- capability: ".buildchain/admitted/publication-capability.json",
64
- npmBootstrap: ".buildchain/paper/npm-bootstrap.json",
65
- npmTrust: ".buildchain/paper/npm-trust.json",
66
- provisioningAuthority: ".buildchain/paper/provisioning-authority.json",
67
- visibility: ".buildchain/paper/visibility.json",
68
- });
69
80
  const PAPER_SCAFFOLD_PATHS = Object.freeze([
70
81
  PAPER_PATHS.config,
71
82
  PAPER_PATHS.contractLock,
@@ -100,23 +111,6 @@ function toPosix(value) {
100
111
  .join("/");
101
112
  }
102
113
 
103
- function stableJson(value) {
104
- if (Array.isArray(value)) {
105
- return `[${value.map(stableJson).join(",")}]`;
106
- }
107
- if (value && typeof value === "object") {
108
- return `{${Object.keys(value)
109
- .sort()
110
- .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
111
- .join(",")}}`;
112
- }
113
- return JSON.stringify(value);
114
- }
115
-
116
- function sha256Text(value) {
117
- return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
118
- }
119
-
120
114
  function sha256File(filePath) {
121
115
  return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
122
116
  }
@@ -125,21 +119,6 @@ function jsonText(value) {
125
119
  return `${JSON.stringify(value, null, 2)}\n`;
126
120
  }
127
121
 
128
- function readJson(filePath) {
129
- if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
130
- return { exists: false, value: undefined, error: "" };
131
- }
132
- try {
133
- return {
134
- exists: true,
135
- value: JSON.parse(fs.readFileSync(filePath, "utf8")),
136
- error: "",
137
- };
138
- } catch (error) {
139
- return { exists: true, value: undefined, error: error.message };
140
- }
141
- }
142
-
143
122
  function existingFileFact(cwd, relativePath) {
144
123
  const filePath = path.resolve(cwd, relativePath);
145
124
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
@@ -160,51 +139,6 @@ function normalizePackageName(value, label = "package name") {
160
139
  return normalized;
161
140
  }
162
141
 
163
- function normalizeRepository(value) {
164
- const normalized = String(value || "")
165
- .trim()
166
- .replace(/^git\+/, "")
167
- .replace(/^git@github\.com:/, "")
168
- .replace(/^ssh:\/\/git@github\.com\//, "")
169
- .replace(/^https?:\/\/github\.com\//, "")
170
- .replace(/\.git$/, "")
171
- .replace(/^\/+|\/+$/g, "");
172
- if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
173
- return "";
174
- }
175
- return normalized;
176
- }
177
-
178
- function commandResult(
179
- command,
180
- args,
181
- { cwd, env = process.env, timeout = 15000 } = {},
182
- ) {
183
- const result = spawnSync(command, args, {
184
- cwd,
185
- env,
186
- encoding: "utf8",
187
- timeout,
188
- maxBuffer: 2 * 1024 * 1024,
189
- });
190
- return {
191
- ok: result.status === 0,
192
- status: result.status ?? 1,
193
- stdout: String(result.stdout || "").trim(),
194
- stderr: String(result.stderr || "").trim(),
195
- error: result.error?.message || "",
196
- };
197
- }
198
-
199
- function gitResult(cwd, args) {
200
- return commandResult("git", args, { cwd });
201
- }
202
-
203
- function gitValue(cwd, args) {
204
- const result = gitResult(cwd, args);
205
- return result.ok ? result.stdout : "";
206
- }
207
-
208
142
  function buildchainPackageIdentity(buildchainRoot, explicitVersion = "") {
209
143
  const packageJson = readJson(
210
144
  path.resolve(buildchainRoot, "package.json"),
@@ -541,16 +475,58 @@ clean:
541
475
  `;
542
476
  }
543
477
 
544
- function scaffoldPackageJson({ name, packageName, repository }) {
545
- return jsonText({
546
- name: packageName,
478
+ function paperPackageScripts(current = {}) {
479
+ return {
480
+ ...current,
481
+ "buildchain:paper": "buildchain paper",
482
+ "paper:preflight": "buildchain paper preflight --json",
483
+ "paper:work:start": "buildchain paper work start",
484
+ "paper:work:submit": "buildchain paper work submit",
485
+ "paper:status": "buildchain paper status --json",
486
+ };
487
+ }
488
+
489
+ function managedPaperPackageJson(current, buildchainVersion) {
490
+ if (!/^3\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(buildchainVersion)) {
491
+ throw new Error(
492
+ "paper repositories require an exact Buildchain v3 version",
493
+ );
494
+ }
495
+ const packageManager = current.packageManager || "pnpm@11.7.0";
496
+ if (!packageManager.startsWith("pnpm@")) {
497
+ throw new Error("paper repositories require a pnpm packageManager");
498
+ }
499
+ return {
500
+ ...current,
547
501
  private: true,
548
- description: `${name} publication source repository.`,
549
- repository: {
550
- type: "git",
551
- url: `git+https://github.com/${repository}.git`,
502
+ scripts: paperPackageScripts(current.scripts),
503
+ devDependencies: {
504
+ ...(current.devDependencies || {}),
505
+ "@kungfu-tech/buildchain": buildchainVersion,
552
506
  },
553
- license: "Apache-2.0",
507
+ packageManager,
508
+ };
509
+ }
510
+
511
+ function scaffoldPackageJson({
512
+ name,
513
+ packageName,
514
+ repository,
515
+ buildchainVersion,
516
+ }) {
517
+ return jsonText({
518
+ ...managedPaperPackageJson(
519
+ {
520
+ name: packageName,
521
+ description: `${name} publication source repository.`,
522
+ repository: {
523
+ type: "git",
524
+ url: `git+https://github.com/${repository}.git`,
525
+ },
526
+ license: "Apache-2.0",
527
+ },
528
+ buildchainVersion,
529
+ ),
554
530
  });
555
531
  }
556
532
 
@@ -562,9 +538,10 @@ This repository is a Buildchain-governed publication artifact source.
562
538
  ## Local workflow
563
539
 
564
540
  \`\`\`sh
565
- buildchain paper preflight --json
566
- buildchain paper build
567
- buildchain paper status --json
541
+ pnpm paper:preflight
542
+ pnpm paper:work:start -- <topic>
543
+ pnpm paper:work:submit
544
+ make pdf
568
545
  \`\`\`
569
546
 
570
547
  The public package identity is \`${packageName}\`. Buildchain owns reproducible
@@ -644,8 +621,13 @@ function scaffoldFiles({
644
621
  acceptedAt,
645
622
  });
646
623
  const contractLockText = jsonText(contractLock);
647
- const buildWorkflow = scaffoldBuildWorkflow(buildchainSha);
648
- const releaseWorkflow = scaffoldReleaseWorkflow(buildchainSha);
624
+ const buildWorkflow = scaffoldBuildWorkflow(buildchainSha, {
625
+ artifactName: name,
626
+ });
627
+ const releaseWorkflow = scaffoldReleaseWorkflow(buildchainSha, {
628
+ artifactPaths: "_build/main.pdf",
629
+ releasePassportProductName: title,
630
+ });
649
631
  const provisioningAuthority = createPaperProvisioningAuthority({
650
632
  repository,
651
633
  packageName,
@@ -673,7 +655,15 @@ function scaffoldFiles({
673
655
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
674
656
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
675
657
  ["Makefile", scaffoldMakefile()],
676
- ["package.json", scaffoldPackageJson({ name, packageName, repository })],
658
+ [
659
+ "package.json",
660
+ scaffoldPackageJson({
661
+ name,
662
+ packageName,
663
+ repository,
664
+ buildchainVersion,
665
+ }),
666
+ ],
677
667
  ["README.md", scaffoldReadme({ title, packageName })],
678
668
  ["docs/MAP.md", scaffoldMap()],
679
669
  ["paper/main.tex", scaffoldMainTex(title)],
@@ -953,12 +943,21 @@ function migrationFiles({
953
943
  buildWorkflow,
954
944
  releaseWorkflow,
955
945
  });
946
+ const currentPackage = readJson(path.resolve(cwd, "package.json"));
947
+ if (!currentPackage.exists || currentPackage.error || !currentPackage.value) {
948
+ throw new Error("paper migration requires a valid package.json");
949
+ }
950
+ const packageJson = managedPaperPackageJson(
951
+ currentPackage.value,
952
+ runtimeIdentity.version,
953
+ );
956
954
  return new Map([
957
955
  [PAPER_PATHS.contractLock, contractLockText],
958
956
  [PAPER_PATHS.versionPin, `${runtimeIdentity.version}\n`],
959
957
  [PAPER_PATHS.buildWorkflow, buildWorkflow],
960
958
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
961
959
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
960
+ ["package.json", jsonText(packageJson)],
962
961
  ]);
963
962
  }
964
963
 
@@ -1063,6 +1062,12 @@ export function planPaperMigration({
1063
1062
  description:
1064
1063
  "Write the reviewed Buildchain-owned control files without changing paper content or publication configuration.",
1065
1064
  },
1065
+ {
1066
+ id: "refresh-pnpm-lock",
1067
+ command: "pnpm install --lockfile-only",
1068
+ description:
1069
+ "Bind the exact Buildchain v3 dependency into pnpm-lock.yaml after the reviewed package update.",
1070
+ },
1066
1071
  ],
1067
1072
  };
1068
1073
  Object.defineProperty(result, "_plannedFiles", {
@@ -1133,26 +1138,6 @@ export function writePaperMigration(plan) {
1133
1138
  };
1134
1139
  }
1135
1140
 
1136
- function paperConfig(cwd) {
1137
- const loaded = loadBuildchainConfig(cwd);
1138
- if (!loaded) {
1139
- return {
1140
- loaded: undefined,
1141
- error: `${PAPER_PATHS.config} is missing`,
1142
- };
1143
- }
1144
- if (loaded.config.project?.type !== "publication-artifact") {
1145
- return {
1146
- loaded,
1147
- error: 'project.type must be "publication-artifact"',
1148
- };
1149
- }
1150
- if (!loaded.config.publication) {
1151
- return { loaded, error: "[publication] is missing" };
1152
- }
1153
- return { loaded, error: "" };
1154
- }
1155
-
1156
1141
  function state(id, status, reason, evidence = []) {
1157
1142
  return {
1158
1143
  id,
@@ -1591,20 +1576,6 @@ export function collectPaperStatus({ cwd = process.cwd() } = {}) {
1591
1576
  };
1592
1577
  }
1593
1578
 
1594
- export function resolvePaperRepository(cwd = process.cwd()) {
1595
- const packagePath = path.resolve(cwd, "package.json");
1596
- const sourcePackage = readJson(packagePath).value;
1597
- const configured =
1598
- typeof sourcePackage?.repository === "string"
1599
- ? sourcePackage.repository
1600
- : sourcePackage?.repository?.url;
1601
- const fromPackage = normalizeRepository(configured);
1602
- if (fromPackage) return fromPackage;
1603
- return normalizeRepository(
1604
- gitValue(cwd, ["config", "--get", "remote.origin.url"]),
1605
- );
1606
- }
1607
-
1608
1579
  function validatePaperProvisioningAuthority(cwd) {
1609
1580
  const authorityPath = path.resolve(cwd, PAPER_PATHS.provisioningAuthority);
1610
1581
  const source = readJson(authorityPath);
@@ -2527,23 +2498,6 @@ export function createPaperBuildPlan({
2527
2498
  };
2528
2499
  }
2529
2500
 
2530
- function parsePaperVersion(version) {
2531
- const normalized = String(version || "")
2532
- .trim()
2533
- .replace(/^v/, "");
2534
- const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
2535
- if (!match) {
2536
- throw new Error("publication.version must be semver before planning Alpha");
2537
- }
2538
- return {
2539
- version: normalized,
2540
- major: Number(match[1]),
2541
- minor: Number(match[2]),
2542
- patch: Number(match[3]),
2543
- prerelease: match[4] || "",
2544
- };
2545
- }
2546
-
2547
2501
  function resolvePaperChannelRef(cwd, ref) {
2548
2502
  const candidates = [
2549
2503
  {
@@ -1,6 +1,12 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import {
5
+ commandId,
6
+ enumerateCliCommandsFromBin,
7
+ } from "./public-surface-cli.js";
8
+
9
+ export { enumerateCliCommandsFromBin } from "./public-surface-cli.js";
4
10
 
5
11
  export const BUILDCHAIN_PUBLIC_SURFACE_AUDIT_CONTRACT = "kungfu-buildchain-public-surface-reverse-audit";
6
12
 
@@ -40,82 +46,6 @@ function listDirectories(root, dir) {
40
46
  .sort();
41
47
  }
42
48
 
43
- function commandId(first = "", second = "", third = "") {
44
- const head = String(first || "").trim();
45
- const sub = String(second || "").trim();
46
- const leaf = String(third || "").trim();
47
- const normalizedLeaf = leaf === "..." ? "" : leaf;
48
- if (!head) return "";
49
- if (["-h", "--help", "help"].includes(head)) return "help";
50
- if (["-v", "--version", "version"].includes(head)) return "version";
51
- if (head === "release" && ["--dry-run", "dry-run", "explain"].includes(sub)) return "release-dry-run";
52
- if (head === "release" && sub === "line") return "release-line-open";
53
- if (head === "release") return "release-transaction";
54
- if (head === "transaction") return "transaction-inspect";
55
- if (head === "collect" && sub) return `collect-${sub}`;
56
- if (head === "verify" && sub) return `verify-${sub}`;
57
- if (head === "explain" && sub) return `explain-${sub}`;
58
- if (head === "inspect" && sub) return `inspect-${sub}`;
59
- if (head === "npm" && sub) return `npm-${sub}`;
60
- if (head === "lifecycle" && sub) return "lifecycle";
61
- if (head === "log" && sub) return "logging";
62
- if (head === "diagnostics" && sub) return `diagnostics-${sub}`;
63
- if (head === "facts" && sub) return "build-facts";
64
- if (head === "kfd") {
65
- if (!sub || sub === "...") return "kfd";
66
- if (sub === "schema") return normalizedLeaf ? `kfd-schema-${normalizedLeaf}` : "kfd-schema";
67
- if (sub === "upstream") return normalizedLeaf ? `kfd-upstream-${normalizedLeaf}` : "kfd-upstream";
68
- if (/^[1-9][0-9]*$/.test(sub)) return normalizedLeaf ? `kfd-${sub}-${normalizedLeaf}` : `kfd-${sub}`;
69
- return `kfd-${sub}`;
70
- }
71
- if (head === "sample" && sub) return `sample-${sub}`;
72
- if (head === "badges" && sub) return `badges-${sub}`;
73
- if (head === "homebrew" && sub) return `homebrew-${sub}`;
74
- if (head === "release-propagation") return "release-propagation";
75
- if (head === "publish-source") return "publish-source";
76
- if (head === "publication-artifact" && sub) return `publication-artifact-${sub}`;
77
- if (head === "publication-artifact") return "publication-artifact";
78
- if (head === "publication" && sub) return `publication-artifact-${sub}`;
79
- if (head === "publication") return "publication-artifact";
80
- if (head === "paper" && sub === "bootstrap" && normalizedLeaf === "npm") return "paper-bootstrap-npm";
81
- if (head === "paper" && sub) return `paper-${sub}`;
82
- if (head === "paper") return "paper";
83
- if (head === "build-contract") return "build-contract";
84
- if (head === "infra-contract") return "infra-contract";
85
- if (head === "web-surface") return "web-surface";
86
- return head;
87
- }
88
-
89
- export function enumerateCliCommandsFromBin({ root = process.cwd(), binPath = "bin/buildchain.mjs" } = {}) {
90
- const source = readText(root, binPath);
91
- const dispatchSource = [
92
- source,
93
- ...listFiles(root, "bin/internal", (name) => name.endsWith(".mjs"))
94
- .map((relPath) => readText(root, relPath)),
95
- ].join("\n");
96
- const usageMatch = source.match(/return `Usage:\n([\s\S]*?)`;\n}/);
97
- const usage = usageMatch?.[1] || "";
98
- const usageCommands = [];
99
- for (const line of usage.split(/\r?\n/)) {
100
- const match = line.trim().match(/^buildchain\s+([^\s]+)(?:\s+([^\s]+))?(?:\s+([^\s]+))?/);
101
- if (!match) continue;
102
- usageCommands.push({
103
- id: commandId(match[1], match[2], match[3]),
104
- usage: line.trim().replace(/\s+/g, " "),
105
- });
106
- }
107
- const dispatchCommands = [...dispatchSource.matchAll(/if\s*\(\s*command\s*===\s*"([^"]+)"/g)]
108
- .map((match) => commandId(match[1]));
109
- return uniqueSorted([
110
- ...usageCommands.map((entry) => entry.id),
111
- ...dispatchCommands,
112
- ]).map((id) => ({
113
- id,
114
- source: "bin/buildchain.mjs",
115
- usage: usageCommands.find((entry) => entry.id === id)?.usage || `buildchain ${id}`,
116
- }));
117
- }
118
-
119
49
  function parseYamlWorkflowCall(text) {
120
50
  const lines = text.split(/\r?\n/);
121
51
  const result = { reusable: false, inputs: [], secrets: [], outputs: [] };
@@ -333,8 +263,26 @@ export function collectPublicSurfaceReverseAudit({
333
263
  mode: "closed-world-enumerable",
334
264
  scope: "Buildchain CLI usage/dispatch, reusable workflow inputs, action inputs, site pages, and documentation command references",
335
265
  residualRisk: [
336
- "Shell commands delegated through helper scripts are only counted when exposed through bin/buildchain.mjs usage or docs.",
337
- "YAML parsing is limited to first-class action/workflow inputs, not arbitrary step environment variables.",
266
+ {
267
+ id: "public-surface-helper-command-enumeration",
268
+ definedBy: "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk",
269
+ riskType: "natural-language-semantic-risk",
270
+ trustImpact: "downgrade-warning",
271
+ machineProvability: "not-machine-verifiable",
272
+ agentAction: "semantic-review-required",
273
+ owner: "Buildchain maintainers",
274
+ reason: "Shell commands delegated through helper scripts are counted only when exposed through bin/buildchain.mjs usage or documentation.",
275
+ },
276
+ {
277
+ id: "public-surface-yaml-enumeration",
278
+ definedBy: "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk",
279
+ riskType: "natural-language-semantic-risk",
280
+ trustImpact: "downgrade-warning",
281
+ machineProvability: "not-machine-verifiable",
282
+ agentAction: "semantic-review-required",
283
+ owner: "Buildchain maintainers",
284
+ reason: "YAML enumeration covers first-class action and workflow inputs, not arbitrary step environment variables.",
285
+ },
338
286
  ],
339
287
  },
340
288
  };
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function readText(root, relPath) {
5
+ return fs.readFileSync(path.join(root, relPath), "utf8");
6
+ }
7
+
8
+ function listFiles(root, dir, predicate = () => true) {
9
+ const base = path.join(root, dir);
10
+ if (!fs.existsSync(base)) return [];
11
+ return fs
12
+ .readdirSync(base, { withFileTypes: true })
13
+ .filter((entry) => entry.isFile() && predicate(entry.name))
14
+ .map((entry) => `${dir}/${entry.name}`)
15
+ .sort();
16
+ }
17
+
18
+ function uniqueSorted(values) {
19
+ return [...new Set(values.filter(Boolean))].sort();
20
+ }
21
+
22
+ function releaseCommandId(sub) {
23
+ if (["--dry-run", "dry-run", "explain"].includes(sub))
24
+ return "release-dry-run";
25
+ if (sub === "line") return "release-line-open";
26
+ return "release-transaction";
27
+ }
28
+
29
+ function kfdCommandId(sub, leaf) {
30
+ if (!sub || sub === "...") return "kfd";
31
+ if (["schema", "upstream"].includes(sub) || /^[1-9][0-9]*$/.test(sub)) {
32
+ return leaf ? `kfd-${sub}-${leaf}` : `kfd-${sub}`;
33
+ }
34
+ return `kfd-${sub}`;
35
+ }
36
+
37
+ function paperCommandId(sub, leaf) {
38
+ if (sub === "bootstrap" && leaf === "npm") return "paper-bootstrap-npm";
39
+ if (["work", "fleet"].includes(sub) && leaf) return `paper-${sub}-${leaf}`;
40
+ return sub ? `paper-${sub}` : "paper";
41
+ }
42
+
43
+ export function commandId(first = "", second = "", third = "") {
44
+ const head = String(first || "").trim();
45
+ const sub = String(second || "").trim();
46
+ const leaf =
47
+ String(third || "").trim() === "..." ? "" : String(third || "").trim();
48
+ const paired = new Set([
49
+ "collect",
50
+ "verify",
51
+ "explain",
52
+ "inspect",
53
+ "npm",
54
+ "diagnostics",
55
+ "sample",
56
+ "badges",
57
+ "homebrew",
58
+ ]);
59
+ if (!head) return "";
60
+ if (["-h", "--help", "help"].includes(head)) return "help";
61
+ if (["-v", "--version", "version"].includes(head)) return "version";
62
+ if (head === "release") return releaseCommandId(sub);
63
+ if (head === "transaction") return "transaction-inspect";
64
+ if (paired.has(head) && sub) return `${head}-${sub}`;
65
+ if (head === "lifecycle" && sub) return "lifecycle";
66
+ if (head === "log" && sub) return "logging";
67
+ if (head === "facts" && sub) return "build-facts";
68
+ if (head === "kfd") return kfdCommandId(sub, leaf);
69
+ if (["release-propagation", "publish-source"].includes(head)) return head;
70
+ if (["publication-artifact", "publication"].includes(head)) {
71
+ return sub ? `publication-artifact-${sub}` : "publication-artifact";
72
+ }
73
+ if (head === "paper") return paperCommandId(sub, leaf);
74
+ return head;
75
+ }
76
+
77
+ function usageCommands(source) {
78
+ const usageMatch = source.match(/`Usage:\n([\s\S]*?)`;/);
79
+ const rows = [];
80
+ for (const line of (usageMatch?.[1] || "").split(/\r?\n/)) {
81
+ const match = line
82
+ .trim()
83
+ .match(/^buildchain\s+([^\s]+)(?:\s+([^\s]+))?(?:\s+([^\s]+))?/);
84
+ if (!match) continue;
85
+ rows.push({
86
+ id: commandId(match[1], match[2], match[3]),
87
+ usage: line.trim().replace(/\s+/g, " "),
88
+ });
89
+ }
90
+ return rows;
91
+ }
92
+
93
+ export function enumerateCliCommandsFromBin({
94
+ root = process.cwd(),
95
+ binPath = "bin/buildchain.mjs",
96
+ } = {}) {
97
+ const binSource = readText(root, binPath);
98
+ const helpSource = readText(root, "scripts/buildchain-cli-help.mjs");
99
+ const dispatchSource = [
100
+ binSource,
101
+ ...listFiles(root, "bin/internal", (name) => name.endsWith(".mjs")).map(
102
+ (relPath) => readText(root, relPath),
103
+ ),
104
+ ].join("\n");
105
+ const usage = usageCommands(helpSource);
106
+ const dispatch = [
107
+ ...dispatchSource.matchAll(/if\s*\(\s*command\s*===\s*"([^"]+)"/g),
108
+ ].map((match) => commandId(match[1]));
109
+ return uniqueSorted([...usage.map((entry) => entry.id), ...dispatch]).map(
110
+ (id) => ({
111
+ id,
112
+ source: "bin/buildchain.mjs",
113
+ usage:
114
+ usage.find((entry) => entry.id === id)?.usage || `buildchain ${id}`,
115
+ }),
116
+ );
117
+ }