@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.55

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 (159) hide show
  1. package/README.md +9 -6
  2. package/actions/release-tail/README.md +8 -0
  3. package/actions/v4-release-candidate-promote/README.md +3 -0
  4. package/architecture/agent-change-map.md +1015 -0
  5. package/architecture/ci-lane-change-budget.json +256 -0
  6. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  7. package/architecture/internal-capabilities.json +220 -15
  8. package/architecture/maintainability-debt.json +3229 -0
  9. package/architecture/maintainability-policy.json +489 -132
  10. package/architecture/release-tail-contract-inventory.json +5 -2
  11. package/architecture/v3-core-mechanism-inventory.json +4 -3
  12. package/architecture/v3-v4-capability-closure.md +59 -0
  13. package/architecture/v3-v4-live-capability-inventory.json +102404 -0
  14. package/architecture/v4-adopter-delivery-parity.json +32 -5
  15. package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
  16. package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
  17. package/architecture/v4-floating-consumer-policy.json +6 -0
  18. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  19. package/architecture/v4-runtime-semantic-closure.json +710 -0
  20. package/bin/buildchain.mjs +7 -1
  21. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  22. package/bin/internal/command-registry.mjs +1 -0
  23. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  24. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  25. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  26. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  27. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  28. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  29. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  30. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  31. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  32. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  33. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  34. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  35. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  36. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  37. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  38. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  39. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  40. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  41. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  42. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  43. package/dist/site/agent-index.json +4 -0
  44. package/dist/site/artifact-schemas.json +8 -0
  45. package/dist/site/buildchain-contract.json +125 -34
  46. package/dist/site/buildchain-site.json +308 -33
  47. package/dist/site/capability-registry.json +13 -12
  48. package/dist/site/cli-registry.json +57 -2
  49. package/dist/site/controller-registry.json +41 -5
  50. package/dist/site/kfd-claims.json +438 -24
  51. package/dist/site/kfd-upstream-aggregate.json +1 -1
  52. package/dist/site/manual-registry.json +22 -7
  53. package/dist/site/node-api-registry.json +12843 -9314
  54. package/dist/site/page-registry.json +288 -21
  55. package/dist/site/public-surface-audit.json +281 -21
  56. package/dist/site/publication-authority-registry.json +66 -8
  57. package/dist/site/publication-registry.json +4 -4
  58. package/dist/site/release-provenance.json +20 -0
  59. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  60. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  61. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  62. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  63. package/dist/site/site-manifest.json +19 -11
  64. package/dist/site/workflow-registry.json +195 -17
  65. package/docs/MAP.md +4 -2
  66. package/docs/cli-reference.md +70 -2
  67. package/docs/cli.md +8 -0
  68. package/docs/dev-delivery-warrant.md +34 -10
  69. package/docs/node-api-reference.md +524 -279
  70. package/docs/publish-transaction.md +11 -4
  71. package/docs/release-candidate.md +7 -0
  72. package/docs/release-tail-provider-plane.md +30 -0
  73. package/docs/v4-adopter-delivery.md +152 -0
  74. package/docs/v4-declarative-release-tail-migration.md +43 -0
  75. package/docs/v4-publication-rehearsal.md +130 -0
  76. package/package.json +24 -4
  77. package/packages/core/adopter-delivery-passport.js +295 -0
  78. package/packages/core/adopter-delivery-vectors.js +158 -0
  79. package/packages/core/adopter-delivery-vectors.json +77 -0
  80. package/packages/core/buildchain-agent-manuals.js +1 -0
  81. package/packages/core/buildchain-config.js +84 -0
  82. package/packages/core/buildchain-contract.js +31 -1
  83. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  84. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  85. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  86. package/packages/core/buildchain-publication-authority.js +2 -0
  87. package/packages/core/ci-lane-change-budget.js +247 -0
  88. package/packages/core/dev-delivery-native-proof.js +43 -27
  89. package/packages/core/dev-delivery-warrant-settlement.js +239 -3
  90. package/packages/core/dev-delivery-warrant-state.js +46 -10
  91. package/packages/core/dev-delivery-warrant.js +29 -7
  92. package/packages/core/github-governance-authority.js +5 -4
  93. package/packages/core/index.js +6 -0
  94. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  95. package/packages/core/paper-scaffold-content.js +401 -0
  96. package/packages/core/paper.js +12 -378
  97. package/packages/core/publication-control-plane-audit.js +135 -111
  98. package/packages/core/publication-rehearsal-projection.js +104 -0
  99. package/packages/core/publication-rehearsal-runtime.js +190 -0
  100. package/packages/core/published-delivery-authority.js +266 -0
  101. package/packages/core/release-candidate-recovery.js +71 -15
  102. package/packages/core/release-candidate.js +2 -2
  103. package/packages/core/release-passport.js +95 -170
  104. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  105. package/packages/core/v4-adopter-delivery.js +359 -0
  106. package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
  107. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  108. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  109. package/packages/core/v4-floating-consumer-policy.js +139 -35
  110. package/packages/core/v4-floating-consumer-release-passport.js +154 -8
  111. package/packages/core/v4-publication-qualification.js +222 -0
  112. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  113. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  114. package/packages/core/v4-publication-rehearsal.js +481 -0
  115. package/packages/core/workflow-yaml-contract.js +24 -2
  116. package/scripts/artifact-signing-controller-core.mjs +1 -1
  117. package/scripts/audit-publication-control-plane.mjs +17 -17
  118. package/scripts/buildchain-cli-help.mjs +13 -0
  119. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  120. package/scripts/check-internal-architecture.mjs +23 -3
  121. package/scripts/check-inventory.mjs +24 -6
  122. package/scripts/check-maintainability.mjs +145 -4
  123. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  124. package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
  125. package/scripts/check-v4-floating-consumer-policy-contract.mjs +30 -4
  126. package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
  127. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  128. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  129. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  130. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  131. package/scripts/dev-delivery-warrant.mjs +50 -6
  132. package/scripts/dev-pr-auto-merge.mjs +25 -11
  133. package/scripts/dev-pr-delivery-warrant.mjs +45 -0
  134. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  135. package/scripts/dev-qualification-patrol.mjs +9 -9
  136. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  137. package/scripts/generate-agent-change-map.mjs +118 -0
  138. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  139. package/scripts/generate-release-candidate-passport.mjs +177 -0
  140. package/scripts/generate-site-bundle.mjs +36 -0
  141. package/scripts/maintainability-domain-metrics.mjs +237 -0
  142. package/scripts/maintainability-governance.mjs +473 -0
  143. package/scripts/maintainability-metrics.mjs +28 -6
  144. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  145. package/scripts/promotion-channel-router.mjs +4 -4
  146. package/scripts/promotion-routing-evidence.mjs +35 -6
  147. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  148. package/scripts/release-candidate-resolver.mjs +44 -0
  149. package/scripts/release-line-policy.mjs +25 -15
  150. package/scripts/release-tail.mjs +68 -3
  151. package/scripts/resume-from-candidate-run.mjs +595 -65
  152. package/scripts/run-lifecycle-core.mjs +341 -376
  153. package/scripts/site-capability-metadata.mjs +21 -0
  154. package/scripts/stable-candidate-qualification.mjs +9 -10
  155. package/scripts/v3-v4-capability-catalog.mjs +439 -0
  156. package/scripts/v4-consumer-policy.mjs +4 -0
  157. package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
  158. package/scripts/v4-declarative-promotion-admission.mjs +42 -0
  159. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { execFileSync } from "node:child_process";
7
+ import { evaluateCiLaneChangeBudget } from "../packages/core/ci-lane-change-budget.js";
8
+
9
+ const root = process.cwd();
10
+ const workflowRoot = path.join(root, ".github", "workflows");
11
+ const policyPath = path.join(
12
+ root,
13
+ "architecture",
14
+ "ci-lane-change-budget.json",
15
+ );
16
+
17
+ const workflows = fs
18
+ .readdirSync(workflowRoot, { withFileTypes: true })
19
+ .filter((entry) => entry.isFile() && /\.(?:yaml|yml)$/u.test(entry.name))
20
+ .map((entry) => {
21
+ const workflowPath = path.posix.join(".github/workflows", entry.name);
22
+ return {
23
+ path: workflowPath,
24
+ text: fs.readFileSync(path.join(workflowRoot, entry.name), "utf8"),
25
+ };
26
+ })
27
+ .sort((left, right) => left.path.localeCompare(right.path));
28
+
29
+ const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
30
+ const baselineRevision = String(policy.baseline?.gitRevision || "").trim();
31
+ if (!/^[0-9a-f]{40}$/u.test(baselineRevision)) {
32
+ throw new Error("baseline.gitRevision must be an exact Git commit");
33
+ }
34
+ const embeddedBaselineLanes = policy.baseline?.lanes;
35
+ if (
36
+ !Array.isArray(embeddedBaselineLanes) ||
37
+ embeddedBaselineLanes.length === 0
38
+ ) {
39
+ throw new Error("baseline.lanes must contain the exact offline lane set");
40
+ }
41
+ const embeddedLaneSetRoot = `sha256:${crypto
42
+ .createHash("sha256")
43
+ .update(`${JSON.stringify(embeddedBaselineLanes)}\n`)
44
+ .digest("hex")}`;
45
+ if (embeddedLaneSetRoot !== policy.baseline?.laneSetRoot) {
46
+ throw new Error("baseline laneSetRoot does not match baseline.lanes");
47
+ }
48
+
49
+ let baselineWorkflows = null;
50
+ try {
51
+ execFileSync("git", ["cat-file", "-e", `${baselineRevision}^{commit}`], {
52
+ cwd: root,
53
+ stdio: "ignore",
54
+ });
55
+ const baselinePaths = execFileSync(
56
+ "git",
57
+ [
58
+ "ls-tree",
59
+ "-r",
60
+ "--name-only",
61
+ baselineRevision,
62
+ "--",
63
+ ".github/workflows",
64
+ ],
65
+ { cwd: root, encoding: "utf8" },
66
+ )
67
+ .split(/\r?\n/u)
68
+ .filter((value) => /\.(?:yaml|yml)$/u.test(value))
69
+ .sort();
70
+ baselineWorkflows = baselinePaths.map((workflowPath) => ({
71
+ path: workflowPath,
72
+ text: execFileSync("git", ["show", `${baselineRevision}:${workflowPath}`], {
73
+ cwd: root,
74
+ encoding: "utf8",
75
+ }),
76
+ }));
77
+ } catch {
78
+ // Shallow and exported source cuts may not contain the pinned commit object.
79
+ // The content-rooted embedded lane set remains the exact offline authority.
80
+ }
81
+
82
+ const embeddedEvaluation = evaluateCiLaneChangeBudget({
83
+ policy,
84
+ workflows,
85
+ });
86
+ const evaluation = baselineWorkflows
87
+ ? evaluateCiLaneChangeBudget({ policy, workflows, baselineWorkflows })
88
+ : embeddedEvaluation;
89
+ if (
90
+ baselineWorkflows &&
91
+ (evaluation.baselineLaneCount !== embeddedEvaluation.baselineLaneCount ||
92
+ JSON.stringify(evaluation.newLanes) !==
93
+ JSON.stringify(embeddedEvaluation.newLanes))
94
+ ) {
95
+ throw new Error(
96
+ "embedded baseline lanes do not match the pinned baseline revision",
97
+ );
98
+ }
99
+ process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
100
+ if (!evaluation.ok) process.exitCode = 1;
@@ -100,8 +100,8 @@ function dependencyCycles(graph) {
100
100
  }
101
101
 
102
102
  function assertIndexShape(index) {
103
- if (index?.schemaVersion !== 1) {
104
- throw new Error("internal architecture index schemaVersion must be 1");
103
+ if (index?.schemaVersion !== 2) {
104
+ throw new Error("internal architecture index schemaVersion must be 2");
105
105
  }
106
106
  for (const field of [
107
107
  "coverageRoots",
@@ -209,7 +209,27 @@ function evaluateCapabilities(root, index) {
209
209
  if (!Array.isArray(capability.tests) || capability.tests.length === 0) {
210
210
  issues.push(`${capability.id}: test mapping is empty`);
211
211
  }
212
- for (const field of ["implementation", "tests", "contracts"]) {
212
+ if (!Array.isArray(capability.generatedOutputs)) {
213
+ issues.push(`${capability.id}: generated output mapping is missing`);
214
+ }
215
+ if (
216
+ !Array.isArray(capability.validationCommands) ||
217
+ capability.validationCommands.length === 0
218
+ ) {
219
+ issues.push(`${capability.id}: validation command mapping is empty`);
220
+ } else if (
221
+ capability.validationCommands.some(
222
+ (command) => !String(command || "").trim(),
223
+ )
224
+ ) {
225
+ issues.push(`${capability.id}: validation command is empty`);
226
+ }
227
+ for (const field of [
228
+ "implementation",
229
+ "tests",
230
+ "contracts",
231
+ "generatedOutputs",
232
+ ]) {
213
233
  for (const relativePath of capability[field] || []) {
214
234
  if (!fs.existsSync(path.resolve(root, relativePath))) {
215
235
  issues.push(
@@ -285,6 +285,10 @@ const channelPromotionWorkflow = fs.readFileSync(
285
285
  path.join(root, ".github/workflows/release-candidate-promote.yml"),
286
286
  "utf8",
287
287
  );
288
+ const boundedAlphaRecoveryWorkflow = fs.readFileSync(
289
+ path.join(root, ".github/workflows/buildchain-ref-promotion-recovery.yml"),
290
+ "utf8",
291
+ );
288
292
  const promotionShellRouting = parsePromotionShellRouting(
289
293
  fs.readFileSync(path.join(root, ".buildchain/promotion-shell-routing.json"), "utf8"),
290
294
  { major: Number(selfDogfoodMajor) },
@@ -295,6 +299,20 @@ if (channelPromotionWorkflow !== generateChannelPromotionWorkflow(advancedPromot
295
299
  })) {
296
300
  throw new Error("generated channel promotion workflow is stale");
297
301
  }
302
+ for (const requiredSnippet of [
303
+ "uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@alpha/v4/v4.0",
304
+ "promotion-shell-ref: ${{ needs.consumer-admission.outputs.shell-call-ref }}",
305
+ "declarative-release-tail: true",
306
+ ]) {
307
+ if (!boundedAlphaRecoveryWorkflow.includes(requiredSnippet)) {
308
+ throw new Error(`bounded alpha recovery missing bootstrap contract: ${requiredSnippet}`);
309
+ }
310
+ }
311
+ for (const workflowSource of [boundedAlphaRecoveryWorkflow, channelPromotionWorkflow]) {
312
+ if (workflowSource.includes("channel-finalization-recovery")) {
313
+ throw new Error("Buildchain-only alpha bootstrap must not add a public or recovery workflow input");
314
+ }
315
+ }
298
316
  for (const requiredSnippet of [
299
317
  "buildchain-channel:",
300
318
  `/${promotionShellRouting.alpha.workflowPath}@${promotionShellRouting.alpha.callRef}`,
@@ -759,8 +777,8 @@ const registeredActionIds = (workflowRegistry.actions || []).map((entry) => entr
759
777
  const readmeActionIndex = fs.readFileSync(path.join(root, "README.md"), "utf8");
760
778
  const mapActionIndex = fs.readFileSync(path.join(root, "docs/MAP.md"), "utf8");
761
779
  const retrospectiveActionIndex = fs.readFileSync(path.join(root, ".github/retrospectives/2026-07-10-buildchain-consolidation.md"), "utf8");
762
- if (registeredActionIds.length !== 7) {
763
- throw new Error(`workflow-registry.json must expose the seven current action entries, got ${registeredActionIds.length}`);
780
+ if (registeredActionIds.length !== 8) {
781
+ throw new Error(`workflow-registry.json must expose the eight current action entries, got ${registeredActionIds.length}`);
764
782
  }
765
783
  for (const actionId of registeredActionIds) {
766
784
  if (!readmeActionIndex.includes(`actions/${actionId}`) || !mapActionIndex.includes(`actions/${actionId}`)) {
@@ -1021,16 +1039,16 @@ for (const forbiddenSnippet of [
1021
1039
  for (const requiredSnippet of [
1022
1040
  "id-token: write",
1023
1041
  "actions: write",
1024
- "uses: ./.github/workflows/.release-candidate-promote.yml",
1042
+ "uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@v4-alpha",
1025
1043
  "github.event.workflow_run.event == 'push'",
1026
1044
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
1027
1045
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
1028
1046
  "resume-candidate-run-id:",
1029
1047
  "resume-expected-source-tree:",
1030
- "resume-buildchain-runtime-sha:",
1031
- "github-release: true",
1048
+ "resume-buildchain-runtime-ref:",
1049
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
1032
1050
  "release-passport-buildchain-self-kfd: true",
1033
- "artifact-patterns: ${{ startsWith(github.event.workflow_run.head_branch || inputs['target-ref'], 'alpha/') && 'buildchain-package-*' || '' }}",
1051
+ "declarative-release-tail: true",
1034
1052
  "release-passport-impact-json: .buildchain/release-impact.json",
1035
1053
  ]) {
1036
1054
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -5,10 +5,52 @@ import path from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import {
8
- analyzeJavaScript,
8
+ analyzeSource,
9
+ analyzeWorkflow,
9
10
  collectMaintainabilityMetrics,
10
11
  isHandMaintainedSource,
11
12
  } from "./maintainability-metrics.mjs";
13
+ import {
14
+ calculateDebtMeasuredExcess,
15
+ evaluateDebtAuthority,
16
+ evaluateDebtBudget,
17
+ evaluateExceptionBudget,
18
+ evaluateExceptionGovernance,
19
+ evaluateTestBudgets,
20
+ evaluateWorkflowBudgets,
21
+ } from "./maintainability-governance.mjs";
22
+
23
+ function collectHotspots(root, current, limit = 20, shallowFallback = []) {
24
+ const maintained = new Set([
25
+ ...Object.keys(current.files || {}),
26
+ ...Object.keys(current.tests || {}),
27
+ ...Object.keys(current.workflows || {}),
28
+ ]);
29
+ // Shallow consumers cannot reproduce repository-wide churn ranking.
30
+ // Reuse the audited routes; full clones still re-rank them below.
31
+ // This keeps consumer verification deterministic without inventing history.
32
+ if (
33
+ gitOutput(root, ["rev-parse", "--is-shallow-repository"]).trim() === "true"
34
+ ) {
35
+ return shallowFallback.slice(0, limit);
36
+ }
37
+ const counts = new Map();
38
+ const record = (file) =>
39
+ maintained.has(file) && counts.set(file, (counts.get(file) || 0) + 1);
40
+ for (const args of [
41
+ ["log", "-n", "500", "--format=", "--name-only"],
42
+ ["diff", "--name-only"],
43
+ ])
44
+ for (const file of gitOutput(root, args).split("\n").filter(Boolean))
45
+ record(file);
46
+ return [...counts]
47
+ .sort(
48
+ ([leftPath, left], [rightPath, right]) =>
49
+ right - left || leftPath.localeCompare(rightPath),
50
+ )
51
+ .slice(0, limit)
52
+ .map(([file]) => file);
53
+ }
12
54
 
13
55
  function readJson(root, file) {
14
56
  return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
@@ -68,7 +110,35 @@ function sourceMetricsAtRevision(root, revision) {
68
110
  return Object.fromEntries(
69
111
  files.map((file) => [
70
112
  file,
71
- analyzeJavaScript(file, gitOutput(root, ["show", `${revision}:${file}`])),
113
+ analyzeSource(file, gitOutput(root, ["show", `${revision}:${file}`])),
114
+ ]),
115
+ );
116
+ }
117
+
118
+ function testMetricsAtRevision(root, revision) {
119
+ const files = gitOutput(root, ["ls-tree", "-r", "--name-only", revision])
120
+ .split("\n")
121
+ .filter((file) =>
122
+ /(?:^tests\/|(?:^|\/)tests?\/).+\.(?:c|m)?js$/u.test(file),
123
+ )
124
+ .sort();
125
+ return Object.fromEntries(
126
+ files.map((file) => [
127
+ file,
128
+ analyzeSource(file, gitOutput(root, ["show", `${revision}:${file}`])),
129
+ ]),
130
+ );
131
+ }
132
+
133
+ function workflowMetricsAtRevision(root, revision) {
134
+ const files = gitOutput(root, ["ls-tree", "-r", "--name-only", revision])
135
+ .split("\n")
136
+ .filter((file) => /^\.github\/workflows\/.*\.ya?ml$/u.test(file))
137
+ .sort();
138
+ return Object.fromEntries(
139
+ files.map((file) => [
140
+ file,
141
+ analyzeWorkflow(file, gitOutput(root, ["show", `${revision}:${file}`])),
72
142
  ]),
73
143
  );
74
144
  }
@@ -276,10 +346,14 @@ function evaluateRepositoryBudgets({ current, policy }) {
276
346
  return issues;
277
347
  }
278
348
 
349
+ function budgetsForFile(policy, file) {
350
+ return file.endsWith(".rs") ? policy.rustBudgets : policy.sourceBudgets;
351
+ }
352
+
279
353
  function evaluateMaintainability({ current, baselineFiles, policy }) {
280
354
  const issues = [];
281
- const budgets = policy.sourceBudgets;
282
355
  for (const [file, metrics] of Object.entries(current.files)) {
356
+ const budgets = budgetsForFile(policy, file);
283
357
  const baseline = baselineFiles[file];
284
358
  if (!baseline) {
285
359
  const transition = policy.approvedNewFileTransitions?.[file];
@@ -381,15 +455,66 @@ function evaluateMaintainability({ current, baselineFiles, policy }) {
381
455
 
382
456
  function checkMaintainability({ root = process.cwd() } = {}) {
383
457
  const policy = readJson(root, "architecture/maintainability-policy.json");
458
+ if (policy.schemaVersion !== 2) {
459
+ throw new Error("maintainability policy schemaVersion must be 2");
460
+ }
384
461
  const baseline = readJson(root, policy.baseline);
385
462
  const enforcementRevision = policy.enforcementRevision || baseline.revision;
463
+ const extendedCoverageRevision =
464
+ policy.extendedCoverageRevision || enforcementRevision;
386
465
  const hydratedRevisions = ensureMaintainabilityRevisionsAvailable(root, {
387
466
  baselineRevision: baseline.revision,
388
467
  enforcementRevision,
389
468
  });
469
+ const hydratedExtendedCoverageRevision = ensureRevisionAvailable(
470
+ root,
471
+ extendedCoverageRevision,
472
+ );
390
473
  const current = collectMaintainabilityMetrics({ root });
474
+ const debt = readJson(root, "architecture/maintainability-debt.json");
475
+ const architecture = readJson(
476
+ root,
477
+ "architecture/internal-capabilities.json",
478
+ );
391
479
  const baselineFiles = sourceMetricsAtRevision(root, enforcementRevision);
392
- const issues = evaluateMaintainability({ current, baselineFiles, policy });
480
+ const extendedSourceFiles = sourceMetricsAtRevision(
481
+ root,
482
+ extendedCoverageRevision,
483
+ );
484
+ for (const [file, metrics] of Object.entries(extendedSourceFiles)) {
485
+ if (file.endsWith(".rs")) baselineFiles[file] = metrics;
486
+ }
487
+ const baselineTests = testMetricsAtRevision(root, extendedCoverageRevision);
488
+ const baselineWorkflows = workflowMetricsAtRevision(
489
+ root,
490
+ extendedCoverageRevision,
491
+ );
492
+ const issues = evaluateExceptionGovernance({ policy });
493
+ issues.push(...evaluateExceptionBudget({ policy }));
494
+ const hotspots = collectHotspots(root, current, 20, debt.hotspots || []);
495
+ issues.push(
496
+ ...evaluateDebtAuthority({
497
+ current,
498
+ policy,
499
+ debt,
500
+ capabilityIds: new Set(
501
+ architecture.capabilities.map((entry) => entry.id),
502
+ ),
503
+ hotspots,
504
+ }),
505
+ );
506
+ issues.push(...evaluateDebtBudget({ debt, policy }));
507
+ issues.push(...evaluateMaintainability({ current, baselineFiles, policy }));
508
+ issues.push(
509
+ ...evaluateTestBudgets({ current, baselineFiles: baselineTests, policy }),
510
+ );
511
+ issues.push(
512
+ ...evaluateWorkflowBudgets({
513
+ current,
514
+ baselineFiles: baselineWorkflows,
515
+ policy,
516
+ }),
517
+ );
393
518
  issues.push(...evaluateRepositoryBudgets({ current, policy }));
394
519
  issues.push(
395
520
  ...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
@@ -401,12 +526,18 @@ function checkMaintainability({ root = process.cwd() } = {}) {
401
526
  schemaVersion: 1,
402
527
  baselineRevision: baseline.revision,
403
528
  enforcementRevision,
529
+ extendedCoverageRevision,
404
530
  hydratedBaselineRevision: hydratedRevisions[baseline.revision],
405
531
  hydratedEnforcementRevision: hydratedRevisions[enforcementRevision],
532
+ hydratedExtendedCoverageRevision,
406
533
  trackedFiles: current.repository.trackedFiles,
407
534
  sourceFiles: current.repository.handMaintainedSourceFiles,
408
535
  publicSurface: current.publicSurface,
409
536
  hotspots: current.hotspots,
537
+ governedDebtSurfaces: Object.values(debt.surfaces).reduce(
538
+ (total, entries) => total + Object.keys(entries).length,
539
+ 0,
540
+ ),
410
541
  };
411
542
  }
412
543
 
@@ -427,11 +558,21 @@ if (
427
558
  }
428
559
 
429
560
  export {
561
+ calculateDebtMeasuredExcess,
430
562
  checkMaintainability,
563
+ collectHotspots,
431
564
  ensureMaintainabilityRevisionsAvailable,
432
565
  ensureRevisionAvailable,
433
566
  evaluateMaintainability,
567
+ evaluateDebtAuthority,
568
+ evaluateDebtBudget,
569
+ evaluateExceptionBudget,
570
+ evaluateExceptionGovernance,
434
571
  evaluateRepositoryBudgets,
572
+ evaluateTestBudgets,
573
+ evaluateWorkflowBudgets,
435
574
  evaluatePublicSurface,
436
575
  sourceMetricsAtRevision,
576
+ testMetricsAtRevision,
577
+ workflowMetricsAtRevision,
437
578
  };
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import {
4
+ ProjectCutAdmissionError,
5
+ qualifyProjectCut,
6
+ releaseFamilyQueueLease,
7
+ } from "./project-cut-merge-queue-admission.mjs";
8
+
9
+ function parseArguments(argv) {
10
+ const options = {};
11
+ const names = new Map([
12
+ ["--base", "base"],
13
+ ["--head", "head"],
14
+ ["--initiative-id", "initiativeId"],
15
+ ["--assignment-id", "assignmentId"],
16
+ ["--delivery-class", "deliveryClass"],
17
+ ["--queue-attempt", "queueAttempt"],
18
+ ["--admission-proof-root", "admissionProofRoot"],
19
+ ["--release-family-marker", "releaseFamilyMarker"],
20
+ ["--expected-pr-head", "expectedPullRequestHead"],
21
+ ["--terminal-reason", "terminalReason"],
22
+ ["--evidence-root", "evidenceRoot"],
23
+ ]);
24
+ for (let index = 0; index < argv.length; index += 1) {
25
+ const name = argv[index];
26
+ const key = names.get(name);
27
+ if (!key) throw new Error(`unknown argument: ${name}`);
28
+ const value = argv[index + 1];
29
+ if (value === undefined || value.startsWith("--"))
30
+ throw new Error(`${name} requires a value`);
31
+ if (Object.hasOwn(options, key))
32
+ throw new Error(`${name} was provided more than once`);
33
+ options[key] = value;
34
+ index += 1;
35
+ }
36
+ return options;
37
+ }
38
+
39
+ function markerText(path) {
40
+ return path === "-"
41
+ ? fs.readFileSync(0, "utf8")
42
+ : fs.readFileSync(path, "utf8");
43
+ }
44
+
45
+ function repairReceipt(error) {
46
+ return {
47
+ schema: "project.cut.merge-queue-admission/v1",
48
+ ok: false,
49
+ decision: "repair-required",
50
+ retryable: false,
51
+ compositionChanged: false,
52
+ reasonCodes: [error.reasonCode],
53
+ error: error.message,
54
+ };
55
+ }
56
+
57
+ export function main(argv = process.argv.slice(2)) {
58
+ try {
59
+ const options = parseArguments(argv);
60
+ if (options.releaseFamilyMarker) {
61
+ console.log(
62
+ JSON.stringify(
63
+ releaseFamilyQueueLease(
64
+ markerText(options.releaseFamilyMarker),
65
+ options,
66
+ ),
67
+ ),
68
+ );
69
+ return 0;
70
+ }
71
+ if (!options.base || !options.head)
72
+ throw new Error("--base and --head are required");
73
+ console.log(
74
+ JSON.stringify(qualifyProjectCut({ cwd: process.cwd(), ...options })),
75
+ );
76
+ return 0;
77
+ } catch (error) {
78
+ if (error instanceof ProjectCutAdmissionError) {
79
+ console.log(JSON.stringify(repairReceipt(error)));
80
+ return 1;
81
+ }
82
+ console.error(`project-cut admission error: ${error.message}`);
83
+ return 2;
84
+ }
85
+ }
86
+
87
+ if (import.meta.url === `file://${process.argv[1]}`) process.exitCode = main();