@kungfu-tech/buildchain 4.1.3-alpha.4 → 4.1.3-alpha.6

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 (79) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/agent-change-map.md +8 -12
  4. package/architecture/capability-state-machine-manifest.json +1 -4
  5. package/architecture/ci-lane-change-budget.json +35 -1026
  6. package/architecture/core-mechanisms.json +3 -6
  7. package/architecture/internal-capabilities.json +1 -5
  8. package/architecture/maintainability-debt.json +44 -36
  9. package/architecture/maintainability-policy.json +8 -8
  10. package/architecture/minimal-consumer-migration.json +80 -1977
  11. package/architecture/platform-stage-checkpoints.json +31 -7
  12. package/architecture/release-topology.json +64 -84
  13. package/architecture/stage-capsule-qualification.json +3 -1
  14. package/architecture/universal-workflow-bootstrap.json +3 -1
  15. package/architecture/universal-workflow-capability-policy.json +4 -4
  16. package/architecture/universal-workflow-fault-campaign.json +3 -1
  17. package/architecture/workflow-taxonomy.json +24 -341
  18. package/dist/site/buildchain-contract.json +13 -67
  19. package/dist/site/buildchain-site.json +33 -23
  20. package/dist/site/capability-registry.json +2 -2
  21. package/dist/site/controller-registry.json +1 -47
  22. package/dist/site/kfd-claims.json +78 -448
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +3 -3
  25. package/dist/site/node-api-registry.json +24 -24
  26. package/dist/site/page-registry.json +26 -16
  27. package/dist/site/public-surface-audit.json +35 -349
  28. package/dist/site/publication-authority-registry.json +51 -511
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +7 -7
  31. package/dist/site/workflow-registry.json +263 -1128
  32. package/docs/code-organization.md +9 -6
  33. package/docs/node-api-reference.md +57 -57
  34. package/docs/release-governance.md +10 -12
  35. package/docs/runtime-entry.md +31 -3
  36. package/docs/runtime-train-validation.md +39 -11
  37. package/docs/stable-candidate-patrol.md +6 -5
  38. package/docs/workflow-catalog.md +3 -32
  39. package/package.json +1 -1
  40. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  41. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  42. package/packages/core/dev-delivery/native/actions.js +2 -0
  43. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  44. package/packages/core/dev-delivery/queue/landing-action.js +49 -1
  45. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  46. package/packages/core/dev-delivery/warrant/service.js +7 -0
  47. package/packages/core/governance/buildchain-publication-authority.js +2 -23
  48. package/packages/core/governance/github-governance-authority.js +1 -0
  49. package/packages/core/observability/controller-evidence.js +0 -8
  50. package/packages/core/providers/github/pipeline-entry-runs.js +8 -1
  51. package/packages/core/providers/github/pipeline-events.js +79 -0
  52. package/packages/core/providers/github/pipeline-source.js +24 -7
  53. package/packages/core/publication/pipeline/pack.js +31 -8
  54. package/packages/core/workflow/pipeline/actions.js +0 -23
  55. package/packages/core/workflow/pipeline/build-evidence.js +123 -5
  56. package/packages/core/workflow/pipeline/build-result.js +1 -0
  57. package/packages/core/workflow/pipeline/channel-control.js +25 -4
  58. package/packages/core/workflow/pipeline/controller.js +9 -1
  59. package/packages/core/workflow/pipeline/delivery-control.js +54 -9
  60. package/packages/core/workflow/pipeline/delivery-observation.js +16 -10
  61. package/packages/core/workflow/pipeline/group-control.js +1 -0
  62. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  63. package/packages/core/workflow/pipeline/guard.js +4 -2
  64. package/packages/core/workflow/pipeline/host.js +23 -1
  65. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  66. package/packages/core/workflow/pipeline/record-action.js +27 -0
  67. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  68. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  69. package/scripts/check-inventory.mjs +7 -70
  70. package/scripts/check-pipeline-publication-topology.mjs +2 -0
  71. package/scripts/check-public-dogfood-contract.mjs +38 -51
  72. package/scripts/check-release-topology.mjs +8 -7
  73. package/scripts/check-self-consumer-contract.mjs +133 -0
  74. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  75. package/scripts/generate-minimal-consumer-contract.mjs +1 -1
  76. package/scripts/inventory/binary.mjs +9 -19
  77. package/scripts/inventory/promotion.mjs +2 -13
  78. package/scripts/prepare-product-toolchain.mjs +10 -0
  79. package/scripts/verify-product-platform.mjs +65 -0
@@ -24,6 +24,24 @@ import { resumePipelineNotifications } from "./notifications.js";
24
24
  import { projectPipelineStatus } from "./web-status.js";
25
25
  import { inspectRecoveryPublication } from "../../publication/pipeline/recovery-inspection.js";
26
26
 
27
+ export function pipelineQueue({ queueToken, ...options }) {
28
+ const reader = new GitHubClient(options);
29
+ return {
30
+ getMergeQueueState: (branch) => reader.getMergeQueueState(branch),
31
+ enqueuePullRequest: async (input) => {
32
+ // GITHUB_TOKEN enqueue suppresses the merge_group workflow it requires.
33
+ if (!queueToken || queueToken === options.token)
34
+ throw new Error(
35
+ "Channel enqueue requires a distinct App or automation credential",
36
+ );
37
+ return new GitHubClient({
38
+ ...options,
39
+ token: queueToken,
40
+ }).enqueuePullRequest(input);
41
+ },
42
+ };
43
+ }
44
+
27
45
  export async function pipelineHost(core, env, jobName) {
28
46
  const token = core.getInput("token", { required: true });
29
47
  const repository = env.GITHUB_REPOSITORY;
@@ -84,7 +102,11 @@ export async function pipelineHost(core, env, jobName) {
84
102
  source,
85
103
  workers: githubPipelineWorker(request),
86
104
  policy: githubPipelinePolicy(request, repository),
87
- queue: new GitHubClient({ repository: { owner, repo }, token }),
105
+ queue: pipelineQueue({
106
+ repository: { owner, repo },
107
+ token,
108
+ queueToken: core.getInput("queue-token"),
109
+ }),
88
110
  integration: githubPipelineIntegration(request, repository, source, runs),
89
111
  groupRecord: (context) => recordPipelineGroup(context, host),
90
112
  channel: (session, admission, inputs) =>
@@ -0,0 +1,113 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ // The landing job already owns statuses:write. Keep its exact admission alive
4
+ // through the actual queue result so later channel PRs need not borrow the
5
+ // temporary queue branch's Actions check suite.
6
+ async function observeProtectedQueue(current, ports) {
7
+ const { repository, request, queue } = ports;
8
+ const now = ports.now || Date.now;
9
+ const pause =
10
+ ports.pause || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
11
+ const deadline = now() + 120 * 60 * 1000;
12
+ const number = current.intent.source.pullRequest;
13
+ const branch = current.intent.source.targetBranch;
14
+ let queueExitAt;
15
+ while (true) {
16
+ const pr = await request(`/repos/${repository}/pulls/${number}`);
17
+ if (
18
+ pr.head?.sha !== current.generation.source.commit ||
19
+ pr.head?.repo?.full_name !== repository ||
20
+ pr.base?.repo?.full_name !== repository ||
21
+ pr.base?.ref !== branch
22
+ )
23
+ throw new Error("Integrated status lost its exact admitted PR source");
24
+ if (pr.merged) return { pr, deadline, now, pause };
25
+ if (now() >= deadline)
26
+ throw new Error(
27
+ "Protected queue did not complete the admitted integration",
28
+ );
29
+ const pending = await queue(branch);
30
+ if (
31
+ pr.state !== "open" ||
32
+ !pending.enabled ||
33
+ !pending.entries.some(
34
+ (entry) =>
35
+ entry.pullRequestNumber === number &&
36
+ entry.pullRequestHeadSha === current.generation.source.commit,
37
+ )
38
+ ) {
39
+ // Queue removal can precede the PR endpoint's merged readback. Bound
40
+ // convergence without treating absence or a closed PR as merge evidence.
41
+ queueExitAt ??= now();
42
+ if (now() - queueExitAt >= 30_000)
43
+ throw new Error("Admitted PR left its protected merge queue");
44
+ } else queueExitAt = undefined;
45
+ await pause(10000);
46
+ }
47
+ }
48
+
49
+ async function observeIntegrationWorkflow(merged, ports) {
50
+ const { pr, deadline, now, pause } = merged;
51
+ if (!/^[0-9a-f]{40}$/u.test(pr.merge_commit_sha || ""))
52
+ throw new Error("Merged PR has no exact integration commit");
53
+ while (true) {
54
+ const result = await ports.request(
55
+ `/repos/${ports.repository}/actions/runs?event=merge_group&head_sha=${pr.merge_commit_sha}&per_page=100`,
56
+ );
57
+ if (!Array.isArray(result.workflow_runs) || result.total_count > 100)
58
+ throw new Error("Integration workflow inventory exceeds its bound");
59
+ const runs = result.workflow_runs.filter(
60
+ (run) =>
61
+ run.path?.split("@")[0] === ".github/workflows/buildchain.yml" &&
62
+ run.event === "merge_group" &&
63
+ run.head_sha === pr.merge_commit_sha,
64
+ );
65
+ if (
66
+ runs.some(
67
+ (run) => run.status === "completed" && run.conclusion === "success",
68
+ )
69
+ )
70
+ return;
71
+ if (!runs.some((run) => run.status !== "completed") || now() >= deadline)
72
+ throw new Error("Merged integration has no successful queue workflow");
73
+ await pause(10000);
74
+ }
75
+ }
76
+
77
+ function validateIntegratedStatus(proof, current, repository) {
78
+ const { root, ...body } = proof;
79
+ if (
80
+ recordDigest(body) !== root ||
81
+ proof.repository !== repository ||
82
+ proof.pullRequest !== current.intent.source.pullRequest ||
83
+ proof.sourceHead !== current.generation.source.commit ||
84
+ proof.branch !== current.intent.source.targetBranch ||
85
+ proof.build.source.commit !== proof.mergeCommit ||
86
+ proof.build.source.repository !== repository ||
87
+ proof.build.outcome !== "success" ||
88
+ proof.run.conclusion !== "success" ||
89
+ proof.run.status !== "completed" ||
90
+ proof.run.event !== "merge_group" ||
91
+ proof.run.head_sha !== proof.mergeCommit
92
+ )
93
+ throw new Error("Integrated status differs from its verified queue result");
94
+ }
95
+
96
+ export async function publishIntegratedPipelineStatus(current, ports) {
97
+ const merged = await observeProtectedQueue(current, ports);
98
+ await observeIntegrationWorkflow(merged, ports);
99
+ const { repository, integration, status } = ports;
100
+ const proof = await integration.observe(current);
101
+ validateIntegratedStatus(proof, current, repository);
102
+ const { root } = proof;
103
+ await status(`/repos/${repository}/statuses/${proof.mergeCommit}`, {
104
+ method: "POST",
105
+ body: {
106
+ context: "check",
107
+ state: "success",
108
+ description: `Qualified protected integration: ${root}`.slice(0, 140),
109
+ target_url: `https://github.com/${repository}/actions/runs/${proof.run.id}/attempts/${proof.run.run_attempt}`,
110
+ },
111
+ });
112
+ return proof;
113
+ }
@@ -0,0 +1,27 @@
1
+ import { pipelineHost } from "./host.js";
2
+ import { recordPipelineBuild } from "./build-control.js";
3
+ import { publishRecorderStatus } from "./recorder-status.js";
4
+ import {
5
+ RECOVERY_BUILD_CONTEXT,
6
+ recordRecoveryBuild,
7
+ } from "./recovery-build-control.js";
8
+
9
+ export async function recordPipelineBuildAction(core, env) {
10
+ const host = await pipelineHost(core, env, "Record product build");
11
+ host.publishBuildStatus = (context, readback) =>
12
+ publishRecorderStatus(context, readback, host);
13
+ const context = JSON.parse(core.getInput("context", { required: true }));
14
+ if (context.schema === "buildchain.pipeline-group-build-context/v1")
15
+ return host.groupRecord(context);
16
+ const result =
17
+ context.schema === RECOVERY_BUILD_CONTEXT
18
+ ? await recordRecoveryBuild(context, host)
19
+ : await recordPipelineBuild(context, host);
20
+ if (result.outcome !== "success")
21
+ throw new Error("Product build did not succeed");
22
+ core.info(
23
+ result.wakePending
24
+ ? "Build retained; next event will retry delivery wake"
25
+ : "Build retained and attempt woken",
26
+ );
27
+ }
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parse } from "yaml";
3
+ import { publishPipelineBuildStatus } from "./guard-build.js";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+
6
+ // A repaired runtime may execute through an older immutable published entry.
7
+ // Read the real recorder job's permission, rather than assuming the runtime's
8
+ // current workflow is the one GitHub scheduled. API failures are never ignored.
9
+ export async function publishRecorderStatus(context, readback, host) {
10
+ const { root, ...body } = readback;
11
+ if (
12
+ context.runId !== host.runId ||
13
+ context.runAttempt !== host.runAttempt ||
14
+ readback.runId !== host.runId ||
15
+ readback.runAttempt !== host.runAttempt ||
16
+ recordDigest(context.source) !== recordDigest(readback.source) ||
17
+ root !== recordDigest(body)
18
+ )
19
+ throw new Error("Recorder status changed its current source or execution");
20
+ const { run } = await host.runs.read(host.runId, host.runAttempt);
21
+ const path = ".github/workflows/.ops-pipeline-execute.yml";
22
+ const entries = (run.referenced_workflows || []).filter((entry) =>
23
+ entry.path?.startsWith(`kungfu-systems/buildchain/${path}@`),
24
+ );
25
+ if (entries.length !== 1 || !/^[0-9a-f]{40}$/u.test(entries[0].sha || ""))
26
+ throw new Error("Recorder has no exact published execution workflow");
27
+ const file = await host.request(
28
+ `/repos/kungfu-systems/buildchain/contents/${path}?ref=${entries[0].sha}`,
29
+ );
30
+ if (
31
+ file.type !== "file" ||
32
+ file.encoding !== "base64" ||
33
+ !Number.isSafeInteger(file.size) ||
34
+ file.size < 1 ||
35
+ file.size > 64000 ||
36
+ typeof file.content !== "string" ||
37
+ file.content.length > 128000
38
+ )
39
+ throw new Error(
40
+ "Recorder execution workflow is unavailable or exceeds its bound",
41
+ );
42
+ const bytes = Buffer.from(file.content, "base64");
43
+ const blob = createHash("sha1")
44
+ .update(`blob ${bytes.length}\0`)
45
+ .update(bytes)
46
+ .digest("hex");
47
+ if (bytes.length !== file.size || blob !== file.sha)
48
+ throw new Error("Recorder execution workflow bytes changed");
49
+ const job = parse(bytes.toString("utf8")).jobs?.["record-build"];
50
+ if (job?.name !== "Record product build")
51
+ throw new Error("Recorder execution workflow changed its job identity");
52
+ if (job.permissions?.statuses !== "write") return { published: false };
53
+ if (readback.outcome !== "success") return { published: false };
54
+ await publishPipelineBuildStatus(
55
+ {
56
+ "expected-head-sha": context.source.commit,
57
+ "source-workflow-run-id": String(readback.runId),
58
+ },
59
+ {
60
+ sourceHead: context.source.commit,
61
+ outcome: readback.outcome,
62
+ run: { id: readback.runId, run_attempt: readback.runAttempt },
63
+ readback,
64
+ },
65
+ host.request,
66
+ host.repository,
67
+ );
68
+ return { published: true };
69
+ }
@@ -20,7 +20,8 @@ const PROTECTED = [
20
20
  "scripts/generate-workflow-taxonomy.mjs",
21
21
  "scripts/check-workflows.sh",
22
22
  "tests/workflow-taxonomy.test.mjs",
23
- ".github/workflows/self-build-verify.yml",
23
+ ".github/workflows/buildchain.yml",
24
+ ".github/workflows/buildchain-recover.yml",
24
25
  "package.json",
25
26
  ".buildchain/buildchain.toml",
26
27
  ];
@@ -63,12 +64,13 @@ function validateEntries(policy, errors) {
63
64
  if (
64
65
  entry.path &&
65
66
  !(
66
- (entry.id === ".build" || entry.id === "build") &&
67
- entry.path === `.github/workflows/${entry.id}.yml`
67
+ [".build", "build", "buildchain", "buildchain-recover"].includes(
68
+ entry.id,
69
+ ) && entry.path === `.github/workflows/${entry.id}.yml`
68
70
  )
69
71
  )
70
72
  errors.push(
71
- `${entry.id}: explicit path is reserved for the build facade and backbone`,
73
+ `${entry.id}: explicit path is reserved for build APIs and the generated consumer pair`,
72
74
  );
73
75
  const file = workflowPath(entry);
74
76
  if (!WORKFLOW.test(file) || paths.has(file))
@@ -191,35 +193,53 @@ function validateGateIntegration(root, errors) {
191
193
  )
192
194
  )
193
195
  errors.push("required check chain does not enforce check:workflows");
194
- const { parse: parseToml } = createRequire(import.meta.url)("smol-toml");
195
- const config = parseToml(read(".buildchain/buildchain.toml"));
196
+ const require = createRequire(import.meta.url);
197
+ const { parse: parseYaml } = require("yaml");
198
+ const { compileConsumerPlan } = require("../consumer/contract/plan.js");
199
+ const { consumerWorkflows } = require("../consumer/contract/entries.js");
200
+ const config = compileConsumerPlan(read(".buildchain/buildchain.toml"));
201
+ for (const platform of ["linux-x64", "macos-arm64", "windows-x64"])
202
+ if (
203
+ !config.products.some(
204
+ (product) =>
205
+ product.platforms.includes(platform) &&
206
+ product.verify.includes("corepack pnpm@11.7.0 run check") &&
207
+ product.verify.includes("node scripts/verify-product-platform.mjs"),
208
+ )
209
+ )
210
+ errors.push(
211
+ `${platform}: declared product verification must execute the full required check and checkpoint recovery`,
212
+ );
213
+ const caller = parseYaml(read(".github/workflows/buildchain.yml"));
214
+ const channel = caller.jobs?.buildchain?.uses?.split("@").at(-1);
215
+ const configPath = caller.jobs?.buildchain?.with?.["config-path"];
216
+ for (const [file, expected] of Object.entries(
217
+ consumerWorkflows(channel, configPath),
218
+ ))
219
+ if (read(file) !== expected)
220
+ errors.push(`${file}: required generated pipeline caller drift`);
221
+ const execution = parseYaml(
222
+ read(".github/workflows/.ops-pipeline-execute.yml"),
223
+ );
224
+ const build = execution.jobs?.build;
225
+ const record = execution.jobs?.["record-build"];
196
226
  if (
197
- !config.lifecycle?.verify?.commands?.includes(
198
- "corepack pnpm@11.7.0 run check",
227
+ build?.permissions?.contents !== "read" ||
228
+ Object.values(build?.permissions || {}).includes("write") ||
229
+ !build?.steps?.some(
230
+ (step) =>
231
+ step.uses === "./.buildchain/runtime/actions/workflow/pipeline/build",
232
+ ) ||
233
+ ![record?.needs].flat().includes("build") ||
234
+ !record?.steps?.some(
235
+ (step) =>
236
+ step.uses ===
237
+ "./.buildchain/runtime/actions/workflow/pipeline/record-build",
199
238
  )
200
239
  )
201
240
  errors.push(
202
- "declared verify lifecycle must execute the full required check",
241
+ "required product execution and independent source qualification boundary is missing",
203
242
  );
204
- const verify = read(".github/workflows/self-build-verify.yml");
205
- const document = parseWorkflowDocument(verify);
206
- for (const trigger of ["pull_request", "merge_group", "push"]) {
207
- if (!document.triggers.some((item) => item.split(":")[0] === trigger))
208
- errors.push(`required Verify workflow lacks ${trigger}`);
209
- }
210
- const checkJob = verify.match(/^ check:\n([\s\S]*?)(?=^ [\w-]+:|(?![\s\S]))/mu)?.[1] || "";
211
- const verifyNode = "./.buildchain/runtime/actions/build/verification/repository";
212
- const verifyAction = read("actions/build/verification/repository/action.yml");
213
- const sourceAction = read("actions/build/verification/qualify-source/action.yml");
214
- const sourceVerification = read("packages/core/build/verification/source.js");
215
- const sourceLifecycle = read("packages/core/build/source/lifecycle.js");
216
- if (!parseYamlUses(checkJob).some(call => call.value === verifyNode) ||
217
- !parseYamlUses(verifyAction).some(call => call.value === "./.buildchain/runtime/actions/build/verification/qualify-source") ||
218
- !/using: node24/u.test(sourceAction) ||
219
- !/qualify = qualifySourceLifecycle/u.test(sourceVerification) ||
220
- !/mode: "verify"/u.test(sourceVerification) ||
221
- !/lifecycle\(\{[\s\S]*stageName,[\s\S]*required: true/u.test(sourceLifecycle))
222
- errors.push("required Verify lifecycle integration is missing");
223
243
  const owners = read(".github/CODEOWNERS")
224
244
  .split(/\r?\n/u)
225
245
  .map((line) => line.trim());
@@ -306,7 +326,7 @@ export function renderWorkflowCatalog(policy) {
306
326
  "",
307
327
  "Public workflows own the consumer API. Component workflows own reusable multi-job topology. Self workflows own Buildchain repository automation. Actions own execution steps; JS adapters and Rust/WASM own implementation.",
308
328
  "",
309
- "Names use `public-<category>-<purpose>.yml`, `.<category>-<purpose>.yml`, and `self-<category>-<purpose>.yml`, with categories `build`, `release`, and `ops`. The primary build API and backbone use `build.yml` and `.build.yml`.",
329
+ "Names use `public-<category>-<purpose>.yml`, `.<category>-<purpose>.yml`, and the generated self callers `buildchain.yml` and `buildchain-recover.yml`, with categories `build`, `release`, and `ops`. The primary build API and backbone use `build.yml` and `.build.yml`.",
310
330
  "",
311
331
  "Register ownership before adding a workflow. `pnpm run check:workflows` validates source, calls, required CI integration and independent review ownership. `pnpm run generate:workflows` regenerates this catalog.",
312
332
  "",
@@ -330,22 +350,22 @@ export function renderWorkflowCatalog(policy) {
330
350
  }
331
351
 
332
352
  function validateEntryOwnership(entry, errors) {
333
- for (const field of ["summary", "owner", "rationale"])
334
- if (typeof entry[field] !== "string" || !entry[field].trim())
335
- errors.push(`${entry.id}: missing ${field}`);
336
- if (!["active", "preview"].includes(entry.status))
337
- errors.push(`${entry.id}: unsupported lifecycle status`);
338
- for (const field of ["compatibility", "migration", "retiredAlias"])
339
- if (field in entry)
340
- errors.push(
341
- `${entry.id}: ${field} is not an executable architecture contract`,
342
- );
343
- if (
344
- !["reusable", "dispatch-service", "repository"].includes(entry.invocation)
345
- )
346
- errors.push(`${entry.id}: invalid invocation`);
347
- if ((entry.role === "self") !== (entry.invocation === "repository"))
348
- errors.push(`${entry.id}: role and invocation disagree`);
349
- if (entry.role === "component" && entry.invocation !== "reusable")
350
- errors.push(`${entry.id}: component must be reusable`);
353
+ for (const field of ["summary", "owner", "rationale"])
354
+ if (typeof entry[field] !== "string" || !entry[field].trim())
355
+ errors.push(`${entry.id}: missing ${field}`);
356
+ if (!["active", "preview"].includes(entry.status))
357
+ errors.push(`${entry.id}: unsupported lifecycle status`);
358
+ for (const field of ["compatibility", "migration", "retiredAlias"])
359
+ if (field in entry)
360
+ errors.push(
361
+ `${entry.id}: ${field} is not an executable architecture contract`,
362
+ );
363
+ if (
364
+ !["reusable", "dispatch-service", "repository"].includes(entry.invocation)
365
+ )
366
+ errors.push(`${entry.id}: invalid invocation`);
367
+ if ((entry.role === "self") !== (entry.invocation === "repository"))
368
+ errors.push(`${entry.id}: role and invocation disagree`);
369
+ if (entry.role === "component" && entry.invocation !== "reusable")
370
+ errors.push(`${entry.id}: component must be reusable`);
351
371
  }
@@ -1,3 +1,4 @@
1
+ import { checkSelfConsumerContract } from "./check-self-consumer-contract.mjs";
1
2
  import { assertBinaryInventory } from "./inventory/binary.mjs";
2
3
  import YAML from "yaml";
3
4
  import { actionInventory } from "../packages/core/contracts/action-inventory.js";
@@ -145,41 +146,29 @@ const requiredPaths = [
145
146
  ".buildchain/alpha-contract-lock.json",
146
147
  ".buildchain/release-impact.json",
147
148
  ".github/actionlint.yaml",
148
- ".github/workflows/self-ops-runner-smoke.yml",
149
- ".github/workflows/self-release-promote.yml",
150
- ".github/workflows/self-release-line-open.yml",
151
149
  ".github/workflows/public-ops-release-governance.yml",
152
150
  ".github/workflows/public-ops-dev-auto-merge.yml",
153
- ".github/workflows/self-ops-dev-delivery.yml",
154
151
  ".github/workflows/public-ops-patrol.yml",
155
152
  ".github/workflows/public-ops-patrol-daily.yml",
156
153
  ".github/workflows/public-ops-patrol-weekly.yml",
157
154
  ".github/workflows/public-ops-patrol-monthly.yml",
158
155
  ".github/workflows/public-ops-observed-evidence.yml",
159
- ".github/workflows/self-ops-patrol-daily.yml",
160
- ".github/workflows/self-ops-patrol-weekly.yml",
161
- ".github/workflows/self-ops-patrol-monthly.yml",
162
- ".github/workflows/self-build-alpha-dogfood.yml",
163
- ".github/workflows/self-build-stable-dogfood.yml",
164
156
  ".github/workflows/public-release-promote.yml",
165
157
  ".github/workflows/.release-promote.yml",
166
158
  ".github/workflows/public-release-propagation.yml",
167
- ".github/workflows/self-release-npm-dry-run.yml",
168
159
  ".github/workflows/public-release-paper.yml",
169
- ".github/workflows/self-build-binary-distribution.yml",
170
160
  ".github/workflows/.release-binary-assets.yml",
171
- ".github/workflows/self-release-binary-assets.yml",
172
- ".github/workflows/self-build-verify.yml",
173
161
  ".github/workflows/.build.yml",
174
162
  ".github/workflows/.build-gate-profile.yml",
175
163
  ".github/workflows/.build-demo-adapter.yml",
176
164
  ".github/workflows/public-build-demo.yml",
177
- ".github/workflows/self-build-demo-dogfood.yml",
178
165
  ".github/workflows/build.yml",
179
- ".github/workflows/self-build-fixture.yml",
180
166
  "fixtures/libnode-shaped/buildchain.toml",
181
167
  "fixtures/libnode-shaped/.github/workflows/build.yml",
182
- "fixtures/libnode-shaped/package.json"
168
+ "fixtures/libnode-shaped/package.json",
169
+ ".github/workflows/buildchain.yml",
170
+ ".github/workflows/buildchain-recover.yml",
171
+ "scripts/check-self-consumer-contract.mjs",
183
172
  ];
184
173
 
185
174
  for (const rel of requiredPaths) {
@@ -277,14 +266,7 @@ for (const channel of ["alpha-contract-lock.json", "contract-lock.json"]) {
277
266
  if (lock.buildchain.ref !== ref)
278
267
  throw new Error(`${channel}: invalid accepted runtime selector`);
279
268
  }
280
- for (const [channel, ref] of [["alpha", `v${selfDogfoodMajor}-alpha`], ["stable", `v${selfDogfoodMajor}`]]) {
281
- const workflow = fs.readFileSync(path.join(root, `.github/workflows/self-build-${channel}-dogfood.yml`), "utf8");
282
- if (!workflow.includes(`/.github/workflows/build.yml@${ref}`) || /steps:|buildchain-channel:|runner-preset:|working-directory:/u.test(workflow)) throw new Error(`${channel} self-dogfood must remain a thin public TOML build caller`);
283
- if (!workflow.includes(`group: buildchain-${channel}-self-dogfood-`) || !workflow.includes("cancel-in-progress: false")) throw new Error(`${channel} self-dogfood must serialize its own runs`);
284
- const expectedInputs = [];
285
- const call = parseWorkflowCallJobs(workflow).find((job) => job.id === `${channel}-consumer`);
286
- if (JSON.stringify(Object.keys(call.with || {})) !== JSON.stringify(expectedInputs)) throw new Error(`${channel} self-dogfood input contract drift`);
287
- }
269
+ checkSelfConsumerContract(root);
288
270
  const reusableBuildWorkflow = fs.readFileSync(
289
271
  path.join(root, ".github/workflows/.build.yml"),
290
272
  "utf8",
@@ -310,10 +292,6 @@ const channelPromotionWorkflow = fs.readFileSync(
310
292
  path.join(root, ".github/workflows/public-release-promote.yml"),
311
293
  "utf8",
312
294
  );
313
- const boundedAlphaRecoveryWorkflow = fs.readFileSync(
314
- path.join(root, ".github/workflows/self-ops-promotion-recovery.yml"),
315
- "utf8",
316
- );
317
295
  assertPromotionInventory(root);
318
296
  for (const workflow of [channelBuildWorkflow, reusableBuildWorkflow]) {
319
297
  const block = workflow.split(" inputs:\n")[1].split(" secrets:\n")[0];
@@ -957,11 +935,6 @@ for (const requiredSnippet of [
957
935
  if (commonJsSourcePattern.test(standaloneBinaryScript)) {
958
936
  throw new Error("scripts/build-standalone-binary.mjs must use ESM syntax");
959
937
  }
960
- const npmPublishWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-release-npm-dry-run.yml"), "utf8");
961
- const buildchainRefPromotionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-release-promote.yml"), "utf8");
962
- const binaryDistributionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-build-binary-distribution.yml"), "utf8");
963
- const binaryReleaseAssetsWorkflow = fs.readFileSync(path.join(root, ".github/workflows/.release-binary-assets.yml"), "utf8");
964
- const selfHostedRunnerSmokeWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-ops-runner-smoke.yml"), "utf8");
965
938
  const npmDryRunScript = fs.readFileSync(path.join(root, "packages/core/publication/npm/preview.js"), "utf8").replace(/\s+/gu, " ");
966
939
  const npmPublishTransactionScript = ["transaction", "environment"].map(name => fs.readFileSync(path.join(root, `packages/core/publication/npm/${name}.js`), "utf8")).join("\n");
967
940
  const rootPackageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
@@ -977,29 +950,6 @@ if (!["patch", "minor", "major"].includes(selfReleaseImpact.classification)) {
977
950
  if (!Array.isArray(selfReleaseImpact.surfaceImpacts) || selfReleaseImpact.surfaceImpacts.length === 0) {
978
951
  throw new Error("Buildchain self release impact requires a summary and surfaceImpacts[]");
979
952
  }
980
- for (const requiredSnippet of [
981
- "runs-on: ubuntu-24.04",
982
- "workflow_dispatch:",
983
- "Dry-run npm publish",
984
- ]) {
985
- if (!npmPublishWorkflow.includes(requiredSnippet)) {
986
- throw new Error(`npm publish workflow missing required snippet: ${requiredSnippet}`);
987
- }
988
- }
989
- for (const forbiddenSnippet of [
990
- "tags:",
991
- "Publish exact release tag",
992
- "npm publish --access public --tag",
993
- ]) {
994
- if (npmPublishWorkflow.includes(forbiddenSnippet)) {
995
- throw new Error(`npm publish dry-run workflow must not contain real publish snippet: ${forbiddenSnippet}`);
996
- }
997
- }
998
- const selfPromotion = inspectWorkflowJob(".github/workflows/self-release-promote.yml", "classify-workflow-run", root);
999
- if (![...selfPromotion.modules.values()].some(source => source.includes("selectFinalizedProductPublicationVersion")))
1000
- throw new Error("self promotion must classify finalization from rooted product state");
1001
- if (buildchainRefPromotionWorkflow.includes("workflow_run.display_title"))
1002
- throw new Error("self promotion must not classify finalization from a display title");
1003
953
  const workflowDir = path.join(root, ".github/workflows");
1004
954
  for (const workflowFile of fs.readdirSync(workflowDir).filter((entry) => entry.endsWith(".yml"))) {
1005
955
  const workflowPath = path.join(workflowDir, workflowFile);
@@ -1054,14 +1004,6 @@ for (const requiredSnippet of [
1054
1004
  throw new Error(`promote-buildchain-ref index missing semver GitHub Release implementation: ${requiredSnippet}`);
1055
1005
  }
1056
1006
  }
1057
- for (const forbiddenSnippet of [
1058
- "run: node packages/core/release/commands/release-candidate-resolver.mjs",
1059
- "uses: ./actions/release/promotion/ref",
1060
- ]) {
1061
- if (buildchainRefPromotionWorkflow.includes(forbiddenSnippet)) {
1062
- throw new Error(`buildchain ref promotion workflow must use the declarative wrapper, found manual snippet: ${forbiddenSnippet}`);
1063
- }
1064
- }
1065
1007
  for (const requiredSnippet of [
1066
1008
  "distTag || (pkg.version.includes(\"-\") ? \"alpha\" : \"latest\")",
1067
1009
  "\"publish\", \"--dry-run\", \"--access\", \"public\"",
@@ -1089,13 +1031,8 @@ for (const requiredSnippet of [
1089
1031
  if (commonJsSourcePattern.test(npmPublishTransactionScript)) {
1090
1032
  throw new Error("packages/core/publication/commands/npm-publish-transaction.mjs must use ESM syntax");
1091
1033
  }
1092
- if (/runs-on:\s*self-hosted/.test(npmPublishWorkflow)) {
1093
- throw new Error("npm publish workflow must use GitHub-hosted runners for trusted publishing");
1094
- }
1095
1034
  assertBinaryInventory(root);
1096
- if (!inspectWorkflowJob(".github/workflows/self-ops-runner-smoke.yml", "smoke", root).modules.get("packages/core/build/verification/runner.js")?.includes('runnerKind: "self-hosted"')) {
1097
- throw new Error("self-hosted smoke must identify its actual runner kind");
1098
- }
1035
+
1099
1036
 
1100
1037
  const inventory = JSON.parse(
1101
1038
  fs.readFileSync(path.join(root, "tests/buildchain-inventory.json"), "utf8")
@@ -13,6 +13,8 @@ export function assertPipelinePublicationTopology(
13
13
  ".github/workflows/public-ops-recover.yml",
14
14
  ".github/workflows/.ops-pipeline-execute.yml",
15
15
  ".github/workflows/.release-pipeline-version.yml",
16
+ ".github/workflows/buildchain.yml",
17
+ ".github/workflows/buildchain-recover.yml",
16
18
  ]);
17
19
  const pipelineTopology = discoverReleaseTopology(
18
20
  pipeline.workflowPaths,