@kungfu-tech/buildchain 4.0.1-alpha.8 → 4.0.1

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 (98) hide show
  1. package/README.md +9 -6
  2. package/actions/v4-release-candidate-promote/README.md +3 -0
  3. package/architecture/agent-change-map.md +1015 -0
  4. package/architecture/ci-lane-change-budget.json +70 -1
  5. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  6. package/architecture/internal-capabilities.json +156 -11
  7. package/architecture/maintainability-debt.json +3229 -0
  8. package/architecture/maintainability-policy.json +480 -129
  9. package/architecture/v3-core-mechanism-inventory.json +4 -3
  10. package/architecture/v3-v4-capability-closure.md +59 -0
  11. package/architecture/v3-v4-live-capability-inventory.json +102404 -0
  12. package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
  13. package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
  14. package/architecture/v4-runtime-semantic-closure.json +710 -0
  15. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  16. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  17. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  18. package/dist/site/agent-index.json +1 -0
  19. package/dist/site/artifact-schemas.json +2 -0
  20. package/dist/site/buildchain-contract.json +123 -32
  21. package/dist/site/buildchain-site.json +96 -28
  22. package/dist/site/capability-registry.json +6 -6
  23. package/dist/site/cli-registry.json +5 -2
  24. package/dist/site/controller-registry.json +41 -5
  25. package/dist/site/kfd-claims.json +170 -21
  26. package/dist/site/kfd-upstream-aggregate.json +1 -1
  27. package/dist/site/manual-registry.json +6 -6
  28. package/dist/site/node-api-registry.json +1474 -215
  29. package/dist/site/page-registry.json +85 -17
  30. package/dist/site/public-surface-audit.json +85 -20
  31. package/dist/site/publication-authority-registry.json +28 -1
  32. package/dist/site/publication-registry.json +4 -4
  33. package/dist/site/release-provenance.json +5 -0
  34. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  35. package/dist/site/site-manifest.json +10 -10
  36. package/dist/site/workflow-registry.json +94 -18
  37. package/docs/MAP.md +1 -1
  38. package/docs/cli-reference.md +4 -4
  39. package/docs/dev-delivery-warrant.md +12 -9
  40. package/docs/node-api-reference.md +291 -206
  41. package/docs/publish-transaction.md +11 -4
  42. package/docs/release-candidate.md +7 -0
  43. package/docs/v4-adopter-delivery.md +23 -4
  44. package/docs/v4-declarative-release-tail-migration.md +43 -0
  45. package/package.json +8 -3
  46. package/packages/core/adopter-delivery-passport.js +36 -0
  47. package/packages/core/buildchain-contract.js +31 -1
  48. package/packages/core/buildchain-publication-authority.js +1 -0
  49. package/packages/core/dev-delivery-warrant-settlement.js +239 -3
  50. package/packages/core/dev-delivery-warrant-state.js +15 -2
  51. package/packages/core/dev-delivery-warrant.js +21 -4
  52. package/packages/core/github-governance-authority.js +4 -4
  53. package/packages/core/index.js +1 -0
  54. package/packages/core/paper-scaffold-content.js +401 -0
  55. package/packages/core/paper.js +12 -378
  56. package/packages/core/publication-control-plane-audit.js +135 -111
  57. package/packages/core/publication-rehearsal-projection.js +104 -0
  58. package/packages/core/publication-rehearsal-runtime.js +190 -0
  59. package/packages/core/release-candidate-recovery.js +6 -5
  60. package/packages/core/release-passport.js +82 -184
  61. package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
  62. package/packages/core/v4-floating-consumer-policy.js +3 -2
  63. package/packages/core/v4-floating-consumer-release-passport.js +134 -15
  64. package/packages/core/v4-publication-qualification.js +222 -0
  65. package/scripts/artifact-signing-controller-core.mjs +1 -1
  66. package/scripts/audit-publication-control-plane.mjs +17 -17
  67. package/scripts/buildchain-cli-help.mjs +3 -0
  68. package/scripts/check-internal-architecture.mjs +23 -3
  69. package/scripts/check-inventory.mjs +23 -5
  70. package/scripts/check-maintainability.mjs +145 -4
  71. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  72. package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
  73. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
  74. package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
  75. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  76. package/scripts/dev-delivery-warrant.mjs +45 -5
  77. package/scripts/dev-pr-auto-merge.mjs +25 -11
  78. package/scripts/dev-pr-delivery-warrant.mjs +44 -0
  79. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  80. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  81. package/scripts/generate-agent-change-map.mjs +118 -0
  82. package/scripts/generate-release-candidate-passport.mjs +26 -1
  83. package/scripts/generate-site-bundle.mjs +6 -0
  84. package/scripts/maintainability-domain-metrics.mjs +237 -0
  85. package/scripts/maintainability-governance.mjs +473 -0
  86. package/scripts/maintainability-metrics.mjs +28 -6
  87. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  88. package/scripts/promotion-routing-evidence.mjs +35 -6
  89. package/scripts/release-candidate-resolver.mjs +44 -0
  90. package/scripts/release-line-policy.mjs +25 -15
  91. package/scripts/release-tail.mjs +19 -2
  92. package/scripts/resume-from-candidate-run.mjs +106 -103
  93. package/scripts/run-lifecycle-core.mjs +341 -376
  94. package/scripts/site-capability-metadata.mjs +4 -0
  95. package/scripts/stable-candidate-qualification.mjs +9 -10
  96. package/scripts/v3-v4-capability-catalog.mjs +439 -0
  97. package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
  98. package/scripts/v4-declarative-promotion-admission.mjs +42 -0
@@ -0,0 +1,473 @@
1
+ function maximumFunction(metrics, field) {
2
+ return Math.max(0, ...(metrics.functions || []).map((entry) => entry[field]));
3
+ }
4
+ function violatedFunctions(metrics, budgets) {
5
+ return Object.fromEntries(
6
+ (metrics.functions || [])
7
+ .filter(
8
+ (entry) =>
9
+ entry.lines > budgets.newFunctionLines ||
10
+ entry.complexity > budgets.newFunctionComplexity,
11
+ )
12
+ .map((entry) => [
13
+ `${entry.name}@${entry.start}`,
14
+ Object.fromEntries(
15
+ [
16
+ ["lines", entry.lines, budgets.newFunctionLines],
17
+ ["complexity", entry.complexity, budgets.newFunctionComplexity],
18
+ ]
19
+ .filter(([, value, limit]) => value > limit)
20
+ .map(([key, value]) => [key, value]),
21
+ ),
22
+ ]),
23
+ );
24
+ }
25
+ function debtSurfaces(current, policy) {
26
+ const collectCode = (files, budgets) =>
27
+ Object.fromEntries(
28
+ Object.entries(files || {}).flatMap(([file, metrics]) => {
29
+ const functions = violatedFunctions(metrics, budgets);
30
+ const measured = {
31
+ ...(metrics.lines > budgets.newFileLines
32
+ ? { lines: metrics.lines }
33
+ : {}),
34
+ ...(Object.keys(functions).length ? { functions } : {}),
35
+ };
36
+ return Object.keys(measured).length ? [[file, measured]] : [];
37
+ }),
38
+ );
39
+ const workflowFields = [
40
+ ["lines", "maxLines"],
41
+ ["jobs", "maxJobs"],
42
+ ["steps", "maxSteps"],
43
+ ["maxStepsPerJob", "maxStepsPerJob"],
44
+ ["decisions", "maxDecisions"],
45
+ ];
46
+ return {
47
+ sources: collectCode(
48
+ Object.fromEntries(
49
+ Object.entries(current.files || {}).filter(
50
+ ([file]) => !file.endsWith(".rs"),
51
+ ),
52
+ ),
53
+ policy.sourceBudgets,
54
+ ),
55
+ rust: collectCode(
56
+ Object.fromEntries(
57
+ Object.entries(current.files || {}).filter(([file]) =>
58
+ file.endsWith(".rs"),
59
+ ),
60
+ ),
61
+ policy.rustBudgets,
62
+ ),
63
+ tests: collectCode(current.tests, policy.testBudgets),
64
+ workflows: Object.fromEntries(
65
+ Object.entries(current.workflows || {}).flatMap(([file, metrics]) => {
66
+ const measured = Object.fromEntries(
67
+ workflowFields
68
+ .filter(
69
+ ([metric, limit]) =>
70
+ metrics[metric] > policy.workflowBudgets[limit],
71
+ )
72
+ .map(([metric]) => [metric, metrics[metric]]),
73
+ );
74
+ return Object.keys(measured).length ? [[file, measured]] : [];
75
+ }),
76
+ ),
77
+ };
78
+ }
79
+ function metricLeaves(value, prefix = "") {
80
+ return Object.entries(value || {}).flatMap(([key, child]) => {
81
+ const path = prefix ? `${prefix}.${key}` : key;
82
+ return child && typeof child === "object"
83
+ ? metricLeaves(child, path)
84
+ : [[path, child]];
85
+ });
86
+ }
87
+ function metricAt(value, metric) {
88
+ return metric.split(".").reduce((current, key) => current?.[key], value);
89
+ }
90
+ function calculateDebtMeasuredExcess(debt) {
91
+ return Object.values(debt?.surfaces || {}).reduce(
92
+ (domainTotal, surfaces) =>
93
+ domainTotal +
94
+ Object.values(surfaces || {}).reduce(
95
+ (surfaceTotal, entry) =>
96
+ surfaceTotal +
97
+ metricLeaves(entry.current).reduce((metricTotal, [metric, value]) => {
98
+ const target = metricAt(entry.target, metric);
99
+ return (
100
+ metricTotal +
101
+ (Number.isFinite(value) && Number.isFinite(target)
102
+ ? Math.max(0, value - target)
103
+ : 0)
104
+ );
105
+ }, 0),
106
+ 0,
107
+ ),
108
+ 0,
109
+ );
110
+ }
111
+ function evaluateDebtBudget({ debt, policy }) {
112
+ const budget = policy.debtBudget;
113
+ if (!budget) return [];
114
+ const issues = [];
115
+ for (const field of [
116
+ "baselineMeasuredExcess",
117
+ "baselineTotalExcess",
118
+ "maxTotalExcess",
119
+ "targetExclusive",
120
+ ]) {
121
+ if (!Number.isInteger(budget[field]) || budget[field] < 0) {
122
+ issues.push(`debt budget ${field} must be a non-negative integer`);
123
+ }
124
+ }
125
+ if (!String(budget.baselineProtectedHead || "").trim()) {
126
+ issues.push("debt budget baselineProtectedHead is required");
127
+ }
128
+ if (issues.length) return issues;
129
+ if (budget.maxTotalExcess > budget.baselineTotalExcess) {
130
+ issues.push("debt budget maximum cannot exceed its frozen baseline");
131
+ }
132
+ if (budget.targetExclusive > budget.baselineTotalExcess) {
133
+ issues.push("debt budget target cannot exceed its frozen baseline");
134
+ }
135
+ const measured = calculateDebtMeasuredExcess(debt);
136
+ const total =
137
+ budget.baselineTotalExcess + measured - budget.baselineMeasuredExcess;
138
+ const ledger = debt.totalExcessLedger || {};
139
+ if (ledger.measuredExcess !== measured || ledger.current !== total) {
140
+ issues.push(
141
+ `debt total-excess ledger is stale: measured ${measured}, calibrated ${total}`,
142
+ );
143
+ }
144
+ if (total > budget.maxTotalExcess) {
145
+ issues.push(
146
+ `maintainability debt total excess is ${total}; frozen maximum is ${budget.maxTotalExcess}`,
147
+ );
148
+ }
149
+ return issues;
150
+ }
151
+ function evaluateDebtEntry(domain, path, entry, expected) {
152
+ if (JSON.stringify(entry.current) !== JSON.stringify(expected))
153
+ return [`${domain}:${path}: current measurement is stale`];
154
+ return metricLeaves(entry.current).flatMap(([metric, measured]) => {
155
+ const baseline = metricAt(entry.baseline, metric),
156
+ target = metricAt(entry.target, metric);
157
+ return [
158
+ ...(!Number.isFinite(baseline) || measured > baseline
159
+ ? [`${domain}:${path}:${metric}: debt widened beyond baseline`]
160
+ : []),
161
+ ...(!Number.isFinite(target) || target >= measured
162
+ ? [`${domain}:${path}:${metric}: target must be lower than current`]
163
+ : []),
164
+ ];
165
+ });
166
+ }
167
+ function evaluateDebtAuthority({
168
+ current,
169
+ policy,
170
+ debt,
171
+ capabilityIds,
172
+ hotspots = [],
173
+ }) {
174
+ const issues = [];
175
+ if (debt?.schemaVersion !== 1)
176
+ return ["maintainability debt schemaVersion must be 1"];
177
+ const defaults = debt.defaults || {};
178
+ for (const field of [
179
+ "owner",
180
+ "capability",
181
+ "expiry",
182
+ "impact",
183
+ "recovery",
184
+ "stopCondition",
185
+ ]) {
186
+ if (!String(defaults[field] || "").trim())
187
+ issues.push(`debt defaults.${field} is required`);
188
+ }
189
+ if (!capabilityIds.has(defaults.capability)) {
190
+ issues.push(
191
+ `debt capability is unmapped: ${defaults.capability || "<empty>"}`,
192
+ );
193
+ }
194
+ const expiry = String(defaults.expiry || "");
195
+ if (
196
+ !/^\d{4}-\d{2}-\d{2}$/u.test(expiry) ||
197
+ expiry < new Date().toISOString().slice(0, 10)
198
+ ) {
199
+ issues.push(`debt expiry is invalid or stale: ${expiry || "<empty>"}`);
200
+ }
201
+ const expected = debtSurfaces(current, policy);
202
+ for (const domain of Object.keys(expected)) {
203
+ const governed = debt.surfaces?.[domain] || {};
204
+ const expectedPaths = Object.keys(expected[domain]).sort();
205
+ const governedPaths = Object.keys(governed).sort();
206
+ for (const path of expectedPaths.filter(
207
+ (entry) => !governedPaths.includes(entry),
208
+ ))
209
+ issues.push(`${domain}:${path}: oversized surface is undeclared`);
210
+ for (const path of governedPaths.filter(
211
+ (entry) => !expectedPaths.includes(entry),
212
+ ))
213
+ issues.push(`${domain}:${path}: debt entry is stale`);
214
+ for (const path of expectedPaths.filter((entry) =>
215
+ governedPaths.includes(entry),
216
+ )) {
217
+ issues.push(
218
+ ...evaluateDebtEntry(
219
+ domain,
220
+ path,
221
+ governed[path],
222
+ expected[domain][path],
223
+ ),
224
+ );
225
+ }
226
+ }
227
+ const declaredHotspots = debt.hotspots || [];
228
+ for (const path of hotspots.filter(
229
+ (entry) => !declaredHotspots.includes(entry),
230
+ ))
231
+ issues.push(`hotspot:${path}: change route is undeclared`);
232
+ for (const slice of debt.burnDownSlices || []) {
233
+ const measured = metricAt(
234
+ expected[slice.domain]?.[slice.path],
235
+ slice.metric,
236
+ );
237
+ if (!Number.isFinite(measured) || measured !== slice.current) {
238
+ issues.push(
239
+ `burn-down:${slice.path}:${slice.metric}: current aggregate is stale`,
240
+ );
241
+ } else if (
242
+ !Number.isFinite(slice.baseline) ||
243
+ slice.current >= slice.baseline
244
+ ) {
245
+ issues.push(
246
+ `burn-down:${slice.path}:${slice.metric}: split-only change did not reduce aggregate debt`,
247
+ );
248
+ }
249
+ }
250
+ return issues;
251
+ }
252
+ const exceptionCollections = [
253
+ "approvedExistingDebtTransitions",
254
+ "approvedNewFileTransitions",
255
+ "approvedExtractedDebt",
256
+ "approvedPublicSurfaceTransitions",
257
+ "approvedTestDebtTransitions",
258
+ "approvedWorkflowDebtTransitions",
259
+ ];
260
+ function evaluateExceptionBudget({ policy }) {
261
+ const budget = policy.exceptionBudget;
262
+ if (!budget) return [];
263
+ const issues = [];
264
+ let total = 0;
265
+ for (const collection of exceptionCollections) {
266
+ const count = Object.keys(policy[collection] || {}).length;
267
+ total += count;
268
+ const maximum = budget.maxByCollection?.[collection];
269
+ if (!Number.isInteger(maximum) || maximum < 0) {
270
+ issues.push(
271
+ `exception budget ${collection} must be a non-negative integer`,
272
+ );
273
+ } else if (count > maximum) {
274
+ issues.push(
275
+ `${collection} has ${count} exceptions; frozen maximum is ${maximum}`,
276
+ );
277
+ }
278
+ }
279
+ if (!Number.isInteger(budget.maxTotal) || budget.maxTotal < 0) {
280
+ issues.push("exception budget maxTotal must be a non-negative integer");
281
+ } else if (total > budget.maxTotal) {
282
+ issues.push(
283
+ `maintainability exceptions total ${total}; frozen maximum is ${budget.maxTotal}`,
284
+ );
285
+ }
286
+ return issues;
287
+ }
288
+ function exceptionGovernanceIssues(label, approval, policy, now) {
289
+ const issues = [];
290
+ const reference = approval?.governance;
291
+ const governance =
292
+ typeof reference === "string"
293
+ ? policy.exceptionGovernanceProfiles?.[reference]
294
+ : reference;
295
+ if (!governance || typeof governance !== "object") {
296
+ return [`${label}: maintainability exception requires governance`];
297
+ }
298
+ if (governance.mode === "expiry") {
299
+ const expiresOn = String(governance.expiresOn || "");
300
+ const parsedExpiry = new Date(`${expiresOn}T00:00:00Z`);
301
+ if (
302
+ !/^\d{4}-\d{2}-\d{2}$/u.test(expiresOn) ||
303
+ Number.isNaN(parsedExpiry.valueOf()) ||
304
+ parsedExpiry.toISOString().slice(0, 10) !== expiresOn
305
+ ) {
306
+ issues.push(`${label}: exception expiry must be YYYY-MM-DD`);
307
+ } else if (expiresOn < now.toISOString().slice(0, 10)) {
308
+ issues.push(
309
+ `${label}: maintainability exception expired on ${expiresOn}`,
310
+ );
311
+ }
312
+ if (!String(governance.owner || "").trim()) {
313
+ issues.push(`${label}: expiring exception requires an owner`);
314
+ }
315
+ if (!String(governance.followUp || "").trim()) {
316
+ issues.push(`${label}: expiring exception requires a follow-up`);
317
+ }
318
+ return issues;
319
+ }
320
+ if (governance.mode === "net-debt-reduction") {
321
+ const metrics = governance.metrics;
322
+ if (
323
+ !metrics ||
324
+ typeof metrics !== "object" ||
325
+ Object.keys(metrics).length === 0
326
+ ) {
327
+ return [`${label}: net debt reduction requires bounded metrics`];
328
+ }
329
+ for (const [metric, reduction] of Object.entries(metrics)) {
330
+ if (
331
+ !Number.isFinite(reduction?.baseline) ||
332
+ !Number.isFinite(reduction?.target) ||
333
+ reduction.target >= reduction.baseline
334
+ ) {
335
+ issues.push(
336
+ `${label}: ${metric} target must be lower than its baseline`,
337
+ );
338
+ }
339
+ if (!Number.isFinite(approval?.[metric])) {
340
+ issues.push(
341
+ `${label}: ${metric} reduction requires a matching exception ceiling`,
342
+ );
343
+ } else if (approval[metric] > reduction.target) {
344
+ issues.push(
345
+ `${label}: ${metric} ceiling exceeds its debt-reduction target`,
346
+ );
347
+ }
348
+ }
349
+ return issues;
350
+ }
351
+ return [
352
+ `${label}: exception governance mode must be expiry or net-debt-reduction`,
353
+ ];
354
+ }
355
+ function evaluateExceptionGovernance({ policy, now = new Date() }) {
356
+ const issues = [];
357
+ for (const collection of exceptionCollections) {
358
+ for (const [key, approval] of Object.entries(policy[collection] || {})) {
359
+ issues.push(
360
+ ...exceptionGovernanceIssues(
361
+ `${collection}:${key}`,
362
+ approval,
363
+ policy,
364
+ now,
365
+ ),
366
+ );
367
+ }
368
+ }
369
+ if (policy.repositoryBudgets) {
370
+ issues.push(
371
+ ...exceptionGovernanceIssues(
372
+ "repositoryBudgets",
373
+ policy.repositoryBudgets,
374
+ policy,
375
+ now,
376
+ ),
377
+ );
378
+ }
379
+ return issues;
380
+ }
381
+ function evaluateTestBudgets({ current, baselineFiles, policy }) {
382
+ const issues = [];
383
+ const budgets = policy.testBudgets;
384
+ for (const [file, metrics] of Object.entries(current.tests || {})) {
385
+ const baseline = baselineFiles[file];
386
+ const transition = policy.approvedTestDebtTransitions?.[file];
387
+ if (transition && !String(transition.rationale || "").trim()) {
388
+ issues.push(`${file}: approved test transition requires a rationale`);
389
+ }
390
+ const limits = baseline
391
+ ? {
392
+ lines:
393
+ transition?.maxLines ??
394
+ Math.max(baseline.lines, budgets.newFileLines),
395
+ functionLines:
396
+ transition?.maxFunctionLines ??
397
+ Math.max(
398
+ maximumFunction(baseline, "lines"),
399
+ budgets.newFunctionLines,
400
+ ),
401
+ functionComplexity:
402
+ transition?.maxFunctionComplexity ??
403
+ Math.max(
404
+ maximumFunction(baseline, "complexity"),
405
+ budgets.newFunctionComplexity,
406
+ ),
407
+ }
408
+ : {
409
+ lines: transition?.maxLines ?? budgets.newFileLines,
410
+ functionLines:
411
+ transition?.maxFunctionLines ?? budgets.newFunctionLines,
412
+ functionComplexity:
413
+ transition?.maxFunctionComplexity ?? budgets.newFunctionComplexity,
414
+ };
415
+ if (metrics.lines > limits.lines) {
416
+ issues.push(
417
+ `${file}: test file has ${metrics.lines} lines; budget is ${limits.lines}`,
418
+ );
419
+ }
420
+ if (maximumFunction(metrics, "lines") > limits.functionLines) {
421
+ issues.push(
422
+ `${file}: maximum test function length exceeds ${limits.functionLines}`,
423
+ );
424
+ }
425
+ if (maximumFunction(metrics, "complexity") > limits.functionComplexity) {
426
+ issues.push(
427
+ `${file}: maximum test function complexity exceeds ${limits.functionComplexity}`,
428
+ );
429
+ }
430
+ }
431
+ return issues;
432
+ }
433
+ function evaluateWorkflowBudgets({ current, baselineFiles, policy }) {
434
+ const issues = [];
435
+ const budgets = policy.workflowBudgets;
436
+ const fields = [
437
+ ["lines", "maxLines"],
438
+ ["jobs", "maxJobs"],
439
+ ["steps", "maxSteps"],
440
+ ["maxStepsPerJob", "maxStepsPerJob"],
441
+ ["decisions", "maxDecisions"],
442
+ ];
443
+ for (const [file, metrics] of Object.entries(current.workflows || {})) {
444
+ const baseline = baselineFiles[file];
445
+ const transition = policy.approvedWorkflowDebtTransitions?.[file];
446
+ if (transition && !String(transition.rationale || "").trim()) {
447
+ issues.push(`${file}: approved workflow transition requires a rationale`);
448
+ }
449
+ for (const [metric, ceiling] of fields) {
450
+ const allowed =
451
+ transition?.[ceiling] ??
452
+ (baseline
453
+ ? Math.max(baseline[metric], budgets[ceiling])
454
+ : budgets[ceiling]);
455
+ if (metrics[metric] > allowed) {
456
+ issues.push(
457
+ `${file}: workflow ${metric} is ${metrics[metric]}; budget is ${allowed}`,
458
+ );
459
+ }
460
+ }
461
+ }
462
+ return issues;
463
+ }
464
+ export {
465
+ calculateDebtMeasuredExcess,
466
+ debtSurfaces,
467
+ evaluateDebtAuthority,
468
+ evaluateDebtBudget,
469
+ evaluateExceptionBudget,
470
+ evaluateExceptionGovernance,
471
+ evaluateTestBudgets,
472
+ evaluateWorkflowBudgets,
473
+ };
@@ -9,6 +9,10 @@ import {
9
9
  dependencyCycles,
10
10
  relativeImports,
11
11
  } from "./check-internal-architecture.mjs";
12
+ import {
13
+ analyzeRust,
14
+ analyzeWorkflow,
15
+ } from "./maintainability-domain-metrics.mjs";
12
16
 
13
17
  const JS_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
14
18
  const HAND_MAINTAINED_EXTENSIONS = new Set([...JS_EXTENSIONS, ".rs"]);
@@ -163,6 +167,12 @@ function analyzeJavaScript(file, source) {
163
167
  };
164
168
  }
165
169
 
170
+ function analyzeSource(file, source) {
171
+ return path.extname(file) === ".rs"
172
+ ? analyzeRust(file, source)
173
+ : analyzeJavaScript(file, source);
174
+ }
175
+
166
176
  function countRegistryEntries(root, file, field, revision = "") {
167
177
  const value = JSON.parse(readTrackedFile(root, file, revision));
168
178
  return Array.isArray(value[field]) ? value[field].length : 0;
@@ -261,14 +271,21 @@ function collectMaintainabilityMetrics({
261
271
  const sourceMetrics = Object.fromEntries(
262
272
  sourceFiles.map((file) => {
263
273
  const source = readTrackedFile(root, file, revision);
264
- return [
265
- file,
266
- JS_EXTENSIONS.has(path.extname(file))
267
- ? analyzeJavaScript(file, source)
268
- : { lines: lineCount(source), complexity: 0, functions: [] },
269
- ];
274
+ return [file, analyzeSource(file, source)];
270
275
  }),
271
276
  );
277
+ const testMetrics = Object.fromEntries(
278
+ testFiles.map((file) => [
279
+ file,
280
+ analyzeJavaScript(file, readTrackedFile(root, file, revision)),
281
+ ]),
282
+ );
283
+ const workflowMetrics = Object.fromEntries(
284
+ workflowFiles.map((file) => [
285
+ file,
286
+ analyzeWorkflow(file, readTrackedFile(root, file, revision)),
287
+ ]),
288
+ );
272
289
  const sumLines = (entries) =>
273
290
  entries.reduce(
274
291
  (total, file) => total + lineCount(readTrackedFile(root, file, revision)),
@@ -347,6 +364,8 @@ function collectMaintainabilityMetrics({
347
364
  },
348
365
  },
349
366
  files: sourceMetrics,
367
+ tests: testMetrics,
368
+ workflows: workflowMetrics,
350
369
  };
351
370
  }
352
371
 
@@ -359,6 +378,9 @@ if (
359
378
 
360
379
  export {
361
380
  analyzeJavaScript,
381
+ analyzeRust,
382
+ analyzeSource,
383
+ analyzeWorkflow,
362
384
  collectMaintainabilityMetrics,
363
385
  isHandMaintainedSource,
364
386
  lineCount,