@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.
- package/README.md +9 -6
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +70 -1
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +156 -11
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +480 -129
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +123 -32
- package/dist/site/buildchain-site.json +96 -28
- package/dist/site/capability-registry.json +6 -6
- package/dist/site/cli-registry.json +5 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +170 -21
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +1474 -215
- package/dist/site/page-registry.json +85 -17
- package/dist/site/public-surface-audit.json +85 -20
- package/dist/site/publication-authority-registry.json +28 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +94 -18
- package/docs/MAP.md +1 -1
- package/docs/cli-reference.md +4 -4
- package/docs/dev-delivery-warrant.md +12 -9
- package/docs/node-api-reference.md +291 -206
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/v4-adopter-delivery.md +23 -4
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/package.json +8 -3
- package/packages/core/adopter-delivery-passport.js +36 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +21 -4
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/index.js +1 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/release-candidate-recovery.js +6 -5
- package/packages/core/release-passport.js +82 -184
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-floating-consumer-policy.js +3 -2
- package/packages/core/v4-floating-consumer-release-passport.js +134 -15
- package/packages/core/v4-publication-qualification.js +222 -0
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +23 -5
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant.mjs +45 -5
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-release-candidate-passport.mjs +26 -1
- package/scripts/generate-site-bundle.mjs +6 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +19 -2
- package/scripts/resume-from-candidate-run.mjs +106 -103
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +4 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- 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,
|