@kungfu-tech/buildchain 3.0.6 → 3.0.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 (64) hide show
  1. package/README.md +4 -2
  2. package/actions/release-tail/README.md +17 -0
  3. package/bin/buildchain.mjs +11 -0
  4. package/bin/internal/command-registry.mjs +2 -0
  5. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  6. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  7. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  8. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  9. package/dist/site/agent-index.json +3 -0
  10. package/dist/site/artifact-schemas.json +6 -0
  11. package/dist/site/buildchain-contract.json +35 -20
  12. package/dist/site/buildchain-site.json +307 -17
  13. package/dist/site/capability-registry.json +12 -9
  14. package/dist/site/cli-registry.json +102 -0
  15. package/dist/site/controller-registry.json +14 -2
  16. package/dist/site/kfd-claims.json +247 -16
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +48 -3
  19. package/dist/site/node-api-registry.json +3108 -1409
  20. package/dist/site/page-registry.json +275 -9
  21. package/dist/site/public-surface-audit.json +304 -15
  22. package/dist/site/publication-authority-registry.json +26 -1
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +5 -0
  25. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  26. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  27. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  28. package/dist/site/site-manifest.json +31 -7
  29. package/dist/site/workflow-registry.json +86 -9
  30. package/docs/MAP.md +5 -2
  31. package/docs/cli-reference.md +136 -0
  32. package/docs/node-api-reference.md +233 -84
  33. package/docs/publication-rehearsal.md +94 -0
  34. package/docs/release-tail-contract.md +160 -0
  35. package/docs/release-tail-provider-plane.md +120 -0
  36. package/package.json +8 -3
  37. package/packages/core/buildchain-agent-manuals.js +3 -0
  38. package/packages/core/buildchain-kfd-claims.js +1 -1
  39. package/packages/core/buildchain-publication-authority.js +1 -0
  40. package/packages/core/index.js +39 -0
  41. package/packages/core/paper-agent-entry.js +11 -5
  42. package/packages/core/paper-repository.js +1 -0
  43. package/packages/core/paper-scaffold-content.js +21 -0
  44. package/packages/core/paper.js +28 -2
  45. package/packages/core/publication-rehearsal-projection.js +173 -0
  46. package/packages/core/publication-rehearsal-runtime.js +909 -0
  47. package/packages/core/release-tail-compatibility.js +60 -0
  48. package/packages/core/release-tail-provider-adapters.js +461 -0
  49. package/packages/core/release-tail-provider-plane.js +1228 -0
  50. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  51. package/scripts/assemble-self-publication-admission.mjs +1 -1
  52. package/scripts/buildchain-cli-help.mjs +13 -0
  53. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  54. package/scripts/check-inventory.mjs +8 -8
  55. package/scripts/check-maintainability.mjs +9 -2
  56. package/scripts/check-release-tail-contract.mjs +435 -0
  57. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  58. package/scripts/generate-site-bundle.mjs +24 -0
  59. package/scripts/init-repo.mjs +26 -2
  60. package/scripts/materialize-self-release-candidate-version.mjs +131 -0
  61. package/scripts/release-tail.mjs +159 -0
  62. package/scripts/site-capability-metadata.mjs +11 -0
  63. package/scripts/v4-architecture.mjs +600 -0
  64. package/scripts/workflow-call-contract.mjs +184 -5
@@ -139,7 +139,7 @@ async function main() {
139
139
  sourceSha,
140
140
  runtimeSha,
141
141
  contractDigest: controllerReceipt.runtime?.contractDigest,
142
- policyDigest: gateAggregate.policyDigest,
142
+ policyDigest: gateBindings.policyDigest,
143
143
  gateRegistryDigest: gateBindings.registryDigest,
144
144
  controllerReceiptDigest: controllerReceipt.digest,
145
145
  runnerProvenanceDigest: runnerProvenance.receiptDigest,
@@ -159,7 +159,7 @@ async function main() {
159
159
  sourceSha,
160
160
  runtimeSha,
161
161
  contractDigest: controllerReceipt.runtime?.contractDigest,
162
- policyDigest: gateAggregate.policyDigest,
162
+ policyDigest: gateBindings.policyDigest,
163
163
  gateRegistryDigest: gateBindings.registryDigest,
164
164
  controllerReceiptDigest: controllerReceipt.digest,
165
165
  runnerProvenanceDigest: runnerProvenance.receiptDigest,
@@ -2,6 +2,12 @@ export const BUILDCHAIN_USAGE = `Usage:
2
2
  buildchain --help
3
3
  buildchain version
4
4
  buildchain layout [--cwd <dir>] [--json]
5
+ buildchain architecture validate [--cwd <dir>] [--json]
6
+ buildchain architecture list [--cwd <dir>] [--json]
7
+ buildchain architecture show <capability-id> [--cwd <dir>] [--json]
8
+ buildchain architecture qualify --authority-revision <git-revision>
9
+ [--candidate-revision <git-revision>]
10
+ [--cwd <dir>] [--json]
5
11
  buildchain portable-cache plan --manifest <file-or-json> [--output <file>]
6
12
  [--github-output <file>] [--json]
7
13
  buildchain portable-cache receipt --plan <file-or-json> [--matched-key <key>]
@@ -33,6 +39,13 @@ export const BUILDCHAIN_USAGE = `Usage:
33
39
  buildchain release-governance reconcile --repository <owner/repo>
34
40
  --branch <dev|alpha|release/vN/vN.N>
35
41
  --candidate-sha <sha> [--apply] [--json]
42
+ buildchain release-tail plan --declaration <json-or-path> [--output <path>]
43
+ buildchain release-tail init --declaration <json-or-path> [--state <path>]
44
+ buildchain release-tail <status|verify> [--state <path>] [--output <path>]
45
+ buildchain release-tail compat --hooks-json <json-or-path> [--output <path>]
46
+ buildchain release-tail rehearse --capsule <path> --capsule-root <absolute-path>
47
+ --mode <simulate|replay> --state <path>
48
+ --evidence <path> [--environment-json <json-or-path>]
36
49
  buildchain github-governance <plan|apply|rollback|protection-policy-plan|ruleset-policy-plan> ...
37
50
  buildchain release <inspect|recover|finalize|abort> ...
38
51
  buildchain transaction inspect ...
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const REQUIRED = [
8
+ "owner",
9
+ "writers",
10
+ "schemas",
11
+ "stores",
12
+ "states",
13
+ "events",
14
+ "transitions",
15
+ "recovery",
16
+ "effects",
17
+ "retry",
18
+ "invalidation",
19
+ "providerReadback",
20
+ "publicSurfaces",
21
+ "compatibilityCallers",
22
+ "sourcePaths",
23
+ "testPaths",
24
+ "migrationDisposition",
25
+ "unresolved",
26
+ ];
27
+
28
+ function loadJson(root, file) {
29
+ return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
30
+ }
31
+
32
+ function registrySurfaceIds(root) {
33
+ const cli = loadJson(root, "dist/site/cli-registry.json").commands.map(
34
+ (entry) => `cli:${entry.id}`,
35
+ );
36
+ const workflows = loadJson(root, "dist/site/workflow-registry.json");
37
+ const workflowIds = workflows.workflows.map(
38
+ (entry) => `workflow:${entry.id}`,
39
+ );
40
+ const actionIds = workflows.actions.map((entry) => `action:${entry.id}`);
41
+ const exports = Object.keys(loadJson(root, "package.json").exports).map(
42
+ (entry) => `export:${entry}`,
43
+ );
44
+ return new Set([...cli, ...workflowIds, ...actionIds, ...exports]);
45
+ }
46
+
47
+ function listFiles(root, relativeRoot) {
48
+ const start = path.join(root, relativeRoot);
49
+ if (!fs.existsSync(start) || !fs.statSync(start).isDirectory()) return [];
50
+ const files = [];
51
+ const visit = (directory, prefix = "") => {
52
+ for (const entry of fs
53
+ .readdirSync(directory, { withFileTypes: true })
54
+ .sort((left, right) => left.name.localeCompare(right.name))) {
55
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
56
+ if (entry.isDirectory())
57
+ visit(path.join(directory, entry.name), relative);
58
+ else if (entry.isFile()) files.push(relative);
59
+ }
60
+ };
61
+ visit(start);
62
+ return files;
63
+ }
64
+
65
+ function validateRequiredDimensions(mechanism, issues) {
66
+ for (const field of REQUIRED) {
67
+ const value = mechanism[field];
68
+ const empty = ["owner", "migrationDisposition"].includes(field)
69
+ ? !String(value || "").trim()
70
+ : !Array.isArray(value) || value.length === 0;
71
+ if (empty) issues.push(`${mechanism.id}: ${field} is empty`);
72
+ }
73
+ }
74
+
75
+ function validateCoordinates(root, mechanism, issues) {
76
+ const writers = (mechanism.writers || []).filter((entry) =>
77
+ /^(?:actions|bin|packages|scripts)\//u.test(entry),
78
+ );
79
+ for (const file of [
80
+ ...(mechanism.sourcePaths || []),
81
+ ...(mechanism.testPaths || []),
82
+ ...writers,
83
+ ]) {
84
+ if (!fs.existsSync(path.join(root, file)))
85
+ issues.push(`${mechanism.id}: coordinate is missing: ${file}`);
86
+ }
87
+ }
88
+
89
+ function validateOwnership(mechanism, ownedSources, issues) {
90
+ for (const file of mechanism.sourcePaths || []) {
91
+ const prior = ownedSources.get(file);
92
+ if (prior)
93
+ issues.push(
94
+ `${file}: ambiguous mechanism ownership (${prior}, ${mechanism.id})`,
95
+ );
96
+ ownedSources.set(file, mechanism.id);
97
+ }
98
+ }
99
+
100
+ function validateSurfaces(mechanism, surfaces, issues) {
101
+ for (const surface of mechanism.publicSurfaces || []) {
102
+ if (!surfaces.has(surface))
103
+ issues.push(
104
+ `${mechanism.id}: public surface is absent from generated/package registries: ${surface}`,
105
+ );
106
+ }
107
+ }
108
+
109
+ function validateGitRefStore(root, mechanism, issues) {
110
+ const source = (mechanism.sourcePaths || [])
111
+ .map((file) => fs.readFileSync(path.join(root, file), "utf8"))
112
+ .join("\n");
113
+ const writesReleaseState = /refs\/heads\/buildchain\/release-state/u.test(
114
+ source,
115
+ );
116
+ const declaresGitRef = (mechanism.stores || []).some((entry) =>
117
+ /Git ref|refs\//u.test(entry),
118
+ );
119
+ if (writesReleaseState && !declaresGitRef)
120
+ issues.push(
121
+ `${mechanism.id}: Git-ref writer lacks an explicit store disposition`,
122
+ );
123
+ }
124
+
125
+ function validateReverseScanReferences(root, reverseScan, issues) {
126
+ const references = [
127
+ ...(reverseScan.registries || []),
128
+ ...(reverseScan.generatedReferences || []),
129
+ reverseScan.commandRegistry,
130
+ String(reverseScan.packageExports || "").split("#")[0],
131
+ ].filter(Boolean);
132
+ for (const file of references) {
133
+ if (!fs.existsSync(path.join(root, file)))
134
+ issues.push(`reverse scan reference is missing: ${file}`);
135
+ }
136
+ }
137
+
138
+ function validateSurfaceKinds(inventory, issues) {
139
+ const surfaceKinds = new Set(
140
+ (inventory.mechanisms || []).flatMap((mechanism) =>
141
+ (mechanism.publicSurfaces || []).map((surface) => surface.split(":")[0]),
142
+ ),
143
+ );
144
+ for (const kind of ["action", "cli", "export", "workflow"]) {
145
+ if (!surfaceKinds.has(kind))
146
+ issues.push(`reverse scan has no ${kind} public surface coverage`);
147
+ }
148
+ return surfaceKinds;
149
+ }
150
+
151
+ function discoverAuthoritySources(root, reverseScan, ids, issues) {
152
+ const discovered = new Map();
153
+ for (const [index, rule] of (
154
+ reverseScan.authoritySourceRules || []
155
+ ).entries()) {
156
+ if (!ids.has(rule?.mechanismId)) {
157
+ issues.push(
158
+ `authority source rule ${index} names an unknown mechanism: ${rule?.mechanismId || "<empty>"}`,
159
+ );
160
+ continue;
161
+ }
162
+ if (!rule.root || !rule.pattern) {
163
+ issues.push(`authority source rule ${index} is incomplete`);
164
+ continue;
165
+ }
166
+ let pattern;
167
+ try {
168
+ pattern = new RegExp(rule.pattern, "u");
169
+ } catch (error) {
170
+ issues.push(
171
+ `authority source rule ${index} has an invalid pattern: ${error.message}`,
172
+ );
173
+ continue;
174
+ }
175
+ const matches = listFiles(root, rule.root).filter((file) =>
176
+ pattern.test(file),
177
+ );
178
+ if (matches.length === 0)
179
+ issues.push(
180
+ `${rule.mechanismId}: authority source rule matched no files under ${rule.root}`,
181
+ );
182
+ for (const relative of matches) {
183
+ const file = path.posix.join(rule.root, relative);
184
+ const mechanismIds = discovered.get(file) || new Set();
185
+ mechanismIds.add(rule.mechanismId);
186
+ discovered.set(file, mechanismIds);
187
+ }
188
+ }
189
+ return discovered;
190
+ }
191
+
192
+ function validateAuthorityOwnership(discovered, ownedSources, issues) {
193
+ for (const [file, mechanismIds] of discovered) {
194
+ if (mechanismIds.size !== 1) {
195
+ issues.push(
196
+ `${file}: ambiguous reverse-scan ownership (${[...mechanismIds].join(", ")})`,
197
+ );
198
+ continue;
199
+ }
200
+ const expected = [...mechanismIds][0];
201
+ const owner = ownedSources.get(file);
202
+ if (!owner)
203
+ issues.push(`${file}: orphan authority coordinate for ${expected}`);
204
+ else if (owner !== expected)
205
+ issues.push(
206
+ `${file}: authority coordinate is owned by ${owner}, expected ${expected}`,
207
+ );
208
+ }
209
+ }
210
+
211
+ function validateGitRefStoreScan(
212
+ root,
213
+ inventory,
214
+ reverseScan,
215
+ ownedSources,
216
+ issues,
217
+ ) {
218
+ let gitRefStores = 0;
219
+ for (const [index, store] of (reverseScan.gitRefStores || []).entries()) {
220
+ const mechanism = (inventory.mechanisms || []).find(
221
+ (entry) => entry.id === store?.mechanismId,
222
+ );
223
+ if (!mechanism || !store.sourcePath || !store.marker) {
224
+ issues.push(`Git-ref store scan ${index} is incomplete`);
225
+ continue;
226
+ }
227
+ const file = path.join(root, store.sourcePath);
228
+ if (!fs.existsSync(file)) {
229
+ issues.push(
230
+ `${store.mechanismId}: Git-ref store source is missing: ${store.sourcePath}`,
231
+ );
232
+ continue;
233
+ }
234
+ if (ownedSources.get(store.sourcePath) !== store.mechanismId)
235
+ issues.push(
236
+ `${store.sourcePath}: Git-ref store source is not owned by ${store.mechanismId}`,
237
+ );
238
+ if (!fs.readFileSync(file, "utf8").includes(store.marker))
239
+ issues.push(
240
+ `${store.sourcePath}: Git-ref store marker is missing: ${store.marker}`,
241
+ );
242
+ if (
243
+ !(mechanism.stores || []).some((entry) =>
244
+ /Git ref|refs\/|ledger-ref|state ref/iu.test(entry),
245
+ )
246
+ )
247
+ issues.push(
248
+ `${store.mechanismId}: Git-ref store lacks an explicit disposition`,
249
+ );
250
+ gitRefStores += 1;
251
+ }
252
+ return gitRefStores;
253
+ }
254
+
255
+ function validateReverseScan(root, inventory, ids, ownedSources, issues) {
256
+ const reverseScan = inventory.reverseScan || {};
257
+ validateReverseScanReferences(root, reverseScan, issues);
258
+ const surfaceKinds = validateSurfaceKinds(inventory, issues);
259
+ const discovered = discoverAuthoritySources(root, reverseScan, ids, issues);
260
+ validateAuthorityOwnership(discovered, ownedSources, issues);
261
+ const gitRefStores = validateGitRefStoreScan(
262
+ root,
263
+ inventory,
264
+ reverseScan,
265
+ ownedSources,
266
+ issues,
267
+ );
268
+ return {
269
+ authorityCoordinates: discovered.size,
270
+ gitRefStores,
271
+ surfaceKinds: [...surfaceKinds].sort(),
272
+ };
273
+ }
274
+
275
+ function checkCoreMechanismInventory({
276
+ root = process.cwd(),
277
+ inventory = loadJson(root, "architecture/v3-core-mechanism-inventory.json"),
278
+ } = {}) {
279
+ const issues = [];
280
+ if (
281
+ inventory.schemaVersion !== 1 ||
282
+ inventory.contract !== "kungfu-buildchain-v3-core-mechanism-inventory"
283
+ )
284
+ issues.push("inventory contract or schemaVersion is invalid");
285
+ if (
286
+ !/^[0-9a-f]{40}$/u.test(inventory.baseline?.commit || "") ||
287
+ !/^[0-9a-f]{40}$/u.test(inventory.baseline?.tree || "")
288
+ )
289
+ issues.push("baseline must bind an exact commit and tree");
290
+ if (inventory.maintainability?.dependencyCycles !== 0)
291
+ issues.push("baseline must report the exact zero-cycle result");
292
+ const surfaces = registrySurfaceIds(root);
293
+ const ids = new Set();
294
+ const ownedSources = new Map();
295
+ for (const mechanism of inventory.mechanisms || []) {
296
+ if (!mechanism.id || ids.has(mechanism.id))
297
+ issues.push(
298
+ `mechanism id is missing or duplicated: ${mechanism.id || "<empty>"}`,
299
+ );
300
+ ids.add(mechanism.id);
301
+ validateRequiredDimensions(mechanism, issues);
302
+ validateCoordinates(root, mechanism, issues);
303
+ validateOwnership(mechanism, ownedSources, issues);
304
+ validateSurfaces(mechanism, surfaces, issues);
305
+ validateGitRefStore(root, mechanism, issues);
306
+ }
307
+ const reverseScan = validateReverseScan(
308
+ root,
309
+ inventory,
310
+ ids,
311
+ ownedSources,
312
+ issues,
313
+ );
314
+ if ((inventory.mechanisms || []).length < 10)
315
+ issues.push("inventory must retain all ten v3 core mechanism families");
316
+ if (issues.length)
317
+ throw new Error(
318
+ `v3 core mechanism inventory check failed:\n- ${issues.join("\n- ")}`,
319
+ );
320
+ return {
321
+ mechanisms: inventory.mechanisms.length,
322
+ sourceCoordinates: ownedSources.size,
323
+ publicSurfaces: inventory.mechanisms.reduce(
324
+ (sum, entry) => sum + entry.publicSurfaces.length,
325
+ 0,
326
+ ),
327
+ dependencyCycles: inventory.maintainability.dependencyCycles,
328
+ ...reverseScan,
329
+ };
330
+ }
331
+
332
+ if (
333
+ process.argv[1] &&
334
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
335
+ ) {
336
+ try {
337
+ const report = checkCoreMechanismInventory();
338
+ console.log(
339
+ `v3 core mechanism inventory check passed: ${report.mechanisms} mechanisms, ${report.sourceCoordinates} owned source coordinates, ${report.authorityCoordinates} reverse-discovered authority coordinates, ${report.publicSurfaces} public surfaces across ${report.surfaceKinds.length} registry kinds, ${report.gitRefStores} Git-ref stores, ${report.dependencyCycles} dependency cycles`,
340
+ );
341
+ } catch (error) {
342
+ console.error(error instanceof Error ? error.message : String(error));
343
+ process.exitCode = 1;
344
+ }
345
+ }
346
+
347
+ export { checkCoreMechanismInventory };
@@ -283,7 +283,7 @@ for (const requiredSnippet of [
283
283
  "authorize-promotion-runtime-override.cjs",
284
284
  "BUILDCHAIN_ROUTER_REPOSITORY: ${{ inputs.buildchain-repository }}",
285
285
  "BUILDCHAIN_RESUME_RUNTIME_SHA: ${{ inputs.resume-buildchain-runtime-sha }}",
286
- "git ls-remote",
286
+ "if [[ \"${ref}\" =~ ^[0-9A-Fa-f]{40}$ ]]; then", "sha=\"${ref,,}\"", "git ls-remote",
287
287
  "Recovery router ref does not match resume-buildchain-runtime-sha",
288
288
  ]) {
289
289
  if (!channelPromotionWorkflow.includes(requiredSnippet)) {
@@ -730,8 +730,8 @@ const registeredActionIds = (workflowRegistry.actions || []).map((entry) => entr
730
730
  const readmeActionIndex = fs.readFileSync(path.join(root, "README.md"), "utf8");
731
731
  const mapActionIndex = fs.readFileSync(path.join(root, "docs/MAP.md"), "utf8");
732
732
  const retrospectiveActionIndex = fs.readFileSync(path.join(root, ".github/retrospectives/2026-07-10-buildchain-consolidation.md"), "utf8");
733
- if (registeredActionIds.length !== 6) {
734
- throw new Error(`workflow-registry.json must expose the six current action entries, got ${registeredActionIds.length}`);
733
+ if (registeredActionIds.length !== 7) {
734
+ throw new Error(`workflow-registry.json must expose the seven current action entries, got ${registeredActionIds.length}`);
735
735
  }
736
736
  for (const actionId of registeredActionIds) {
737
737
  if (!readmeActionIndex.includes(`actions/${actionId}`) || !mapActionIndex.includes(`actions/${actionId}`)) {
@@ -992,7 +992,7 @@ for (const forbiddenSnippet of [
992
992
  for (const requiredSnippet of [
993
993
  "id-token: write",
994
994
  "actions: write",
995
- "uses: ./.github/workflows/.release-candidate-promote.yml",
995
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@9a0cdf8d84aacf8c7daaac82efa43d1b34696a03\n",
996
996
  "github.event.workflow_run.event == 'push'",
997
997
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
998
998
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
@@ -1114,7 +1114,7 @@ for (const retiredWorkflow of [
1114
1114
  }
1115
1115
  }
1116
1116
  const promoteBuildchainRefAction = fs.readFileSync(path.join(root, "actions/promote-buildchain-ref/action.yml"), "utf8");
1117
- const promoteBuildchainRefIndex = fs.readFileSync(path.join(root, "actions/promote-buildchain-ref/index.js"), "utf8");
1117
+ const promoteBuildchainRefIndex = ["actions/promote-buildchain-ref/index.js", "actions/promote-buildchain-ref/github-release.js"].map((entry) => fs.readFileSync(path.join(root, entry), "utf8")).join("\n");
1118
1118
  for (const requiredSnippet of [
1119
1119
  "github-release:",
1120
1120
  "github-release-title:",
@@ -1140,8 +1140,8 @@ for (const requiredSnippet of [
1140
1140
  }
1141
1141
  }
1142
1142
  for (const forbiddenSnippet of [
1143
- "run: node scripts/release-candidate-resolver.mjs",
1144
- "uses: ./actions/promote-buildchain-ref",
1143
+ "uses: ./.github/workflows/.release-candidate-promote.yml",
1144
+ "uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@",
1145
1145
  ]) {
1146
1146
  if (buildchainRefPromotionWorkflow.includes(forbiddenSnippet)) {
1147
1147
  throw new Error(`buildchain ref promotion workflow must use the declarative wrapper, found manual snippet: ${forbiddenSnippet}`);
@@ -1284,7 +1284,7 @@ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-rele
1284
1284
  throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
1285
1285
  }
1286
1286
 
1287
- for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "release-passport-check-manifest.json", "schemas/release-passport-v1.schema.json", "buildchain-contract.json"]) {
1287
+ for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "release-passport-check-manifest.json", "schemas/release-passport-v1.schema.json", "schemas/publication-rehearsal-capsule-v1.schema.json", "schemas/release-tail-capabilities-v1.schema.json", "schemas/release-tail-provider-bindings-v1.schema.json", "buildchain-contract.json"]) {
1288
1288
  if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
1289
1289
  throw new Error(`site bundle missing ${siteFile}`);
1290
1290
  }
@@ -282,9 +282,16 @@ function evaluateMaintainability({ current, baselineFiles, policy }) {
282
282
  for (const [file, metrics] of Object.entries(current.files)) {
283
283
  const baseline = baselineFiles[file];
284
284
  if (!baseline) {
285
- if (metrics.lines > budgets.newFileLines) {
285
+ const transition = policy.approvedNewFileTransitions?.[file];
286
+ if (transition && !String(transition.rationale || "").trim()) {
286
287
  issues.push(
287
- `${file}: new file has ${metrics.lines} lines; budget is ${budgets.newFileLines}`,
288
+ `${file}: approved new-file transition requires a rationale`,
289
+ );
290
+ }
291
+ const allowedFileLines = transition?.maxLines ?? budgets.newFileLines;
292
+ if (metrics.lines > allowedFileLines) {
293
+ issues.push(
294
+ `${file}: new file has ${metrics.lines} lines; budget is ${allowedFileLines}`,
288
295
  );
289
296
  }
290
297
  for (const entry of metrics.functions) {