@kungfu-tech/buildchain 4.0.2-alpha.2 → 4.0.2-alpha.21

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 (71) hide show
  1. package/architecture/agent-change-map.md +52 -1
  2. package/architecture/ci-lane-change-budget.json +349 -0
  3. package/architecture/internal-capabilities.json +44 -1
  4. package/architecture/maintainability-debt.json +53 -33
  5. package/architecture/maintainability-policy.json +20 -15
  6. package/architecture/v3-core-mechanism-inventory.json +1 -0
  7. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  8. package/architecture/v4-release-invocation-fixtures.json +20 -0
  9. package/architecture/v4-release-topology.json +160 -84
  10. package/architecture/v4-runtime-semantic-closure.json +4 -1
  11. package/architecture/v4-universal-workflow-bootstrap.json +171 -0
  12. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  13. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  14. package/contracts/buildchain-v2-residuals-v1.json +0 -54
  15. package/contracts/v4-release-invocation-v1.schema.json +50 -2
  16. package/dist/site/buildchain-contract.json +87 -42
  17. package/dist/site/buildchain-site.json +15 -10
  18. package/dist/site/capability-registry.json +1 -1
  19. package/dist/site/controller-registry.json +46 -10
  20. package/dist/site/kfd-claims.json +174 -196
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +2 -2
  23. package/dist/site/node-api-registry.json +95 -20
  24. package/dist/site/page-registry.json +9 -4
  25. package/dist/site/public-surface-audit.json +178 -198
  26. package/dist/site/publication-authority-registry.json +67 -57
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/site-manifest.json +6 -6
  29. package/dist/site/workflow-registry.json +209 -259
  30. package/docs/node-api-reference.md +25 -22
  31. package/docs/reusable-build-surface.md +16 -0
  32. package/package.json +2 -2
  33. package/packages/core/buildchain-publication-authority.js +3 -2
  34. package/packages/core/ci-lane-change-budget.js +14 -1
  35. package/packages/core/dev-delivery-candidate-identity.js +18 -0
  36. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  37. package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
  38. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  39. package/packages/core/dev-delivery-warrant-state.js +42 -26
  40. package/packages/core/dev-delivery-warrant.js +8 -0
  41. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  42. package/packages/core/release-tail-product-capabilities.js +29 -0
  43. package/packages/core/release-tail-provider-plane.js +3 -3
  44. package/packages/core/v4-canonical-contracts.js +6 -0
  45. package/packages/core/v4-product-publication.js +387 -0
  46. package/packages/core/v4-protected-publication-source.js +93 -0
  47. package/packages/core/v4-publication-qualification.js +1 -0
  48. package/packages/core/v4-release-invocation.js +72 -3
  49. package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
  50. package/scripts/check-inventory.mjs +13 -22
  51. package/scripts/check-maintainability.mjs +16 -113
  52. package/scripts/check-v4-release-topology.mjs +296 -40
  53. package/scripts/check-v4-universal-workflow-bootstrap.mjs +270 -0
  54. package/scripts/dev-delivery-warrant-store.mjs +73 -7
  55. package/scripts/dev-delivery-warrant-transition.mjs +109 -0
  56. package/scripts/dev-delivery-warrant.mjs +84 -100
  57. package/scripts/dev-pr-auto-merge.mjs +15 -15
  58. package/scripts/generate-channel-build-workflow.mjs +5 -1
  59. package/scripts/generate-channel-promotion-workflow.mjs +5 -3
  60. package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
  61. package/scripts/maintainability-public-surface.mjs +115 -0
  62. package/scripts/release-candidate-resolver.mjs +23 -24
  63. package/scripts/resume-from-candidate-run.mjs +16 -17
  64. package/scripts/universal-facade-maintainability.mjs +82 -0
  65. package/scripts/v4-product-publication-intent.mjs +114 -0
  66. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  67. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  68. package/scripts/v4-universal-workflow-engine.mjs +617 -0
  69. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  70. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  71. package/templates/universal-buildchain-bootstrap.yml +28 -0
@@ -19,6 +19,8 @@ import {
19
19
  evaluateTestBudgets,
20
20
  evaluateWorkflowBudgets,
21
21
  } from "./maintainability-governance.mjs";
22
+ import { governUniversalFacadeWorkflowMetrics } from "./universal-facade-maintainability.mjs";
23
+ import { evaluatePublicSurface } from "./maintainability-public-surface.mjs";
22
24
 
23
25
  function collectHotspots(
24
26
  root,
@@ -162,116 +164,6 @@ function selectedFunction(metrics, file, name) {
162
164
  return metrics.files[file]?.functions.find((entry) => entry.name === name);
163
165
  }
164
166
 
165
- function readJsonAtRevision(root, revision, file) {
166
- return JSON.parse(gitOutput(root, ["show", `${revision}:${file}`]));
167
- }
168
-
169
- function publicSurfaceContract(entry, kind) {
170
- if (kind === "cli") return { id: entry.id, usage: entry.usage };
171
- if (kind === "node")
172
- return {
173
- export: entry.export,
174
- specifier: entry.specifier,
175
- target: entry.target,
176
- };
177
- return {
178
- id: entry.id,
179
- path: entry.path,
180
- reusable: entry.reusable,
181
- inputs: entry.inputs || [],
182
- secrets: entry.secrets || [],
183
- outputs: entry.outputs || [],
184
- };
185
- }
186
-
187
- function evaluatePublicSurface({ root, revision, policy }) {
188
- const issues = [];
189
- const lifecycleFields = policy.publicSurfacePolicy.requiredLifecycleFields;
190
- const capabilityGroups = new Set(
191
- readJson(root, "dist/site/capability-registry.json").groups.map(
192
- (entry) => entry.id,
193
- ),
194
- );
195
- const definitions = [
196
- {
197
- file: "dist/site/cli-registry.json",
198
- collection: "commands",
199
- kind: "cli",
200
- key: "id",
201
- },
202
- {
203
- file: "dist/site/node-api-registry.json",
204
- collection: "exports",
205
- kind: "node",
206
- key: "export",
207
- },
208
- {
209
- file: "dist/site/workflow-registry.json",
210
- collection: "workflows",
211
- kind: "workflow",
212
- key: "id",
213
- },
214
- {
215
- file: "dist/site/workflow-registry.json",
216
- collection: "actions",
217
- kind: "action",
218
- key: "id",
219
- },
220
- ];
221
- for (const definition of definitions) {
222
- const current =
223
- readJson(root, definition.file)[definition.collection] || [];
224
- const baseline =
225
- readJsonAtRevision(root, revision, definition.file)[
226
- definition.collection
227
- ] || [];
228
- const baselineByKey = new Map(
229
- baseline.map((entry) => [entry[definition.key], entry]),
230
- );
231
- for (const entry of current) {
232
- const label = `${definition.kind}:${entry[definition.key]}`;
233
- for (const field of lifecycleFields) {
234
- if (
235
- !Object.prototype.hasOwnProperty.call(entry, field) ||
236
- typeof entry[field] !== "string"
237
- ) {
238
- issues.push(`${label}: lifecycle field ${field} is missing`);
239
- }
240
- }
241
- if (!capabilityGroups.has(entry.capabilityGroup)) {
242
- issues.push(
243
- `${label}: capability group ${entry.capabilityGroup || "<empty>"} is not registered`,
244
- );
245
- }
246
- const previous = baselineByKey.get(entry[definition.key]);
247
- const currentContract = publicSurfaceContract(entry, definition.kind);
248
- if (
249
- previous &&
250
- JSON.stringify(currentContract) !==
251
- JSON.stringify(publicSurfaceContract(previous, definition.kind))
252
- ) {
253
- const approval = policy.approvedPublicSurfaceTransitions?.[label];
254
- const approved =
255
- approval?.fromRevision === revision &&
256
- String(approval?.rationale || "").trim() &&
257
- JSON.stringify(approval?.contract) ===
258
- JSON.stringify(currentContract);
259
- if (!approved) {
260
- issues.push(
261
- `${label}: existing public contract drifted from ${revision}`,
262
- );
263
- }
264
- }
265
- if (!previous && !entry.nonDuplicationRationale) {
266
- issues.push(
267
- `${label}: new public surface requires a non-duplication rationale`,
268
- );
269
- }
270
- }
271
- }
272
- return issues;
273
- }
274
-
275
167
  function evaluateAddedFunctionBudgets({
276
168
  file,
277
169
  metrics,
@@ -497,12 +389,18 @@ function checkMaintainability({ root = process.cwd() } = {}) {
497
389
  root,
498
390
  extendedCoverageRevision,
499
391
  );
392
+ const { governed: governedCurrent, migration: facadeMigration } =
393
+ governUniversalFacadeWorkflowMetrics({
394
+ root,
395
+ current,
396
+ workflowMetricsAtRevision,
397
+ });
500
398
  const issues = evaluateExceptionGovernance({ policy });
501
399
  issues.push(...evaluateExceptionBudget({ policy }));
502
400
  const hotspots = collectHotspots(root, current, 20, debt.hotspots || []);
503
401
  issues.push(
504
402
  ...evaluateDebtAuthority({
505
- current,
403
+ current: governedCurrent,
506
404
  policy,
507
405
  debt,
508
406
  capabilityIds: new Set(
@@ -518,14 +416,19 @@ function checkMaintainability({ root = process.cwd() } = {}) {
518
416
  );
519
417
  issues.push(
520
418
  ...evaluateWorkflowBudgets({
521
- current,
419
+ current: governedCurrent,
522
420
  baselineFiles: baselineWorkflows,
523
421
  policy,
524
422
  }),
525
423
  );
526
424
  issues.push(...evaluateRepositoryBudgets({ current, policy }));
527
425
  issues.push(
528
- ...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
426
+ ...evaluatePublicSurface({
427
+ root,
428
+ revision: enforcementRevision,
429
+ policy,
430
+ migration: facadeMigration,
431
+ }),
529
432
  );
530
433
  if (issues.length > 0) {
531
434
  throw new Error(`maintainability check failed:\n- ${issues.join("\n- ")}`);
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from "node:assert/strict";
4
+ import crypto from "node:crypto";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
8
 
8
9
  import {
9
10
  parseWorkflowDocument,
@@ -12,13 +13,150 @@ import {
12
13
 
13
14
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
15
  const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
15
- const releaseMarker =
16
- /(?:release-candidate-promote\.yml|promote-buildchain-ref|v4-release-candidate-promote|release-tail-runtime)/u;
17
16
 
18
17
  function read(relative) {
19
18
  return fs.readFileSync(path.join(root, relative), "utf8");
20
19
  }
21
20
 
21
+ const LOCAL_MODULE_EXTENSIONS = ["", ".js", ".mjs", ".cjs"];
22
+ const PRIVILEGED_ENTRYPOINTS = [
23
+ "actions/v4-release-candidate-promote/index.js",
24
+ ];
25
+
26
+ function localModuleSpecifiers(source) {
27
+ const specifiers = [];
28
+ const expression =
29
+ /(?:\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)["'](\.[^"']+)["']/gu;
30
+ for (const match of source.matchAll(expression)) specifiers.push(match[1]);
31
+ return [...new Set(specifiers)].sort();
32
+ }
33
+
34
+ function resolveLocalModule(importer, specifier) {
35
+ const unresolved = path.posix.normalize(
36
+ path.posix.join(path.posix.dirname(importer), specifier),
37
+ );
38
+ assert.ok(
39
+ !unresolved.startsWith("../") && !path.posix.isAbsolute(unresolved),
40
+ `local module import escapes the repository: ${importer} -> ${specifier}`,
41
+ );
42
+ const candidates = [
43
+ ...LOCAL_MODULE_EXTENSIONS.map((extension) => `${unresolved}${extension}`),
44
+ ...LOCAL_MODULE_EXTENSIONS.slice(1).map((extension) =>
45
+ path.posix.join(unresolved, `index${extension}`),
46
+ ),
47
+ ];
48
+ const matches = candidates.filter((relative) => {
49
+ const absolute = path.join(root, relative);
50
+ return fs.existsSync(absolute) && fs.statSync(absolute).isFile();
51
+ });
52
+ assert.equal(
53
+ matches.length,
54
+ 1,
55
+ `local module import must resolve exactly once: ${importer} -> ${specifier}`,
56
+ );
57
+ return matches[0];
58
+ }
59
+
60
+ export function discoverStaticModuleClosure(entrypoints) {
61
+ const pending = [...entrypoints];
62
+ const visited = new Set();
63
+ while (pending.length > 0) {
64
+ const relative = pending.pop();
65
+ if (visited.has(relative)) continue;
66
+ assert.ok(
67
+ fs.statSync(path.join(root, relative)).isFile(),
68
+ `privileged entry is not a file: ${relative}`,
69
+ );
70
+ visited.add(relative);
71
+ for (const specifier of localModuleSpecifiers(read(relative))) {
72
+ const dependency = resolveLocalModule(relative, specifier);
73
+ if (!visited.has(dependency)) pending.push(dependency);
74
+ }
75
+ }
76
+ return [...visited].sort();
77
+ }
78
+
79
+ function rootedPathSet(paths) {
80
+ return `sha256:${crypto
81
+ .createHash("sha256")
82
+ .update(JSON.stringify([...paths].sort()))
83
+ .digest("hex")}`;
84
+ }
85
+
86
+ function productionFiles(relative, extensions) {
87
+ const absolute = path.join(root, relative);
88
+ if (!fs.existsSync(absolute)) return [];
89
+ const entries = fs.readdirSync(absolute, { withFileTypes: true });
90
+ return entries.flatMap((entry) => {
91
+ const child = path.posix.join(relative, entry.name);
92
+ if (
93
+ entry.isDirectory() &&
94
+ !["dist", "node_modules", ".git"].includes(entry.name)
95
+ )
96
+ return productionFiles(child, extensions);
97
+ if (
98
+ entry.isFile() &&
99
+ extensions.some((extension) => entry.name.endsWith(extension))
100
+ )
101
+ return [child];
102
+ return [];
103
+ });
104
+ }
105
+
106
+ function matchingProductionFiles(roots, extensions, pattern) {
107
+ return roots
108
+ .flatMap((relative) => productionFiles(relative, extensions))
109
+ .filter((relative) => relative !== "scripts/check-v4-release-topology.mjs")
110
+ .filter((relative) => pattern.test(read(relative)))
111
+ .sort();
112
+ }
113
+
114
+ export function discoverV4ReleaseAuthorityClosure() {
115
+ const providerPlane = matchingProductionFiles(
116
+ ["actions", "packages/core"],
117
+ [".js", ".mjs", ".cjs"],
118
+ /(?:release-tail-provider-(?:adapters|plane)|create(?:GitHubReleaseAssets|SignedStaticChannel|SiteReleaseActivation|ReleasedEvidence)Adapter)/u,
119
+ );
120
+ const runtimeSelectors = matchingProductionFiles(
121
+ [".github/workflows", "scripts", "packages/core"],
122
+ [".yml", ".yaml", ".js", ".mjs", ".cjs"],
123
+ /(?:promotion-runtime-sha|resume-buildchain-runtime-sha|BUILDCHAIN_(?:CURRENT_RUNTIME_SHA|RUNTIME_REF|RESUME_CANDIDATE_RUN_ID)|authorizeV4RuntimeSelection|scanV4RuntimeSelectorPersistence|v4-release-candidate-adapter)/u,
124
+ );
125
+ const terminalProjections = matchingProductionFiles(
126
+ [".github/workflows", "actions", "packages/core", "scripts"],
127
+ [".yml", ".yaml", ".js", ".mjs", ".cjs"],
128
+ /(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
129
+ );
130
+ const privilegedModules = discoverStaticModuleClosure(PRIVILEGED_ENTRYPOINTS);
131
+ const legacyEngineModules = productionFiles(
132
+ "actions/promote-buildchain-ref",
133
+ [".js", ".mjs", ".cjs"],
134
+ )
135
+ .filter((relative) =>
136
+ /(?:^|\/)(?:lib|promote-(?:alpha|release|major)-channel|durable-transaction-operations)\.js$/u.test(
137
+ relative,
138
+ ),
139
+ )
140
+ .sort();
141
+ return {
142
+ providerAdapters: [
143
+ ...new Set([
144
+ ...providerPlane,
145
+ "packages/core/release-tail-provider-plane.js",
146
+ ]),
147
+ ].sort(),
148
+ runtimeSelectors,
149
+ runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
150
+ terminalProjections,
151
+ privilegedExecutableClosure: {
152
+ entrypoints: PRIVILEGED_ENTRYPOINTS,
153
+ modules: privilegedModules,
154
+ root: rootedPathSet(privilegedModules),
155
+ },
156
+ legacyEngineModules,
157
+ };
158
+ }
159
+
22
160
  function jobBlock(source, jobId) {
23
161
  const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
24
162
  return (
@@ -42,7 +180,9 @@ function permission(block, name) {
42
180
  function workflowSnapshot(relative) {
43
181
  const source = read(relative);
44
182
  const document = parseWorkflowDocument(source);
45
- const jobs = document.jobs.map((job) => {
183
+ const jobs = document.jobs
184
+ .filter((job) => job.id !== "universal-bootstrap")
185
+ .map((job) => {
46
186
  const block = jobBlock(source, job.id);
47
187
  const uses = job.uses || null;
48
188
  return {
@@ -68,7 +208,7 @@ function workflowSnapshot(relative) {
68
208
  "direct-publication-command",
69
209
  ].filter(Boolean),
70
210
  };
71
- });
211
+ });
72
212
  return {
73
213
  path: relative,
74
214
  triggers: document.triggers,
@@ -76,6 +216,7 @@ function workflowSnapshot(relative) {
76
216
  reusableEdges: parseYamlUses(source)
77
217
  .map(({ value }) => value)
78
218
  .filter((value) => /\.github\/workflows\//u.test(value))
219
+ .filter((value) => !/\/bootstrap\.yml(?:@v4)?$/u.test(value))
79
220
  .sort(),
80
221
  };
81
222
  }
@@ -104,25 +245,27 @@ export function discoverV4ReleaseTopology(
104
245
  "carriers",
105
246
  "mutationSignals",
106
247
  ],
107
- workflows: workflows.map((workflow) => ({
108
- path: workflow.path,
109
- triggers: workflow.triggers,
110
- jobs: workflow.jobs.map((job) =>
111
- [
112
- job.id,
113
- job.kind,
114
- job.uses || "-",
115
- job.permissions.contents || "-",
116
- job.permissions.idToken || "-",
117
- Object.entries(job.carriers)
118
- .filter(([, present]) => present)
119
- .map(([name]) => name)
120
- .join(",") || "-",
121
- job.mutationSignals.join(",") || "-",
122
- ].join("|"),
123
- ),
124
- reusableEdges: workflow.reusableEdges,
125
- })),
248
+ workflows: workflows
249
+ .filter((workflow) => semanticPaths.has(workflow.path))
250
+ .map((workflow) => ({
251
+ path: workflow.path,
252
+ triggers: workflow.triggers,
253
+ jobs: workflow.jobs.map((job) =>
254
+ [
255
+ job.id,
256
+ job.kind,
257
+ job.uses || "-",
258
+ job.permissions.contents || "-",
259
+ job.permissions.idToken || "-",
260
+ Object.entries(job.carriers)
261
+ .filter(([, present]) => present)
262
+ .map(([name]) => name)
263
+ .join(",") || "-",
264
+ job.mutationSignals.join(",") || "-",
265
+ ].join("|"),
266
+ ),
267
+ reusableEdges: workflow.reusableEdges,
268
+ })),
126
269
  metrics: {
127
270
  workflowCount: workflows.length,
128
271
  jobCount: jobs.length,
@@ -174,7 +317,26 @@ export function findUnknownV4ReleaseTopology(
174
317
  const declared = new Set(workflowPaths);
175
318
  return allWorkflowPaths
176
319
  .filter((relative) => !declared.has(relative))
177
- .filter((relative) => releaseMarker.test(readWorkflow(relative)))
320
+ .filter((relative) => {
321
+ const source = readWorkflow(relative);
322
+ const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
323
+ /(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
324
+ value,
325
+ ),
326
+ );
327
+ const hasReleaseLanguage =
328
+ /(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
329
+ source,
330
+ );
331
+ const hasMutationAuthority =
332
+ /(?:contents|id-token):\s*write/u.test(source) ||
333
+ /(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
334
+ source,
335
+ );
336
+ return (
337
+ usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
338
+ );
339
+ })
178
340
  .sort();
179
341
  }
180
342
 
@@ -187,10 +349,95 @@ function assertClosedWorld(workflowPaths) {
187
349
  );
188
350
  }
189
351
 
352
+ function assertAuthorityClosure(ledger) {
353
+ const closure = ledger.authorityClosure;
354
+ assert.ok(
355
+ closure && typeof closure === "object",
356
+ "authority closure missing",
357
+ );
358
+ for (const className of [
359
+ "providerAdapters",
360
+ "runtimeSelectors",
361
+ "runtimeEngines",
362
+ "terminalProjections",
363
+ "legacyEngineModules",
364
+ ]) {
365
+ assert.ok(
366
+ Array.isArray(closure[className]) && closure[className].length > 0,
367
+ `authority closure class ${className} is empty`,
368
+ );
369
+ for (const relative of closure[className])
370
+ assert.ok(
371
+ fs.statSync(path.join(root, relative)).isFile(),
372
+ `authority closure path is not a file: ${relative}`,
373
+ );
374
+ }
375
+ const discovered = discoverV4ReleaseAuthorityClosure();
376
+ for (const className of Object.keys(discovered))
377
+ assert.deepEqual(
378
+ closure[className],
379
+ discovered[className],
380
+ `authority closure class drifted: ${className}`,
381
+ );
382
+ assert.deepEqual(
383
+ closure.privilegedExecutableClosure,
384
+ discovered.privilegedExecutableClosure,
385
+ "privileged executable transitive closure drifted",
386
+ );
387
+ assert.match(
388
+ closure.privilegedExecutableClosure.root,
389
+ /^sha256:[0-9a-f]{64}$/u,
390
+ );
391
+ const reachableLegacyEngines = closure.legacyEngineModules.filter(
392
+ (relative) =>
393
+ closure.privilegedExecutableClosure.modules.includes(relative),
394
+ );
395
+ assert.deepEqual(
396
+ reachableLegacyEngines,
397
+ [],
398
+ `legacy release engines remain reachable from APPLY: ${reachableLegacyEngines.join(", ")}`,
399
+ );
400
+ assert.deepEqual(closure.runtimeEngines, [
401
+ "actions/v4-release-candidate-promote/index.js",
402
+ ]);
403
+ assert.equal(
404
+ closure.freshEntry,
405
+ ".github/workflows/release-candidate-promote.yml",
406
+ );
407
+ assert.equal(
408
+ closure.recoveryEntry,
409
+ ".github/workflows/buildchain-ref-promotion-recovery.yml",
410
+ );
411
+ const engineSurface = [
412
+ ".github/workflows/.release-candidate-promote.yml",
413
+ ...closure.privilegedExecutableClosure.modules,
414
+ ]
415
+ .map(read)
416
+ .join("\n");
417
+ for (const pattern of closure.forbiddenLegacyEnginePatterns)
418
+ assert.doesNotMatch(
419
+ engineSurface,
420
+ new RegExp(pattern, "u"),
421
+ `legacy release engine remains reachable: ${pattern}`,
422
+ );
423
+ const canonicalWorkflow = read(
424
+ ".github/workflows/.release-candidate-promote.yml",
425
+ );
426
+ assert.match(
427
+ canonicalWorkflow,
428
+ /scripts\/v4-release-candidate-adapter\.mjs/u,
429
+ );
430
+ assert.match(
431
+ canonicalWorkflow,
432
+ /release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
433
+ );
434
+ }
435
+
190
436
  export function checkV4ReleaseTopology() {
191
437
  const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
192
438
  assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
193
439
  assertClosedWorld(ledger.closedWorld.workflowPaths);
440
+ assertAuthorityClosure(ledger);
194
441
  const actual = discoverV4ReleaseTopology(
195
442
  ledger.closedWorld.workflowPaths,
196
443
  ledger.semanticScope.workflowPaths,
@@ -219,19 +466,28 @@ export function checkV4ReleaseTopology() {
219
466
  return actual;
220
467
  }
221
468
 
222
- if (process.argv.includes("--print")) {
223
- const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
224
- process.stdout.write(
225
- `${JSON.stringify(
226
- discoverV4ReleaseTopology(
227
- ledger.closedWorld.workflowPaths,
228
- ledger.semanticScope.workflowPaths,
229
- ),
230
- null,
231
- 2,
232
- )}\n`,
233
- );
234
- } else {
235
- checkV4ReleaseTopology();
236
- process.stdout.write("v4 release topology: ok\n");
469
+ if (
470
+ process.argv[1] &&
471
+ import.meta.url === pathToFileURL(process.argv[1]).href
472
+ ) {
473
+ if (process.argv.includes("--print-closure")) {
474
+ process.stdout.write(
475
+ `${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
476
+ );
477
+ } else if (process.argv.includes("--print")) {
478
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
479
+ process.stdout.write(
480
+ `${JSON.stringify(
481
+ discoverV4ReleaseTopology(
482
+ ledger.closedWorld.workflowPaths,
483
+ ledger.semanticScope.workflowPaths,
484
+ ),
485
+ null,
486
+ 2,
487
+ )}\n`,
488
+ );
489
+ } else {
490
+ checkV4ReleaseTopology();
491
+ process.stdout.write("v4 release topology: ok\n");
492
+ }
237
493
  }