@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/architecture/action-taxonomy.json +24 -0
  2. package/architecture/agent-change-map.md +256 -2
  3. package/architecture/ci-lane-change-budget.json +374 -0
  4. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  5. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  6. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  7. package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
  8. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  9. package/architecture/internal-capabilities.json +361 -2
  10. package/architecture/maintainability-debt.json +6 -4
  11. package/architecture/maintainability-policy.json +8 -8
  12. package/architecture/minimal-consumer-migration.json +7567 -0
  13. package/architecture/product-upstreams.json +20 -0
  14. package/architecture/release-topology.json +282 -11
  15. package/architecture/universal-workflow-bootstrap.json +12 -0
  16. package/architecture/universal-workflow-capability-policy.json +1 -1
  17. package/architecture/workflow-taxonomy.json +66 -0
  18. package/dist/readers/business-attempt.cjs +809 -0
  19. package/dist/site/buildchain-contract.json +7 -5
  20. package/dist/site/buildchain-site.json +18 -13
  21. package/dist/site/capability-registry.json +3 -3
  22. package/dist/site/kfd-claims.json +385 -10
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +1 -1
  25. package/dist/site/node-api-registry.json +21 -21
  26. package/dist/site/page-registry.json +13 -8
  27. package/dist/site/public-surface-audit.json +258 -7
  28. package/dist/site/publication-authority-registry.json +141 -1
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +5 -5
  31. package/dist/site/workflow-registry.json +522 -7
  32. package/docs/node-api-reference.md +39 -39
  33. package/docs/runtime-entry.md +45 -5
  34. package/docs/workflow-catalog.md +6 -0
  35. package/package.json +4 -3
  36. package/packages/core/build/standalone/build.js +7 -1
  37. package/packages/core/consumer/buildchain-config.js +2 -21
  38. package/packages/core/consumer/contract/entries.js +85 -0
  39. package/packages/core/consumer/contract/examples.js +96 -0
  40. package/packages/core/consumer/contract/identity.js +65 -0
  41. package/packages/core/consumer/contract/inspection.js +45 -0
  42. package/packages/core/consumer/contract/plan.js +146 -0
  43. package/packages/core/consumer/contract/products.js +119 -0
  44. package/packages/core/consumer/contract/reader.js +47 -0
  45. package/packages/core/consumer/contract/shape.js +46 -0
  46. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  47. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  48. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  49. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  50. package/packages/core/dev-delivery/native/actions.js +7 -1
  51. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  52. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  53. package/packages/core/dev-delivery/queue/landing-action.js +5 -0
  54. package/packages/core/governance/buildchain-publication-authority.js +6 -0
  55. package/packages/core/paper/operations/bootstrap.js +2 -0
  56. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  57. package/packages/core/providers/github/attempt-index.js +60 -0
  58. package/packages/core/providers/github/attempt-journal.js +154 -0
  59. package/packages/core/providers/github/discussions/materials.js +7 -2
  60. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  61. package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
  62. package/packages/core/providers/github/pipeline-events.js +52 -0
  63. package/packages/core/providers/github/pipeline-integration.js +95 -0
  64. package/packages/core/providers/github/pipeline-policy.js +213 -0
  65. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  66. package/packages/core/providers/github/pipeline-product-release.js +191 -0
  67. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  68. package/packages/core/providers/github/pipeline-release-evidence.js +111 -0
  69. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  70. package/packages/core/providers/github/pipeline-runs.js +132 -0
  71. package/packages/core/providers/github/pipeline-source.js +178 -0
  72. package/packages/core/providers/github/pipeline-stable-source.js +354 -0
  73. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  74. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  75. package/packages/core/providers/github/pipeline-version.js +199 -0
  76. package/packages/core/providers/github/pipeline-worker.js +108 -0
  77. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  78. package/packages/core/publication/npm/pack-preview.js +2 -2
  79. package/packages/core/publication/npm/pack-result.js +31 -0
  80. package/packages/core/publication/npm/package.js +3 -6
  81. package/packages/core/publication/npm/pipeline-channel.js +87 -0
  82. package/packages/core/publication/npm/pipeline-provider.js +109 -0
  83. package/packages/core/publication/npm/registry.js +12 -9
  84. package/packages/core/publication/pipeline/actions.js +116 -0
  85. package/packages/core/publication/pipeline/apply.js +188 -0
  86. package/packages/core/publication/pipeline/build-segments.js +107 -0
  87. package/packages/core/publication/pipeline/capsules.js +89 -0
  88. package/packages/core/publication/pipeline/context.js +39 -0
  89. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  90. package/packages/core/publication/pipeline/development-current.js +63 -0
  91. package/packages/core/publication/pipeline/development-pr.js +112 -0
  92. package/packages/core/publication/pipeline/development-proof.js +76 -0
  93. package/packages/core/publication/pipeline/development-transition.js +112 -0
  94. package/packages/core/publication/pipeline/distribution.js +124 -0
  95. package/packages/core/publication/pipeline/documents.js +192 -0
  96. package/packages/core/publication/pipeline/effects.js +162 -0
  97. package/packages/core/publication/pipeline/files.js +66 -0
  98. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  99. package/packages/core/publication/pipeline/journal.js +110 -0
  100. package/packages/core/publication/pipeline/next-development.js +117 -0
  101. package/packages/core/publication/pipeline/pack.js +157 -0
  102. package/packages/core/publication/pipeline/package-policy.js +22 -0
  103. package/packages/core/publication/pipeline/plan.js +136 -0
  104. package/packages/core/publication/pipeline/prepare.js +139 -0
  105. package/packages/core/publication/pipeline/qualification.js +175 -0
  106. package/packages/core/publication/pipeline/qualify.js +104 -0
  107. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  108. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  109. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  110. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  111. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  112. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  113. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  114. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  115. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  116. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  117. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  118. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  119. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  120. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  121. package/packages/core/publication/pipeline/settle.js +43 -0
  122. package/packages/core/publication/pipeline/signing.js +140 -0
  123. package/packages/core/publication/pipeline/source-plan.js +83 -0
  124. package/packages/core/publication/pipeline/stable-entry.js +215 -0
  125. package/packages/core/publication/pipeline/stable-products.js +317 -0
  126. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  127. package/packages/core/publication/pipeline/stable.js +82 -0
  128. package/packages/core/publication/pipeline/version-actions.js +93 -0
  129. package/packages/core/publication/pipeline/version-build.js +130 -0
  130. package/packages/core/publication/pipeline/version-context.js +143 -0
  131. package/packages/core/publication/pipeline/version-material.js +36 -0
  132. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  133. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  134. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  135. package/packages/core/publication/pipeline/version.js +81 -0
  136. package/packages/core/publication/pipeline/worker.js +72 -0
  137. package/packages/core/publication/publication-reproducibility.js +2 -2
  138. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  139. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  140. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  141. package/packages/core/release/stable-release-gate.js +11 -0
  142. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  143. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  144. package/packages/core/runtime/entry/actions.js +27 -25
  145. package/packages/core/runtime/entry/attempt.js +25 -0
  146. package/packages/core/runtime/entry/selection.js +14 -0
  147. package/packages/core/runtime/entry/source.js +62 -0
  148. package/packages/core/workflow/attempt/identity.js +170 -0
  149. package/packages/core/workflow/attempt/journal.js +87 -0
  150. package/packages/core/workflow/attempt/materials.js +72 -0
  151. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  152. package/packages/core/workflow/attempt/reader.js +149 -0
  153. package/packages/core/workflow/attempt/records.js +153 -0
  154. package/packages/core/workflow/attempt/store.js +75 -0
  155. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  156. package/packages/core/workflow/pipeline/action-output.js +11 -0
  157. package/packages/core/workflow/pipeline/actions.js +75 -0
  158. package/packages/core/workflow/pipeline/build-control.js +140 -0
  159. package/packages/core/workflow/pipeline/build-evidence.js +92 -0
  160. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  161. package/packages/core/workflow/pipeline/build-result.js +69 -0
  162. package/packages/core/workflow/pipeline/build.js +142 -0
  163. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  164. package/packages/core/workflow/pipeline/channel-control.js +127 -0
  165. package/packages/core/workflow/pipeline/controller.js +133 -0
  166. package/packages/core/workflow/pipeline/delivery-control.js +204 -0
  167. package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
  168. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  169. package/packages/core/workflow/pipeline/events.js +84 -0
  170. package/packages/core/workflow/pipeline/fence.js +42 -0
  171. package/packages/core/workflow/pipeline/group-control.js +56 -0
  172. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  173. package/packages/core/workflow/pipeline/guard.js +156 -0
  174. package/packages/core/workflow/pipeline/host.js +114 -0
  175. package/packages/core/workflow/pipeline/materials.js +58 -0
  176. package/packages/core/workflow/pipeline/notifications.js +36 -0
  177. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  178. package/packages/core/workflow/pipeline/platforms.js +21 -0
  179. package/packages/core/workflow/pipeline/progress.js +108 -0
  180. package/packages/core/workflow/pipeline/projection.js +20 -0
  181. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  182. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  183. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  184. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  185. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  186. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  187. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  188. package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
  189. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  190. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  191. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  192. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  193. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  194. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  195. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  196. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  197. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  198. package/packages/core/workflow/pipeline/selection.js +106 -0
  199. package/packages/core/workflow/pipeline/session.js +72 -0
  200. package/packages/core/workflow/pipeline/settlement.js +137 -0
  201. package/packages/core/workflow/pipeline/wake.js +38 -0
  202. package/packages/core/workflow/pipeline/web-status.js +108 -0
  203. package/scripts/build-release-discussion-reader.mjs +11 -7
  204. package/scripts/check-pipeline-publication-topology.mjs +48 -0
  205. package/scripts/check-release-topology.mjs +7 -1
  206. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  207. package/scripts/generate-site-bundle.mjs +1 -1
  208. package/scripts/verify-golden-path.mjs +5 -1
  209. package/scripts/verify-standalone-product.mjs +42 -0
  210. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  211. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  212. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  213. package/templates/minimal-consumer/binary/package.json +6 -0
  214. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  215. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  216. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  217. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  218. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  219. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  220. package/templates/minimal-consumer/npm/package.json +6 -0
  221. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  222. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  223. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  224. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  225. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  226. package/templates/minimal-consumer/paper/package.json +6 -0
  227. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  228. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -11,7 +11,12 @@ import {
11
11
  readLogPath,
12
12
  noopLogger,
13
13
  } from "./logging.js";
14
- import { platformTriple, copyNodeBinary, sourceSha } from "./identity.js";
14
+ import {
15
+ platformTriple,
16
+ copyNodeBinary,
17
+ sourceSha,
18
+ packageVersion,
19
+ } from "./identity.js";
15
20
  import { bundleCli, postjectArgs } from "./bundle.js";
16
21
  import { run } from "./process.js";
17
22
  function prepareStandaloneExecutable({
@@ -124,6 +129,7 @@ export function buildStandaloneBinary({
124
129
  nodePath = process.execPath,
125
130
  nodeVersion = process.version,
126
131
  } = {}) {
132
+ version ||= packageVersion(cwd);
127
133
  const resolvedOutputDir = path.resolve(cwd, outputDir);
128
134
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-sea-"));
129
135
  try {
@@ -5,8 +5,8 @@ import { isDeepStrictEqual } from "node:util";
5
5
  import { parse } from "smol-toml";
6
6
  import {
7
7
  BUILDCHAIN_CONFIG_PATH,
8
- resolveBuildchainConfigPath,
9
8
  } from "../contracts/buildchain-layout.js";
9
+ import { readProductConfiguration } from "./contract/reader.js";
10
10
  import { runShellCommandSync } from "../runtime/spawn-command.js";
11
11
  import { normalizeBuildConfiguration } from "../build/build-configuration.js";
12
12
 
@@ -181,26 +181,7 @@ function updateTomlStringValuePreservingSource({ source, content, key, value, fi
181
181
  }
182
182
 
183
183
  export function loadBuildchainConfig(cwd = process.cwd()) {
184
- const configPath = resolveBuildchainConfigPath(cwd);
185
- const filePath = path.join(cwd, configPath);
186
- if (!fs.existsSync(filePath)) {
187
- return undefined;
188
- }
189
- const source = fs.readFileSync(filePath, "utf8");
190
- let config;
191
- try {
192
- config = parse(source);
193
- } catch (error) {
194
- throw new Error(`${configPath} parse failed: ${error.message}`);
195
- }
196
- if (config.schema !== 1) {
197
- throw new Error(`${configPath} schema must be 1`);
198
- }
199
- return {
200
- path: configPath,
201
- filePath,
202
- config: normalizeBuildchainConfig(config),
203
- };
184
+ return readProductConfiguration(cwd, normalizeBuildchainConfig);
204
185
  }
205
186
 
206
187
  export function normalizeBuildchainConfig(config) {
@@ -0,0 +1,85 @@
1
+ import { choice, object, relativePath, text } from "./shape.js";
2
+ import { CONFIG_PATH } from "./plan.js";
3
+
4
+ export const PIPELINE_ENTRY = ".github/workflows/public-ops-pipeline.yml";
5
+ export const RECOVERY_ENTRY = ".github/workflows/public-ops-recover.yml";
6
+ export const PIPELINE_EVENTS = {
7
+ pull_request: [
8
+ "opened",
9
+ "reopened",
10
+ "synchronize",
11
+ "ready_for_review",
12
+ "converted_to_draft",
13
+ "closed",
14
+ "labeled",
15
+ "unlabeled",
16
+ "edited",
17
+ "enqueued",
18
+ "dequeued",
19
+ ],
20
+ pull_request_review: ["submitted", "dismissed"],
21
+ pull_request_target: ["closed"],
22
+ merge_group: ["checks_requested"],
23
+ push: [],
24
+ repository_dispatch: ["buildchain-attempt-wake"],
25
+ };
26
+
27
+ export function normalInputs(value = {}) {
28
+ object(value, [], ["config-path"], "inputs");
29
+ const configPath = relativePath(
30
+ value["config-path"] ?? CONFIG_PATH,
31
+ "inputs.config-path",
32
+ );
33
+ if (!configPath.endsWith(".toml") || configPath.includes("*"))
34
+ throw new Error("inputs.config-path: expected one TOML path");
35
+ return { configPath };
36
+ }
37
+
38
+ export function recoveryInputs(value) {
39
+ object(value, ["attempt"], ["runtime-ref"], "inputs");
40
+ text(value.attempt, "inputs.attempt", /^attempt-[0-9a-f]{64}$/u);
41
+ const runtimeRef = value["runtime-ref"] ?? "";
42
+ if (runtimeRef !== "")
43
+ text(
44
+ runtimeRef,
45
+ "inputs.runtime-ref",
46
+ /^(?:[0-9a-f]{40}|v4(?:-alpha)?|train\/v4\/v4\.[0-9]+\/[a-z0-9][a-z0-9-]*)$/u,
47
+ );
48
+ return { attempt: value.attempt, runtimeRef };
49
+ }
50
+
51
+ export function admitEvent(name, action = "") {
52
+ choice(name, Object.keys(PIPELINE_EVENTS), "event.name");
53
+ choice(
54
+ action,
55
+ PIPELINE_EVENTS[name].length ? PIPELINE_EVENTS[name] : [""],
56
+ "event.action",
57
+ );
58
+ return { name, action };
59
+ }
60
+
61
+ function call(entry, channel, extra = "") {
62
+ return `permissions:\n contents: read\njobs:\n buildchain:\n uses: kungfu-systems/buildchain/${entry}@${channel}\n${extra} secrets: inherit\n permissions:\n actions: write\n attestations: write\n checks: write\n contents: write\n discussions: write\n id-token: write\n pull-requests: write\n statuses: write\n`;
63
+ }
64
+
65
+ // Product-independent source of the two caller bytes. Public entry publication
66
+ // is qualified separately; rendering this contract is not an execution receipt.
67
+ export function consumerWorkflows(channel = "v4", configPath = CONFIG_PATH) {
68
+ choice(channel, ["v4", "v4-alpha"], "entry.channel");
69
+ normalInputs({ "config-path": configPath });
70
+ const configInput =
71
+ configPath === CONFIG_PATH
72
+ ? ""
73
+ : ` with:\n config-path: ${configPath}\n`;
74
+ const events = Object.entries(PIPELINE_EVENTS)
75
+ .map(([name, types]) => {
76
+ return types.length
77
+ ? ` ${name}:\n types: [${types.join(", ")}]\n`
78
+ : ` ${name}:\n`;
79
+ })
80
+ .join("");
81
+ return {
82
+ ".github/workflows/buildchain.yml": `name: Buildchain pipeline\non:\n${events}${call(PIPELINE_ENTRY, channel, configInput)}`,
83
+ ".github/workflows/buildchain-recover.yml": `name: Buildchain recovery\non:\n workflow_dispatch:\n inputs:\n attempt:\n description: Exact Buildchain attempt to recover\n required: true\n type: string\n runtime-ref:\n description: Optional repaired runtime\n required: false\n type: string\n${call(RECOVERY_ENTRY, channel, " with:\n attempt: ${{ inputs.attempt }}\n runtime-ref: ${{ inputs.runtime-ref }}\n")}`,
84
+ };
85
+ }
@@ -0,0 +1,96 @@
1
+ import { choice } from "./shape.js";
2
+ import { consumerWorkflows } from "./entries.js";
3
+ import { CONFIG_PATH } from "./plan.js";
4
+ import { PRODUCT_TYPES } from "./products.js";
5
+
6
+ const ARTIFACTS = {
7
+ npm: { path: "dist/package", kind: "npm-package", provider: "npm" },
8
+ binary: {
9
+ path: "dist/hello.tar.gz",
10
+ kind: "archive",
11
+ provider: "github-release",
12
+ },
13
+ paper: { path: "dist/paper.pdf", kind: "pdf", provider: "github-release" },
14
+ };
15
+
16
+ function productSource(type) {
17
+ const prefix =
18
+ 'import fs from "node:fs";\nfs.mkdirSync("dist", { recursive: true });\n';
19
+ if (type === "npm")
20
+ return `${prefix}fs.mkdirSync("dist/package", { recursive: true });\nfs.writeFileSync("dist/package/index.js", 'export const hello = "hello";\\n');\nfs.writeFileSync("dist/package/package.json", JSON.stringify({ name: "@example/minimal", version: JSON.parse(fs.readFileSync("package.json")).version, type: "module", exports: "./index.js" }));\n`;
21
+ if (type === "binary")
22
+ return `${prefix}import { execFileSync } from "node:child_process";\nexecFileSync("cc", ["src/hello.c", "-o", "dist/hello"], { stdio: "inherit" });\nexecFileSync("tar", ["-czf", "dist/hello.tar.gz", "-C", "dist", "hello"], { stdio: "inherit" });\n`;
23
+ return `${prefix}const stream = "BT /F1 24 Tf 50 700 Td (Minimal Paper) Tj ET";
24
+ const objects = ["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>", "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", \`<< /Length \${stream.length} >>\\nstream\\n\${stream}\\nendstream\`];
25
+ let pdf = "%PDF-1.4\\n";
26
+ const offsets = [0];
27
+ for (const [index, object] of objects.entries()) { offsets.push(Buffer.byteLength(pdf)); pdf += \`\${index + 1} 0 obj\\n\${object}\\nendobj\\n\`; }
28
+ const xref = Buffer.byteLength(pdf);
29
+ pdf += \`xref\\n0 \${offsets.length}\\n0000000000 65535 f \\n\`;
30
+ for (const offset of offsets.slice(1)) pdf += \`\${String(offset).padStart(10, "0")} 00000 n \\n\`;
31
+ pdf += \`trailer\\n<< /Size \${offsets.length} /Root 1 0 R >>\\nstartxref\\n\${xref}\\n%%EOF\\n\`;
32
+ fs.writeFileSync("dist/paper.pdf", pdf);
33
+ `;
34
+ }
35
+
36
+ export function standardConsumerExample(type, channel = "v4") {
37
+ choice(type, PRODUCT_TYPES, "product.type");
38
+ const artifact = ARTIFACTS[type];
39
+ const npmAccess = type === "npm" ? 'access = "public"\n' : "";
40
+ const files = {
41
+ ...consumerWorkflows(channel),
42
+ "package.json": `${JSON.stringify({ name: `minimal-${type}`, private: true, version: "1.0.0-alpha.1", type: "module" }, null, 2)}\n`,
43
+ "src/build.mjs": productSource(type),
44
+ "src/verify.mjs": `import assert from "node:assert/strict";\nimport fs from "node:fs";\nassert.ok(fs.statSync(${JSON.stringify(artifact.path)}).${type === "npm" ? "isDirectory()" : "size > 0"});\n`,
45
+ [CONFIG_PATH]: `schema = 2
46
+
47
+ [[products]]
48
+ id = "main"
49
+ type = "${type}"
50
+ platforms = ["linux-x64"]
51
+ build = ["node src/build.mjs"]
52
+ verify = ["node src/verify.mjs"]
53
+
54
+ [[products.artifacts]]
55
+ id = "main"
56
+ path = "${artifact.path}"
57
+ kind = "${artifact.kind}"
58
+
59
+ [[products.targets]]
60
+ provider = "${artifact.provider}"
61
+ artifacts = ["main"]
62
+ ${npmAccess}
63
+ [version]
64
+ strategy = "semver"
65
+
66
+ [[version.files]]
67
+ path = "package.json"
68
+ format = "json"
69
+ key = "version"
70
+
71
+ [[channels]]
72
+ from = "feature/*"
73
+ to = "dev/v1/v1.0"
74
+ operation = "develop"
75
+
76
+ [[channels]]
77
+ from = "dev/v1/v1.0"
78
+ to = "alpha/v1/v1.0"
79
+ operation = "alpha"
80
+
81
+ [[channels]]
82
+ from = "alpha/v1/v1.0"
83
+ to = "release/v1/v1.0"
84
+ operation = "stable"
85
+
86
+ [review]
87
+ minimum_approvals = 1
88
+ code_owners = true
89
+ merge_queue = true
90
+ `,
91
+ };
92
+ if (type === "binary")
93
+ files["src/hello.c"] =
94
+ '#include <stdio.h>\nint main(void) { puts("hello"); return 0; }\n';
95
+ return files;
96
+ }
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ import { object, relativePath, text } from "./shape.js";
3
+ import { compileConsumerPlan, CONSUMER_CONTRACT } from "./plan.js";
4
+
5
+ export function contentDigest(bytes) {
6
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7
+ }
8
+
9
+ // Source identities are prepared from trusted GitHub readback, never caller inputs.
10
+ export function bindConsumerSource(source, configBytes) {
11
+ object(
12
+ source,
13
+ ["repository", "commit", "tree", "configPath", "configBlob"],
14
+ [],
15
+ "source",
16
+ );
17
+ text(
18
+ source.repository,
19
+ "source.repository",
20
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u,
21
+ );
22
+ for (const key of ["commit", "tree", "configBlob"])
23
+ text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
24
+ relativePath(source.configPath, "source.configPath");
25
+ const bytes = Buffer.from(configBytes);
26
+ const blob = createHash("sha1")
27
+ .update(`blob ${bytes.length}\0`)
28
+ .update(bytes)
29
+ .digest("hex");
30
+ if (blob !== source.configBlob)
31
+ throw new Error("source.configBlob: config bytes do not match Git blob");
32
+ const plan = compileConsumerPlan(bytes.toString("utf8"));
33
+ const identity = {
34
+ schema: "buildchain.consumer-source/v1",
35
+ repository: source.repository,
36
+ commit: source.commit,
37
+ tree: source.tree,
38
+ configPath: source.configPath,
39
+ configBlob: blob,
40
+ configDigest: contentDigest(bytes),
41
+ contract: CONSUMER_CONTRACT,
42
+ };
43
+ return {
44
+ identity,
45
+ identityDigest: contentDigest(JSON.stringify(identity)),
46
+ plan,
47
+ };
48
+ }
49
+
50
+ export function consumerContractLock({ entry, runtime, configDigest }) {
51
+ for (const [name, value] of Object.entries({ entry, runtime })) {
52
+ object(value, ["repository", "sha"], [], name);
53
+ if (value.repository !== "kungfu-systems/buildchain")
54
+ throw new Error(`${name}: untrusted Buildchain repository`);
55
+ text(value.sha, `${name}.sha`, /^[0-9a-f]{40}$/u);
56
+ }
57
+ text(configDigest, "configDigest", /^sha256:[0-9a-f]{64}$/u);
58
+ return {
59
+ schema: "buildchain.consumer-contract-lock/v2",
60
+ contract: CONSUMER_CONTRACT,
61
+ entry: { ...entry },
62
+ runtime: { ...runtime },
63
+ configDigest,
64
+ };
65
+ }
@@ -0,0 +1,45 @@
1
+ import { consumerWorkflows } from "./entries.js";
2
+ import { compileConsumerPlan, CONFIG_PATH } from "./plan.js";
3
+
4
+ const INTERNAL_WIRING = [
5
+ /\b(?:request-json|native-roots-json|used-nonces-json|warrant-result|source-proof|integration-proof)\b/iu,
6
+ /\bbuildchain\s+(?:dev\s+(?:warrant|proof)|release|promote|recover|publish)\b/iu,
7
+ /\b(?:npm\s+publish|gh\s+release\s+(?:create|upload)|gh\s+run\s+download)\b/iu,
8
+ /actions\/download-artifact@/u,
9
+ /\.buildchain\/runtime\/|packages\/core\/(?:publication|release|dev-delivery)\//u,
10
+ /\b(?:fencingToken|candidateRoot|sourceProofRoot|usedNonces|generationRoot|compareAndSwap)\b/u,
11
+ /buildchain[-./][\w-]*(?:warrant|proof|fence|nonce|candidate|authority|transaction)/iu,
12
+ ];
13
+
14
+ // The supplied file map must be the complete tracked consumer tree, including
15
+ // package scripts and the source files they invoke. Runtime source is never run.
16
+ export function inspectConsumerContract(
17
+ files,
18
+ { channel = "v4", configPath = CONFIG_PATH } = {},
19
+ ) {
20
+ const issues = [];
21
+ let plan;
22
+ try {
23
+ plan = compileConsumerPlan(files[configPath]);
24
+ } catch (error) {
25
+ issues.push(`${configPath}: ${error.message}`);
26
+ }
27
+ const workflows = consumerWorkflows(channel, configPath);
28
+ for (const [file, expected] of Object.entries(workflows))
29
+ if (files[file] !== expected)
30
+ issues.push(`${file}: must match the generated thin caller`);
31
+ for (const [file, source] of Object.entries(files)) {
32
+ if (
33
+ /^\.github\/workflows\/.*\.ya?ml$/u.test(file) &&
34
+ !Object.hasOwn(workflows, file)
35
+ )
36
+ issues.push(`${file}: extra consumer workflow is forbidden`);
37
+ if (!/\.(?:toml|json|ya?ml|[cm]?js|ts|py|sh|ps1|rb)$/u.test(file)) continue;
38
+ for (const pattern of INTERNAL_WIRING)
39
+ if (pattern.test(String(source)))
40
+ issues.push(
41
+ `${file}: consumer owns internal orchestration (${pattern.source})`,
42
+ );
43
+ }
44
+ return { ok: issues.length === 0, issues, plan };
45
+ }
@@ -0,0 +1,146 @@
1
+ import { parse } from "smol-toml";
2
+ import { choice, list, object, relativePath, text, unique } from "./shape.js";
3
+ import { compileProducts } from "./products.js";
4
+
5
+ export const CONSUMER_CONTRACT = "buildchain.consumer-contract/v2";
6
+ export const CONSUMER_PLAN = "buildchain.consumer-plan/v1";
7
+ export const CONFIG_PATH = ".buildchain/buildchain.toml";
8
+
9
+ function compileVersion(value) {
10
+ object(value, ["strategy", "files"], ["derived_files"], "version");
11
+ choice(value.strategy, ["semver", "anchored"], "version.strategy");
12
+ list(value.files, "version.files", (item, field) => {
13
+ object(item, ["path", "format", "key"], [], field);
14
+ relativePath(item.path, `${field}.path`);
15
+ choice(item.format, ["json", "toml"], `${field}.format`);
16
+ text(item.key, `${field}.key`, /^[A-Za-z0-9_.-]+$/u);
17
+ });
18
+ unique(
19
+ value.files.map((item) => `${item.path}:${item.key}`),
20
+ "version.files",
21
+ );
22
+ if (value.derived_files !== undefined)
23
+ list(value.derived_files, "version.derived_files", relativePath);
24
+ return structuredClone(value);
25
+ }
26
+
27
+ function compileChannels(values) {
28
+ const channels = list(values, "channels", (value, field) => {
29
+ object(value, ["from", "to", "operation"], [], field);
30
+ text(
31
+ value.from,
32
+ `${field}.from`,
33
+ /^(?:feature|dev|alpha|release)\/[A-Za-z0-9/*._-]+$/u,
34
+ );
35
+ text(
36
+ value.to,
37
+ `${field}.to`,
38
+ /^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u,
39
+ );
40
+ choice(
41
+ value.operation,
42
+ ["develop", "alpha", "stable", "major"],
43
+ `${field}.operation`,
44
+ );
45
+ const source = value.from.split("/")[0],
46
+ target = value.to.split("/")[0];
47
+ const legal = {
48
+ develop: source === "feature" && target === "dev",
49
+ alpha: source === "dev" && target === "alpha",
50
+ stable: source === "alpha" && target === "release",
51
+ major: source === "release" && value.to === "publish-gate/major",
52
+ };
53
+ if (
54
+ !legal[value.operation] ||
55
+ value.from.includes("..") ||
56
+ value.to.includes("..")
57
+ )
58
+ throw new Error(`${field}: unlawful channel route`);
59
+ return { ...value };
60
+ });
61
+ unique(
62
+ channels.map((value) => `${value.from}:${value.to}`),
63
+ "channels",
64
+ );
65
+ return channels;
66
+ }
67
+
68
+ function compileReview(value) {
69
+ object(
70
+ value,
71
+ ["minimum_approvals", "code_owners", "merge_queue"],
72
+ [],
73
+ "review",
74
+ );
75
+ if (!Number.isInteger(value.minimum_approvals) || value.minimum_approvals < 1)
76
+ throw new Error(
77
+ "review.minimum_approvals: independent approval is required",
78
+ );
79
+ if (value.code_owners !== true || value.merge_queue !== true)
80
+ throw new Error(
81
+ "review: code owners and protected merge queue are required",
82
+ );
83
+ return { ...value };
84
+ }
85
+
86
+ function compileStable(value) {
87
+ object(
88
+ value,
89
+ [
90
+ "minimum_interval_seconds",
91
+ "minimum_soak_seconds",
92
+ "product_paths",
93
+ "impact_file",
94
+ "require_published_entry",
95
+ ],
96
+ [],
97
+ "stable",
98
+ );
99
+ for (const field of ["minimum_interval_seconds", "minimum_soak_seconds"])
100
+ if (!Number.isSafeInteger(value[field]) || value[field] < 0)
101
+ throw new Error(
102
+ `stable.${field}: expected non-negative safe integer seconds`,
103
+ );
104
+ const paths = list(
105
+ value.product_paths,
106
+ "stable.product_paths",
107
+ (item, field) => {
108
+ text(item, field);
109
+ relativePath(item.endsWith("/") ? item.slice(0, -1) : item, field);
110
+ if (item.includes("*"))
111
+ throw new Error(`${field}: expected a literal product path`);
112
+ return item;
113
+ },
114
+ );
115
+ unique(paths, "stable.product_paths");
116
+ relativePath(value.impact_file, "stable.impact_file");
117
+ if (!value.impact_file.endsWith(".json") || value.impact_file.includes("*"))
118
+ throw new Error(
119
+ "stable.impact_file: expected one product impact JSON file",
120
+ );
121
+ if (typeof value.require_published_entry !== "boolean")
122
+ throw new Error("stable.require_published_entry: expected a boolean");
123
+ return structuredClone(value);
124
+ }
125
+
126
+ // This compiler receives bytes. It has no filesystem, process, network or provider port.
127
+ export function compileConsumerPlan(source) {
128
+ const config = parse(source);
129
+ object(
130
+ config,
131
+ ["schema", "products", "version", "channels", "review"],
132
+ ["stable"],
133
+ );
134
+ choice(config.schema, [2], "schema");
135
+ return {
136
+ schema: CONSUMER_PLAN,
137
+ contract: CONSUMER_CONTRACT,
138
+ products: compileProducts(config.products),
139
+ version: compileVersion(config.version),
140
+ channels: compileChannels(config.channels),
141
+ review: compileReview(config.review),
142
+ ...(config.stable === undefined
143
+ ? {}
144
+ : { stable: compileStable(config.stable) }),
145
+ };
146
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ choice,
3
+ list,
4
+ object,
5
+ relativePath,
6
+ slug,
7
+ text,
8
+ unique,
9
+ } from "./shape.js";
10
+
11
+ export const PRODUCT_TYPES = ["npm", "binary", "paper"];
12
+ export const PLATFORMS = [
13
+ "linux-x64",
14
+ "linux-arm64",
15
+ "macos-arm64",
16
+ "macos-x64",
17
+ "windows-x64",
18
+ ];
19
+
20
+ function artifact(value, location) {
21
+ object(value, ["id", "path", "kind"], ["filename"], location);
22
+ slug(value.id, `${location}.id`);
23
+ if (value.path !== "." || value.kind !== "npm-package")
24
+ relativePath(value.path, `${location}.path`);
25
+ choice(value.kind, ["npm-package", "archive", "pdf"], `${location}.kind`);
26
+ if (value.filename !== undefined) {
27
+ text(
28
+ value.filename,
29
+ `${location}.filename`,
30
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/u,
31
+ );
32
+ const extension =
33
+ value.kind === "npm-package"
34
+ ? /\.tgz$/u
35
+ : value.kind === "pdf"
36
+ ? /\.pdf$/u
37
+ : /\.(?:tar\.gz|tar\.xz|tgz|zip|tar)$/u;
38
+ if (!extension.test(value.filename))
39
+ throw new Error(
40
+ `${location}.filename: artifact extension does not match its kind`,
41
+ );
42
+ }
43
+ return { ...value };
44
+ }
45
+
46
+ function target(value, location) {
47
+ object(value, ["provider", "artifacts"], ["access"], location);
48
+ choice(value.provider, ["npm", "github-release"], `${location}.provider`);
49
+ list(value.artifacts, `${location}.artifacts`, slug);
50
+ unique(value.artifacts, `${location}.artifacts`);
51
+ if (value.provider === "npm")
52
+ choice(value.access, ["public", "restricted"], `${location}.access`);
53
+ else if (Object.hasOwn(value, "access"))
54
+ throw new Error(`${location}.access: npm only`);
55
+ return structuredClone(value);
56
+ }
57
+
58
+ export function compileProducts(values) {
59
+ const products = list(values, "products", (value, location) => {
60
+ object(
61
+ value,
62
+ ["id", "type", "platforms", "build", "verify", "artifacts", "targets"],
63
+ ["install", "directory"],
64
+ location,
65
+ );
66
+ slug(value.id, `${location}.id`);
67
+ choice(value.type, PRODUCT_TYPES, `${location}.type`);
68
+ list(value.platforms, `${location}.platforms`, (item, field) =>
69
+ choice(item, PLATFORMS, field),
70
+ );
71
+ unique(value.platforms, `${location}.platforms`);
72
+ for (const operation of [
73
+ "build",
74
+ "verify",
75
+ ...(value.install !== undefined ? ["install"] : []),
76
+ ])
77
+ list(value[operation], `${location}.${operation}`, text);
78
+ if (value.directory !== undefined)
79
+ relativePath(value.directory, `${location}.directory`);
80
+ const artifacts = list(value.artifacts, `${location}.artifacts`, artifact);
81
+ unique(
82
+ artifacts.map((item) => item.id),
83
+ `${location}.artifacts`,
84
+ );
85
+ const targets = list(value.targets, `${location}.targets`, target);
86
+ unique(
87
+ targets.map((item) => item.provider),
88
+ `${location}.targets`,
89
+ );
90
+ const expectedKind = {
91
+ npm: "npm-package",
92
+ binary: "archive",
93
+ paper: "pdf",
94
+ }[value.type];
95
+ if (artifacts.some((item) => item.kind !== expectedKind))
96
+ throw new Error(`${location}: artifact kind does not match product type`);
97
+ const published = targets.flatMap((item) => item.artifacts);
98
+ if (
99
+ artifacts.some((item) => !published.includes(item.id)) ||
100
+ published.some((id) => !artifacts.some((item) => item.id === id))
101
+ )
102
+ throw new Error(
103
+ `${location}: publication targets must cover exactly declared artifacts`,
104
+ );
105
+ if (
106
+ targets.some((item) => item.provider === "npm") !==
107
+ (value.type === "npm")
108
+ )
109
+ throw new Error(
110
+ `${location}: npm products require an npm target; other products cannot publish to npm`,
111
+ );
112
+ return structuredClone({ ...value, artifacts, targets });
113
+ });
114
+ unique(
115
+ products.map((item) => item.id),
116
+ "products",
117
+ );
118
+ return products;
119
+ }
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse } from "smol-toml";
4
+ import { resolveBuildchainConfigPath } from "../../contracts/buildchain-layout.js";
5
+ import { compileConsumerPlan } from "./plan.js";
6
+
7
+ function productConfiguration(source, config) {
8
+ const plan = compileConsumerPlan(source);
9
+ // Read-only version tooling uses the established normalized field names.
10
+ // Product commands remain in the plan; no publish lifecycle is synthesized.
11
+ return {
12
+ ...config,
13
+ version: {
14
+ required: true,
15
+ strategy: plan.version.strategy,
16
+ next: plan.version.strategy === "anchored" ? "manual" : "auto",
17
+ files: plan.version.files.map(({ format, ...file }) => ({
18
+ ...file,
19
+ type: format,
20
+ })),
21
+ derivedFiles: plan.version.derived_files || [],
22
+ },
23
+ };
24
+ }
25
+
26
+ export function readProductConfiguration(cwd, normalizeLegacy) {
27
+ const configPath = resolveBuildchainConfigPath(cwd);
28
+ const filePath = path.join(cwd, configPath);
29
+ if (!fs.existsSync(filePath)) return undefined;
30
+ const source = fs.readFileSync(filePath, "utf8");
31
+ let config;
32
+ try {
33
+ config = parse(source);
34
+ } catch (error) {
35
+ throw new Error(`${configPath} parse failed: ${error.message}`);
36
+ }
37
+ if (![1, 2].includes(config.schema))
38
+ throw new Error(`${configPath} schema must be 1 or 2`);
39
+ return {
40
+ path: configPath,
41
+ filePath,
42
+ config:
43
+ config.schema === 2
44
+ ? productConfiguration(source, config)
45
+ : normalizeLegacy(config),
46
+ };
47
+ }