@kungfu-tech/buildchain 4.1.6-alpha.0 → 4.1.7-alpha.0

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 (101) hide show
  1. package/AGENTS.md +7 -5
  2. package/CONTRIBUTING.md +12 -6
  3. package/README.md +9 -5
  4. package/architecture/action-taxonomy.json +2 -0
  5. package/architecture/agent-change-map.md +6 -1
  6. package/architecture/build-orchestration.json +7 -1
  7. package/architecture/ci-lane-change-budget.json +1836 -0
  8. package/architecture/consumer-upgrade.json +6482 -0
  9. package/architecture/internal-capabilities.json +5 -2
  10. package/architecture/maintainability-debt.json +18 -10
  11. package/architecture/maintainability-policy.json +9 -9
  12. package/architecture/release-topology.json +10 -1
  13. package/architecture/universal-workflow-bootstrap.json +88 -18
  14. package/architecture/universal-workflow-capability-policy.json +2 -2
  15. package/architecture/workflow-taxonomy.json +2 -1
  16. package/contracts/dev-delivery-authority-v2.schema.json +29 -3
  17. package/contracts/fixtures/consumer-upgrade/build-v4.0.0.json +764 -0
  18. package/contracts/fixtures/consumer-upgrade/consumer-calls.json +447 -0
  19. package/contracts/fixtures/consumer-upgrade/dev-delivery-identity-v4.0.0.json +27 -0
  20. package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.0.json +256 -0
  21. package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.9.json +358 -0
  22. package/contracts/fixtures/consumer-upgrade/release-v4.0.0.json +815 -0
  23. package/contracts/promotion-invocation-v1.schema.json +5 -0
  24. package/dist/site/buildchain-contract.json +40 -23
  25. package/dist/site/buildchain-site.json +20 -20
  26. package/dist/site/capability-registry.json +5 -5
  27. package/dist/site/controller-registry.json +17 -5
  28. package/dist/site/kfd-claims.json +1376 -143
  29. package/dist/site/kfd-upstream-aggregate.json +1 -1
  30. package/dist/site/manual-registry.json +4 -4
  31. package/dist/site/node-api-registry.json +22 -22
  32. package/dist/site/page-registry.json +14 -14
  33. package/dist/site/public-surface-audit.json +1733 -101
  34. package/dist/site/publication-authority-registry.json +651 -32
  35. package/dist/site/publication-registry.json +1 -1
  36. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +29 -3
  37. package/dist/site/site-manifest.json +5 -5
  38. package/dist/site/workflow-registry.json +2501 -149
  39. package/docs/MAP.md +5 -4
  40. package/docs/code-organization.md +11 -8
  41. package/docs/getting-started.md +31 -2
  42. package/docs/migration-inventory.md +8 -6
  43. package/docs/node-api-reference.md +36 -36
  44. package/docs/workflow-path-migration.md +8 -7
  45. package/package.json +1 -1
  46. package/packages/core/build/artifact/contracts.js +10 -2
  47. package/packages/core/build/artifact/transport.js +1 -1
  48. package/packages/core/build/artifact/upload.js +2 -2
  49. package/packages/core/build/plan/action.js +2 -0
  50. package/packages/core/build/plan/admission.js +9 -3
  51. package/packages/core/build/plan/compatibility-artifacts.js +47 -0
  52. package/packages/core/build/plan/compatibility-plan.js +121 -0
  53. package/packages/core/build/plan/compatibility.js +162 -0
  54. package/packages/core/build/plan/configuration.js +10 -37
  55. package/packages/core/build/plan/identity.js +42 -0
  56. package/packages/core/build/plan/lifecycle.js +1 -0
  57. package/packages/core/build/plan/resolve.js +2 -0
  58. package/packages/core/build/summary/aggregation.js +4 -2
  59. package/packages/core/build/summary/compatibility.js +24 -0
  60. package/packages/core/build/summary/finalization.js +18 -0
  61. package/packages/core/build/summary/historical-outputs.js +88 -0
  62. package/packages/core/contracts/runtime-contract-inspection.js +8 -0
  63. package/packages/core/dev-delivery/candidate/submission.js +2 -1
  64. package/packages/core/dev-delivery/commands/dev-delivery-authority-command-adapters.mjs +2 -1
  65. package/packages/core/dev-delivery/commands/dev-delivery-authority.mjs +6 -5
  66. package/packages/core/dev-delivery/commands/dev-delivery-warrant-options.mjs +22 -3
  67. package/packages/core/dev-delivery/dev-delivery-candidate-identity.js +13 -4
  68. package/packages/core/dev-delivery/dev-delivery-warrant.js +4 -1
  69. package/packages/core/dev-delivery/warrant/handoff.js +2 -1
  70. package/packages/core/dev-delivery/warrant/transitions.js +2 -2
  71. package/packages/core/governance/buildchain-publication-authority.js +5 -9
  72. package/packages/core/release/github-release-metadata.js +58 -0
  73. package/packages/core/release/promote-candidate/action.js +4 -2
  74. package/packages/core/release/promote-candidate/preparation.js +6 -0
  75. package/packages/core/release/promote-candidate/publication.js +36 -0
  76. package/packages/core/release/promote-candidate/release-documents.js +1 -1
  77. package/packages/core/release/promotion/candidate.js +4 -2
  78. package/packages/core/release/promotion/compatibility-context.js +93 -0
  79. package/packages/core/release/promotion/compatibility-qualification.js +48 -0
  80. package/packages/core/release/promotion/compatibility.js +141 -0
  81. package/packages/core/release/promotion/historical-evidence-action.js +17 -0
  82. package/packages/core/release/promotion/historical-evidence-files.js +112 -0
  83. package/packages/core/release/promotion/historical-evidence.js +231 -0
  84. package/packages/core/release/promotion/qualification.js +12 -1
  85. package/packages/core/release/promotion/routing.js +3 -2
  86. package/packages/core/release/promotion-request.js +31 -2
  87. package/packages/core/release/release-tail-provider-adapters.js +18 -0
  88. package/packages/core/runtime/entry/actions.js +4 -1
  89. package/packages/core/runtime/entry/compatibility.js +53 -0
  90. package/packages/core/workflow/engine/conformance.js +7 -3
  91. package/packages/core/workflow/pipeline/controller.js +26 -26
  92. package/scripts/check-dev-delivery-actions.mjs +12 -0
  93. package/scripts/check-inventory.mjs +1 -1
  94. package/scripts/check-public-dogfood-contract.mjs +2 -0
  95. package/scripts/check-release-topology.mjs +5 -1
  96. package/scripts/check-self-consumer-contract.mjs +5 -1
  97. package/scripts/generate-bootstrap-admission.mjs +20 -0
  98. package/scripts/generate-site-bundle.mjs +5 -5
  99. package/scripts/generate-workflow-taxonomy.mjs +17 -0
  100. package/scripts/maintainability-metrics.mjs +30 -2
  101. package/scripts/maintainability-public-surface.mjs +5 -3
@@ -0,0 +1,53 @@
1
+ // Retain old selector names at the historical workflow boundary. The selected
2
+ // runtime still goes through the same lock validation and trusted recovery gate.
3
+ export function historicalEntrySelection(value, workflowRef) {
4
+ if (!value) return {};
5
+ const inputs = typeof value === "string" ? JSON.parse(value) : value;
6
+ if (!inputs || typeof inputs !== "object" || Array.isArray(inputs))
7
+ throw new Error("Historical entry inputs must be an object");
8
+ const read = (name, fallback = "") => {
9
+ const result = inputs[name] ?? fallback;
10
+ if (typeof result !== "string")
11
+ throw new Error(`Invalid historical selector: ${name}`);
12
+ return result;
13
+ };
14
+ if (
15
+ read("buildchain-repository", "kungfu-systems/buildchain") !==
16
+ "kungfu-systems/buildchain"
17
+ )
18
+ throw new Error(
19
+ "Historical entry requires the official Buildchain repository",
20
+ );
21
+ const calledChannel = /@(?:refs\/tags\/)?v4-alpha$/u.test(workflowRef)
22
+ ? "alpha"
23
+ : "stable";
24
+ const requested = read("buildchain-channel", "auto");
25
+ if (!["auto", "alpha", "stable"].includes(requested))
26
+ throw new Error("Invalid historical Buildchain channel");
27
+ const channel = requested === "auto" ? calledChannel : requested;
28
+ let ref = read("buildchain-ref").replace(/^v[23](-alpha)?$/u, "v4$1");
29
+ if (workflowRef?.split("@")[0] === "kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml") {
30
+ const recovery = read("resume-buildchain-runtime-sha");
31
+ if (recovery && !/^[a-f0-9]{40}$/u.test(recovery))
32
+ throw new Error("Historical recovery selector requires an exact runtime SHA");
33
+ if (recovery) ref = recovery;
34
+ }
35
+ const stableLock = read(
36
+ "buildchain-stable-contract-lock-path",
37
+ ".buildchain/contract-lock.json",
38
+ );
39
+ const alphaLock = read(
40
+ "buildchain-alpha-contract-lock-path",
41
+ ".buildchain/alpha-contract-lock.json",
42
+ );
43
+ return {
44
+ runtimeRef:
45
+ ref && ref !== (calledChannel === "alpha" ? "v4-alpha" : "v4") ? ref : "",
46
+ lockPath:
47
+ read("buildchain-contract-lock-path") ||
48
+ (channel === "alpha" ? alphaLock : stableLock),
49
+ channel,
50
+ stableLock,
51
+ alphaLock,
52
+ };
53
+ }
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { compatibilityWorkflowTarget } from "../../consumer/compatibility-workflows.js";
4
5
  import { exactObject, exactRuntime, fail, contentRoot } from "./identity.js";
5
6
  function rawFileRoot(runtimeRoot, relative) {
6
7
  const hash = crypto.createHash("sha256");
@@ -41,14 +42,17 @@ export function executeBootstrapConformance(
41
42
  ),
42
43
  ),
43
44
  );
45
+ const governed = [...new Set(architecture.bootstrapGovernedWorkflows.map(relative =>
46
+ compatibilityWorkflowTarget(runtimeRoot, relative, fs.readFileSync(path.join(runtimeRoot, relative), "utf8"))
47
+ ))];
44
48
  if (
45
- architecture.bootstrapGovernedWorkflows.length !==
49
+ governed.length !==
46
50
  request.payload.expectedGovernedWorkflowCount ||
47
51
  architecture.status !== "active" ||
48
52
  policy.allowedCapabilities.includes("workflow-contract")
49
53
  )
50
54
  fail("candidate Bootstrap conformance does not close execution semantics");
51
- for (const relative of architecture.bootstrapGovernedWorkflows) {
55
+ for (const relative of governed) {
52
56
  const source = fs.readFileSync(path.join(runtimeRoot, relative), "utf8");
53
57
  if (
54
58
  relative !== architecture.bootstrap.publicWorkflow &&
@@ -60,7 +64,7 @@ export function executeBootstrapConformance(
60
64
  schema: "kungfu-buildchain-v4-universal-bootstrap-conformance-result/v1",
61
65
  status: "candidate-engine-executed",
62
66
  runtime: exactRuntime(admission),
63
- governedWorkflowCount: architecture.bootstrapGovernedWorkflows.length,
67
+ governedWorkflowCount: governed.length,
64
68
  architectureRoot: rawFileRoot(
65
69
  runtimeRoot,
66
70
  "architecture/universal-workflow-bootstrap.json",
@@ -37,6 +37,11 @@ async function groupBuild(event, inputs, host) {
37
37
  return { operation: "build", context };
38
38
  }
39
39
 
40
+ async function wakePipeline(host, attempt, reason) {
41
+ await host.wake(attempt);
42
+ return { operation: "wait", reason, attempt };
43
+ }
44
+
40
45
  export async function controlPipeline(name, payload, inputs, host) {
41
46
  const event = pipelineEvent(name, payload, host.repository);
42
47
  host.terminalOnly = event.terminalOnly;
@@ -66,14 +71,12 @@ export async function controlPipeline(name, payload, inputs, host) {
66
71
  if (
67
72
  current.identity.requestKey.startsWith("recover:") &&
68
73
  recordDigest(current.events[0].runtime) !== recordDigest(host.runtime)
69
- ) {
70
- await host.wake(current.identity.id);
71
- return {
72
- operation: "wait",
73
- reason: "admitted-recovery-runtime-continuation-required",
74
- attempt: current.identity.id,
75
- };
76
- }
74
+ )
75
+ return wakePipeline(
76
+ host,
77
+ current.identity.id,
78
+ "admitted-recovery-runtime-continuation-required",
79
+ );
77
80
  if (
78
81
  ["failure", "cancelled", "superseded", "complete"].includes(
79
82
  session.observed.status,
@@ -90,14 +93,12 @@ export async function controlPipeline(name, payload, inputs, host) {
90
93
  if (
91
94
  admission.live.source &&
92
95
  host.selection.source.sha !== admission.live.source.commit
93
- ) {
94
- await host.wake(session.observed.attempt);
95
- return {
96
- operation: "wait",
97
- reason: "source-generation-runtime-selection-required",
98
- attempt: session.observed.attempt,
99
- };
100
- }
96
+ )
97
+ return wakePipeline(
98
+ host,
99
+ session.observed.attempt,
100
+ "source-generation-runtime-selection-required",
101
+ );
101
102
  if (session.intent.expectedNodes.includes("warrant"))
102
103
  host.queueExit ||= githubPipelineQueueExit(
103
104
  host.github.graphql,
@@ -118,16 +119,15 @@ export async function controlPipeline(name, payload, inputs, host) {
118
119
  return { operation: "build", context };
119
120
  }
120
121
  await host.project(session);
121
- if (operation.operation === "successor") {
122
- // Closed or retargeted PRs do not authorize a new attempt on the old route.
123
- // A later normal event re-admits a lawful source after the old candidate ends.
124
- await host.wake(session.observed.attempt);
125
- return {
126
- operation: "wait",
127
- reason: operation.reason,
128
- attempt: session.observed.attempt,
129
- };
130
- }
122
+ // PR run.head_sha and signing certificates can name different commits.
123
+ const dispatchPublication =
124
+ operation.operation === "publish" && event.kind !== "attempt";
125
+ if (operation.operation === "successor" || dispatchPublication)
126
+ return wakePipeline(
127
+ host,
128
+ session.observed.attempt,
129
+ operation.reason || "publication-requires-a-normal-dispatch",
130
+ );
131
131
  if (operation.operation === "settle") {
132
132
  const settled = await host.settle(
133
133
  session,
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import YAML from "yaml";
8
8
  import { lowerSelfReferencesForLint } from "./workflow-self-reference.mjs";
9
+ import { compatibilityWorkflowTarget } from "../packages/core/consumer/compatibility-workflows.js";
9
10
  const root = process.cwd();
10
11
  const contract = JSON.parse(fs.readFileSync(path.join(root, "architecture/dev-delivery-orchestration.json"), "utf8"));
11
12
  const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-delivery-actionlint-")));
@@ -16,6 +17,17 @@ try {
16
17
  fs.mkdirSync(path.join(temp, ".buildchain"));
17
18
  fs.symlinkSync(path.join(root, "actions"), path.join(temp, "actions"), "junction");
18
19
  const files = fs.readdirSync(workflows).filter(file => /\.ya?ml$/u.test(file)).map(file => path.join(workflows, file));
20
+ const configPath = path.join(temp, ".github/actionlint.yaml");
21
+ const config = YAML.parse(fs.readFileSync(configPath, "utf8"));
22
+ for (const file of files) {
23
+ const relative = `.github/workflows/${path.basename(file)}`;
24
+ const canonical = compatibilityWorkflowTarget(root, relative, fs.readFileSync(file, "utf8"));
25
+ // Verified generated copies inherit only their canonical source's scoped
26
+ // rules for GitHub contexts newer than the pinned actionlint release.
27
+ if (canonical !== relative && config.paths?.[canonical])
28
+ config.paths[relative] = config.paths[canonical];
29
+ }
30
+ fs.writeFileSync(configPath, YAML.stringify(config));
19
31
  for (const node of contract.nodes) for (const implementation of node.implementations) {
20
32
  const action = YAML.parse(fs.readFileSync(path.join(root, implementation.action), "utf8"));
21
33
  assert.deepEqual(Object.keys(action.inputs || {}), implementation.inputs, `${implementation.action}: input contract drift`);
@@ -296,7 +296,7 @@ assertPromotionInventory(root);
296
296
  for (const workflow of [channelBuildWorkflow, reusableBuildWorkflow]) {
297
297
  const block = workflow.split(" inputs:\n")[1].split(" secrets:\n")[0];
298
298
  const names = [...block.matchAll(/^ ([a-z0-9-]+):$/gm)].map((match) => match[1]);
299
- if (JSON.stringify(names.sort()) !== JSON.stringify(['config-path','contract-lock','resume-run-id','runtime-ref','runtime-selection'])) throw new Error('build API must expose only the locator and unified runtime entry inputs');
299
+ if (JSON.stringify(names.sort()) !== JSON.stringify(['compatibility-inputs','config-path','contract-lock','resume-run-id','runtime-ref','runtime-selection'])) throw new Error('build component must expose only its locator, runtime transport and historical argument transport');
300
300
  }
301
301
  if ((channelBuildWorkflow.match(/uses: \.\/\.github\/workflows\/\.build\.yml/g) || []).length !== 1) {
302
302
  throw new Error("public build facade must invoke its exact backbone once");
@@ -5,6 +5,7 @@ import YAML from "yaml";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { checkSelfConsumerContract } from "./check-self-consumer-contract.mjs";
8
+ import { compatibilityWorkflowTarget } from "../packages/core/consumer/compatibility-workflows.js";
8
9
 
9
10
  const DEFAULT_ROOT = path.resolve(
10
11
  path.dirname(fileURLToPath(import.meta.url)),
@@ -188,6 +189,7 @@ function assertWorkflowInventory(root) {
188
189
  if (!entry.isFile() || !/\.ya?ml$/u.test(entry.name)) continue;
189
190
  let relative = `.github/workflows/${entry.name}`;
190
191
  const text = read(root, relative);
192
+ relative = compatibilityWorkflowTarget(root, relative, text);
191
193
  if (
192
194
  relative !== REUSABLE_PATH &&
193
195
  (text.includes(
@@ -5,6 +5,7 @@ import { assertPipelinePublicationTopology } from "./check-pipeline-publication-
5
5
  import { inspectWorkflowJob, readWorkflow } from "./workflow-action-graph.mjs";
6
6
  import crypto from "node:crypto";
7
7
  import fs from "node:fs";
8
+ import { readConsumerUpgrade, compatibilityWorkflowTarget } from "../packages/core/consumer/compatibility-workflows.js";
8
9
  import ts from "typescript";
9
10
 
10
11
  import path from "node:path";
@@ -406,7 +407,10 @@ export function findUnknownReleaseTopology(
406
407
  }
407
408
 
408
409
  function assertClosedWorld(workflowPaths) {
409
- const unknown = findUnknownReleaseTopology(workflowPaths);
410
+ const retained = (readConsumerUpgrade(root)?.entries || []).filter(entry =>
411
+ workflowPaths.includes(compatibilityWorkflowTarget(root, entry.path, read(entry.path)))
412
+ ).map(entry => entry.path);
413
+ const unknown = findUnknownReleaseTopology([...workflowPaths, ...retained]);
410
414
  assert.deepEqual(
411
415
  unknown,
412
416
  [],
@@ -4,7 +4,8 @@ import { fileURLToPath } from "node:url";
4
4
  import { parse as yaml } from "yaml";
5
5
  import * as consumerPlan from "../packages/core/consumer/contract/plan.js";
6
6
  import { consumerWorkflows } from "../packages/core/consumer/contract/entries.js";
7
- import { workflowPath } from "../packages/core/workflow/workflow-taxonomy.mjs";
7
+ import { checkWorkflowTaxonomy, workflowPath } from "../packages/core/workflow/workflow-taxonomy.mjs";
8
+ import { readConsumerUpgrade } from "../packages/core/consumer/compatibility-workflows.js";
8
9
 
9
10
  export const SELF_ENTRY_CHANNEL = "v4";
10
11
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -33,6 +34,9 @@ export function checkSelfConsumerContract(root = ROOT) {
33
34
  if (read(file) !== bytes) fail(`${file}: generated caller drift`);
34
35
  const taxonomy = JSON.parse(read("architecture/workflow-taxonomy.json"));
35
36
  const registered = new Set(taxonomy.entries.map(workflowPath));
37
+ const inventory = checkWorkflowTaxonomy(root, { integration: false, documentation: false });
38
+ if (!inventory.ok) fail(inventory.errors.join("; "));
39
+ for (const entry of readConsumerUpgrade(root)?.entries || []) registered.add(entry.path);
36
40
  const observed = fs
37
41
  .readdirSync(path.join(root, ".github/workflows"))
38
42
  .filter((file) => /\.ya?ml$/u.test(file))
@@ -3,6 +3,7 @@ import { format } from "prettier";
3
3
  import path from "node:path";
4
4
  import crypto from "node:crypto";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { readConsumerUpgrade } from "../packages/core/consumer/compatibility-workflows.js";
6
7
 
7
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const contract = JSON.parse(
@@ -16,6 +17,25 @@ const target = path.join(
16
17
  "architecture/universal-workflow-capability-policy.json",
17
18
  );
18
19
  const policy = JSON.parse(fs.readFileSync(target, "utf8"));
20
+ for (const entry of readConsumerUpgrade(root)?.entries || []) {
21
+ if (!entry.interface.workflow_call) continue;
22
+ for (const field of [
23
+ "inventoryWorkflows",
24
+ "directCapabilityWorkflows",
25
+ "bootstrapGovernedWorkflows",
26
+ ]) {
27
+ if (
28
+ contract[field].includes(entry.target) &&
29
+ !contract[field].includes(entry.path)
30
+ )
31
+ contract[field].push(entry.path);
32
+ contract[field].sort();
33
+ }
34
+ }
35
+ fs.writeFileSync(
36
+ path.join(root, "architecture/universal-workflow-bootstrap.json"),
37
+ await format(JSON.stringify(contract), { parser: "json" }),
38
+ );
19
39
  policy.contractRoots = contract.bootstrap.admissionPolicySources
20
40
  .map((file) => {
21
41
  if (path.isAbsolute(file) || file.split("/").includes(".."))
@@ -2,6 +2,7 @@
2
2
  import { actionSurfaceAudit } from "./site-action-audit.mjs";
3
3
  import { actionInventory } from "../packages/core/contracts/action-inventory.js";
4
4
  import { readWorkflowTaxonomy, workflowPath } from "../packages/core/workflow/workflow-taxonomy.mjs";
5
+ import { readConsumerUpgrade } from "../packages/core/consumer/compatibility-workflows.js";
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
7
8
  import crypto from "node:crypto";
@@ -830,12 +831,14 @@ function buildSiteBundle() {
830
831
  };
831
832
  const nodeApiRegistry = createSiteNodeApiRegistry({ root, packageJson, nodeApiMeta, sha256File, publicSurfaceLifecycle });
832
833
  const taxonomy = readWorkflowTaxonomy(root);
834
+ const retainedEntries = readConsumerUpgrade(root)?.entries || [];
833
835
  const workflowRegistry = {
834
836
  schemaVersion: 1,
835
837
  contract: "kungfu-buildchain-workflow-registry",
836
838
  workflowSource: ".github/workflows reverse input enumeration",
837
839
  workflows: enumerateWorkflowInputs({ root }).map((entry) => {
838
- const classification = taxonomy?.entries.find((item) => workflowPath(item) === entry.path || item.compatibility?.path === entry.path);
840
+ const retained = retainedEntries.find(item => item.path === entry.path);
841
+ const classification = taxonomy?.entries.find((item) => workflowPath(item) === (retained?.target || entry.path));
839
842
  const semanticId = classification?.id || entry.id;
840
843
  const surfaceById = new Map([
841
844
  ["build", "channel-build-router"],
@@ -877,14 +880,10 @@ function buildSiteBundle() {
877
880
  [".auditable-demo", "preview"],
878
881
  [".declarative-auditable-demo", "preview"],
879
882
  ["release-propagation", "preview"],
880
- ["candidate-lab", "repository-internal"],
881
- ["build-surface-fixture", "repository-internal"],
882
- ["buildchain-stable-candidate-qualification", "repository-internal"],
883
883
  ["engineering-housekeeper", "preview"],
884
884
  ["engineering-housekeeper-daily", "preview"],
885
885
  ["engineering-housekeeper-weekly", "preview"],
886
886
  ["engineering-housekeeper-monthly", "preview"],
887
- ["self-hosted-runner-smoke", "compatibility-fixture"],
888
887
  ]);
889
888
  const engineeringHousekeeper = semanticId.startsWith("engineering-housekeeper");
890
889
  if (classification?.role === "component") statusById.set(semanticId, "repository-internal");
@@ -905,6 +904,7 @@ function buildSiteBundle() {
905
904
  ? "One reusable policy and evidence boundary owns Engineering Housekeeper execution; scheduled callers contain cadence values only."
906
905
  : "Canonical workflow owns one declared API or orchestration responsibility.",
907
906
  }),
907
+ ...(retained ? { compatibilityPromise: "retained-consumer-contract", status: "compatibility", apiRole: "compatibility", nonDuplicationRationale: "Generated from the registered canonical implementation; retained published inputs, outputs, defaults, permissions and job contexts are verified before release." } : {}),
908
908
  };
909
909
  }),
910
910
  actionSource: "actions/*/*/*/action.yml reverse input enumeration",
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import {
5
+ compatibilityWorkflowEntries,
6
+ renderCompatibilityWorkflow,
7
+ } from "../packages/core/consumer/compatibility-workflows.js";
4
8
  import {
5
9
  readWorkflowTaxonomy,
6
10
  renderWorkflowCatalog,
@@ -19,6 +23,19 @@ for (const entry of policy.entries) {
19
23
  fs.readFileSync(path.join(root, workflowPath(entry)), "utf8"),
20
24
  );
21
25
  }
26
+ for (const entry of compatibilityWorkflowEntries(
27
+ root,
28
+ undefined,
29
+ policy.entries.map((entry) => ({ ...entry, path: workflowPath(entry) })),
30
+ )) {
31
+ fs.writeFileSync(
32
+ path.join(root, entry.path),
33
+ renderCompatibilityWorkflow(
34
+ entry,
35
+ fs.readFileSync(path.join(root, entry.target), "utf8"),
36
+ ),
37
+ );
38
+ }
22
39
  fs.writeFileSync(path.join(root, TAXONOMY_DOC), renderWorkflowCatalog(policy));
23
40
  process.stdout.write(
24
41
  `Generated ${policy.entries.length} canonical workflow declarations.\n`,
@@ -5,6 +5,10 @@ import path from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import ts from "typescript";
8
+ import {
9
+ CONSUMER_UPGRADE_PATH,
10
+ renderCompatibilityWorkflow,
11
+ } from "../packages/core/consumer/compatibility-workflows.js";
8
12
  import {
9
13
  dependencyCycles,
10
14
  relativeImports,
@@ -292,7 +296,29 @@ function collectMaintainabilityMetrics({
292
296
  path: file,
293
297
  text: readTrackedFile(root, file, revision),
294
298
  }));
295
- const logicalWorkflows = physicalWorkflows;
299
+ const retained = files.includes(CONSUMER_UPGRADE_PATH)
300
+ ? JSON.parse(readTrackedFile(root, CONSUMER_UPGRADE_PATH, revision)).entries
301
+ : [];
302
+ const generatedWorkflows = new Set(
303
+ retained
304
+ .filter((entry) => {
305
+ const actual = physicalWorkflows.find(
306
+ (file) => file.path === entry.path,
307
+ );
308
+ const canonical = physicalWorkflows.find(
309
+ (file) => file.path === entry.target,
310
+ );
311
+ return (
312
+ actual &&
313
+ canonical &&
314
+ actual.text === renderCompatibilityWorkflow(entry, canonical.text)
315
+ );
316
+ })
317
+ .map((entry) => entry.path),
318
+ );
319
+ const logicalWorkflows = physicalWorkflows.filter(
320
+ (entry) => !generatedWorkflows.has(entry.path),
321
+ );
296
322
  const workflowMetrics = Object.fromEntries(
297
323
  logicalWorkflows.map((entry) => [
298
324
  entry.path,
@@ -319,7 +345,7 @@ function collectMaintainabilityMetrics({
319
345
  (total, entry) => total + entry.lines,
320
346
  0,
321
347
  );
322
- const workflowLines = sumLines(workflowFiles);
348
+ const workflowLines = sumLines(logicalWorkflows.map((entry) => entry.path));
323
349
  const actionDefinitionLines = sumLines(actionDefinitions);
324
350
  const head = revision || gitText(root, ["rev-parse", "HEAD"]).trim();
325
351
  return {
@@ -334,6 +360,8 @@ function collectMaintainabilityMetrics({
334
360
  testLines: sumLines(testFiles),
335
361
  workflowFiles: logicalWorkflows.length,
336
362
  workflowLines,
363
+ generatedCompatibilityWorkflowFiles: generatedWorkflows.size,
364
+ generatedCompatibilityWorkflowLines: sumLines([...generatedWorkflows]),
337
365
  actionDefinitions: actionDefinitions.length,
338
366
  actionDefinitionLines,
339
367
  automationImplementationLines:
@@ -1,14 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { readConsumerUpgrade } from "../packages/core/consumer/compatibility-workflows.js";
3
4
 
4
5
  function readJson(root, file) {
5
6
  return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
6
7
  }
7
8
 
8
- // Public API ownership follows the current architecture. Retired release
9
- // contracts do not require aliases or forwarding implementations in 4.1.
9
+ // Current APIs and explicitly retained published contracts have distinct owners.
10
10
  export function evaluatePublicSurface({ root, policy }) {
11
11
  const issues = [];
12
+ const retained = new Set((readConsumerUpgrade(root)?.entries || []).map(entry => entry.path));
12
13
  const groups = new Set(readJson(root, "dist/site/capability-registry.json").groups.map(entry => entry.id));
13
14
  for (const [file, collection, kind, key] of [
14
15
  ["dist/site/cli-registry.json", "commands", "cli", "id"],
@@ -26,7 +27,8 @@ export function evaluatePublicSurface({ root, policy }) {
26
27
  }
27
28
  if (!groups.has(entry.capabilityGroup)) issues.push(`${label}: capability group ${entry.capabilityGroup || "<empty>"} is not registered`);
28
29
  if (!entry.nonDuplicationRationale?.trim()) issues.push(`${label}: public surface requires a non-duplication rationale`);
29
- if (entry.compatibilityPromise !== "current-contract-only") issues.push(`${label}: historical compatibility promise is not part of the current architecture`);
30
+ const promise = kind === "workflow" && retained.has(entry.path) ? "retained-consumer-contract" : "current-contract-only";
31
+ if (entry.compatibilityPromise !== promise) issues.push(`${label}: compatibility promise does not match the registered contract`);
30
32
  if (kind === "action" && !["public", "implementation"].includes(entry.apiRole)) issues.push(`${label}: action API role is missing`);
31
33
  const target = kind === "node" ? entry.target : ["workflow", "action"].includes(kind) ? entry.path : null;
32
34
  if (target) {