@kungfu-tech/buildchain 3.0.4-alpha.2 → 3.0.4-alpha.4
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/bin/buildchain.mjs +126 -72
- package/bin/internal/trust-release-cli.mjs +15 -537
- package/bin/internal/trust-release-command-handlers.mjs +14 -0
- package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
- package/bin/internal/trust-release-release-handlers.mjs +317 -0
- package/bin/internal/trust-release-verification-handlers.mjs +306 -0
- package/dist/site/buildchain-contract.json +35 -24
- package/dist/site/buildchain-site.json +22 -10
- package/dist/site/controller-registry.json +16 -3
- package/dist/site/kfd-claims.json +9 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +7 -7
- package/dist/site/page-registry.json +9 -4
- package/dist/site/public-surface-audit.json +56 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +7 -0
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +13 -5
- package/docs/MAP.md +1 -1
- package/docs/release-propagation.md +166 -8
- package/package.json +1 -1
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/controller-evidence.js +8 -1
- package/packages/core/index.js +1 -13
- package/packages/core/paper-agent-entry.js +10 -11
- package/packages/core/paper-npm-bootstrap.js +492 -0
- package/packages/core/paper.js +271 -669
- package/packages/core/public-surface-cli.js +12 -1
- package/packages/core/release-passport.js +67 -71
- package/packages/core/release-propagation-common.js +64 -0
- package/packages/core/release-propagation-execution-profile.js +59 -0
- package/packages/core/release-propagation-release.js +196 -0
- package/packages/core/release-propagation-stage-evidence.js +364 -0
- package/packages/core/release-propagation-work-capture.js +64 -0
- package/packages/core/release-propagation-work-constants.js +34 -0
- package/packages/core/release-propagation-work-control.js +203 -0
- package/packages/core/release-propagation-work-transitions.js +145 -0
- package/packages/core/release-propagation-work.js +517 -0
- package/packages/core/release-propagation.js +34 -158
- package/scripts/aws-windows-jit-controller-core.mjs +269 -0
- package/scripts/aws-windows-jit-controller.mjs +502 -0
- package/scripts/check-internal-architecture.mjs +178 -52
- package/scripts/check-maintainability.mjs +76 -17
- package/scripts/generate-site-bundle.mjs +16 -7
- package/scripts/maintainability-metrics.mjs +24 -4
- package/scripts/release-propagation.mjs +126 -0
- package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
- package/scripts/web-surface-core.mjs +46 -207
- package/scripts/web-surface-routing.mjs +286 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
6
7
|
|
|
7
8
|
const implementationExtensions = new Set([".js", ".mjs", ".cjs", ".sh"]);
|
|
@@ -9,7 +10,10 @@ const importPattern =
|
|
|
9
10
|
/(?:\bimport\s*(?:\([^)]*?\)|[^"'\n]*?\s+from\s+)?|\bexport\s+[^"'\n]*?\s+from\s+)(["'])([^"'\n]+)\1/g;
|
|
10
11
|
|
|
11
12
|
function normalizeRelative(root, value) {
|
|
12
|
-
return path
|
|
13
|
+
return path
|
|
14
|
+
.relative(root, path.resolve(root, value))
|
|
15
|
+
.split(path.sep)
|
|
16
|
+
.join("/");
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
function isInside(relativePath, prefix) {
|
|
@@ -22,11 +26,15 @@ function collectImplementationFiles(root, entry) {
|
|
|
22
26
|
throw new Error(`internal architecture path is missing: ${entry}`);
|
|
23
27
|
}
|
|
24
28
|
if (fs.statSync(absolute).isFile()) {
|
|
25
|
-
return implementationExtensions.has(path.extname(absolute))
|
|
29
|
+
return implementationExtensions.has(path.extname(absolute))
|
|
30
|
+
? [absolute]
|
|
31
|
+
: [];
|
|
26
32
|
}
|
|
27
|
-
return fs
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
return fs
|
|
34
|
+
.readdirSync(absolute, { withFileTypes: true })
|
|
35
|
+
.flatMap((item) =>
|
|
36
|
+
collectImplementationFiles(root, path.join(entry, item.name)),
|
|
37
|
+
);
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
function relativeImports(source) {
|
|
@@ -38,7 +46,10 @@ function relativeImports(source) {
|
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
function resolveImportTarget(root, sourcePath, specifier) {
|
|
41
|
-
const absolute = path.resolve(
|
|
49
|
+
const absolute = path.resolve(
|
|
50
|
+
path.dirname(path.resolve(root, sourcePath)),
|
|
51
|
+
specifier,
|
|
52
|
+
);
|
|
42
53
|
return normalizeRelative(root, absolute);
|
|
43
54
|
}
|
|
44
55
|
|
|
@@ -65,10 +76,9 @@ function dependencyCycles(graph) {
|
|
|
65
76
|
const start = stack.indexOf(node);
|
|
66
77
|
const cycle = [...stack.slice(start), node];
|
|
67
78
|
const members = cycle.slice(0, -1);
|
|
68
|
-
const rotations = members.map((_, index) =>
|
|
69
|
-
...members.slice(index),
|
|
70
|
-
|
|
71
|
-
].join(" -> "));
|
|
79
|
+
const rotations = members.map((_, index) =>
|
|
80
|
+
[...members.slice(index), ...members.slice(0, index)].join(" -> "),
|
|
81
|
+
);
|
|
72
82
|
const key = rotations.sort()[0];
|
|
73
83
|
if (!canonical.has(key)) {
|
|
74
84
|
canonical.add(key);
|
|
@@ -92,38 +102,106 @@ function assertIndexShape(index) {
|
|
|
92
102
|
if (index?.schemaVersion !== 1) {
|
|
93
103
|
throw new Error("internal architecture index schemaVersion must be 1");
|
|
94
104
|
}
|
|
95
|
-
for (const field of [
|
|
105
|
+
for (const field of [
|
|
106
|
+
"coverageRoots",
|
|
107
|
+
"dependencyRules",
|
|
108
|
+
"ownershipRules",
|
|
109
|
+
"capabilities",
|
|
110
|
+
]) {
|
|
96
111
|
if (!Array.isArray(index[field]) || index[field].length === 0) {
|
|
97
|
-
throw new Error(
|
|
112
|
+
throw new Error(
|
|
113
|
+
`internal architecture index requires non-empty ${field}`,
|
|
114
|
+
);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
function repositoryJavaScriptFiles(root) {
|
|
120
|
+
return execFileSync(
|
|
121
|
+
"git",
|
|
122
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
123
|
+
{
|
|
124
|
+
cwd: root,
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
.split("\0")
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.filter((file) => fs.existsSync(path.resolve(root, file)))
|
|
131
|
+
.filter((file) => [".js", ".mjs", ".cjs"].includes(path.extname(file)))
|
|
132
|
+
.filter((file) => !file.startsWith("tests/"))
|
|
133
|
+
.filter((file) => !/(?:^|\/)tests?\//u.test(file))
|
|
134
|
+
.filter((file) => !/^actions\/[^/]+\/dist\//u.test(file))
|
|
135
|
+
.sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ownershipFor(index, sourcePath) {
|
|
139
|
+
return index.ownershipRules.filter((rule) =>
|
|
140
|
+
(rule.paths || []).some((prefix) => isInside(sourcePath, prefix)),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function evaluateSourceOwnership(index, repositorySources) {
|
|
145
|
+
const issues = [];
|
|
146
|
+
const exclusions = new Map(
|
|
147
|
+
(index.ownershipExclusions || []).map((entry) => [entry.path, entry]),
|
|
148
|
+
);
|
|
149
|
+
let ownedSources = 0;
|
|
150
|
+
let excludedSources = 0;
|
|
151
|
+
for (const sourcePath of repositorySources) {
|
|
152
|
+
const owners = ownershipFor(index, sourcePath);
|
|
153
|
+
const exclusion = exclusions.get(sourcePath);
|
|
154
|
+
if (owners.length === 1 && !exclusion) {
|
|
155
|
+
if (!String(owners[0].owner || "").trim()) {
|
|
156
|
+
issues.push(`${owners[0].id}: ownership rule owner is empty`);
|
|
157
|
+
} else {
|
|
158
|
+
ownedSources += 1;
|
|
159
|
+
}
|
|
160
|
+
} else if (owners.length === 0 && exclusion) {
|
|
161
|
+
if (!String(exclusion.rationale || "").trim()) {
|
|
162
|
+
issues.push(`${sourcePath}: ownership exclusion requires a rationale`);
|
|
163
|
+
} else {
|
|
164
|
+
excludedSources += 1;
|
|
165
|
+
}
|
|
166
|
+
} else if (owners.length === 0) {
|
|
167
|
+
issues.push(`repository source has no owner: ${sourcePath}`);
|
|
168
|
+
} else if (owners.length > 1) {
|
|
169
|
+
issues.push(
|
|
170
|
+
`repository source has multiple owners: ${sourcePath} (${owners.map((entry) => entry.id).join(", ")})`,
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
issues.push(
|
|
174
|
+
`repository source is both owned and excluded: ${sourcePath}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const [sourcePath] of exclusions) {
|
|
179
|
+
if (!repositorySources.includes(sourcePath)) {
|
|
180
|
+
issues.push(`ownership exclusion is stale: ${sourcePath}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { issues, ownedSources, excludedSources };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function evaluateCapabilities(root, index) {
|
|
113
187
|
const issues = [];
|
|
114
188
|
const capabilityIds = new Set();
|
|
115
189
|
const coveredImplementation = new Map();
|
|
116
|
-
|
|
117
190
|
for (const capability of index.capabilities) {
|
|
118
191
|
if (!capability?.id || capabilityIds.has(capability.id)) {
|
|
119
|
-
issues.push(
|
|
192
|
+
issues.push(
|
|
193
|
+
`capability id must be present and unique: ${capability?.id || "<empty>"}`,
|
|
194
|
+
);
|
|
120
195
|
continue;
|
|
121
196
|
}
|
|
122
197
|
capabilityIds.add(capability.id);
|
|
123
198
|
if (!capability.owner || typeof capability.owner !== "string") {
|
|
124
199
|
issues.push(`${capability.id}: owner is empty`);
|
|
125
200
|
}
|
|
126
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
!Array.isArray(capability.implementation) ||
|
|
203
|
+
capability.implementation.length === 0
|
|
204
|
+
) {
|
|
127
205
|
issues.push(`${capability.id}: implementation mapping is empty`);
|
|
128
206
|
}
|
|
129
207
|
if (!Array.isArray(capability.tests) || capability.tests.length === 0) {
|
|
@@ -131,47 +209,58 @@ function checkInternalArchitecture({
|
|
|
131
209
|
}
|
|
132
210
|
for (const field of ["implementation", "tests", "contracts"]) {
|
|
133
211
|
for (const relativePath of capability[field] || []) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
212
|
+
if (!fs.existsSync(path.resolve(root, relativePath))) {
|
|
213
|
+
issues.push(
|
|
214
|
+
`${capability.id}: ${field} path is missing: ${relativePath}`,
|
|
215
|
+
);
|
|
137
216
|
}
|
|
138
|
-
if (field
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
217
|
+
if (field !== "implementation") continue;
|
|
218
|
+
const owner = coveredImplementation.get(relativePath);
|
|
219
|
+
if (owner) {
|
|
220
|
+
issues.push(
|
|
221
|
+
`${relativePath}: implementation is mapped by both ${owner} and ${capability.id}`,
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
coveredImplementation.set(relativePath, capability.id);
|
|
147
225
|
}
|
|
148
226
|
}
|
|
149
227
|
}
|
|
150
228
|
}
|
|
151
|
-
|
|
152
229
|
const expectedImplementation = new Set(
|
|
153
230
|
index.coverageRoots.flatMap((entry) =>
|
|
154
231
|
collectImplementationFiles(root, entry).map((absolutePath) =>
|
|
155
|
-
normalizeRelative(root, absolutePath)
|
|
156
|
-
)
|
|
232
|
+
normalizeRelative(root, absolutePath),
|
|
233
|
+
),
|
|
157
234
|
),
|
|
158
235
|
);
|
|
159
236
|
for (const relativePath of expectedImplementation) {
|
|
160
237
|
if (!coveredImplementation.has(relativePath)) {
|
|
161
|
-
issues.push(
|
|
238
|
+
issues.push(
|
|
239
|
+
`capability-to-test mapping is missing implementation: ${relativePath}`,
|
|
240
|
+
);
|
|
162
241
|
}
|
|
163
242
|
}
|
|
164
243
|
for (const relativePath of coveredImplementation.keys()) {
|
|
165
244
|
if (!expectedImplementation.has(relativePath)) {
|
|
166
|
-
issues.push(
|
|
245
|
+
issues.push(
|
|
246
|
+
`capability implementation is outside coverageRoots: ${relativePath}`,
|
|
247
|
+
);
|
|
167
248
|
}
|
|
168
249
|
}
|
|
250
|
+
return { issues, expectedImplementation };
|
|
251
|
+
}
|
|
169
252
|
|
|
253
|
+
function evaluateDependencyDirections(root, index, sourceOverrides) {
|
|
254
|
+
const issues = [];
|
|
170
255
|
for (const rule of index.dependencyRules) {
|
|
171
256
|
const allowed = rule.allowedRelativeTargets || [];
|
|
172
257
|
for (const sourceEntry of rule.sources || []) {
|
|
173
|
-
for (const absoluteSource of collectImplementationFiles(
|
|
174
|
-
|
|
258
|
+
for (const absoluteSource of collectImplementationFiles(
|
|
259
|
+
root,
|
|
260
|
+
sourceEntry,
|
|
261
|
+
)) {
|
|
262
|
+
if (![".js", ".mjs", ".cjs"].includes(path.extname(absoluteSource)))
|
|
263
|
+
continue;
|
|
175
264
|
const sourcePath = normalizeRelative(root, absoluteSource);
|
|
176
265
|
const source = sourceOverrides.has(sourcePath)
|
|
177
266
|
? sourceOverrides.get(sourcePath)
|
|
@@ -187,18 +276,42 @@ function checkInternalArchitecture({
|
|
|
187
276
|
}
|
|
188
277
|
}
|
|
189
278
|
}
|
|
279
|
+
return issues;
|
|
280
|
+
}
|
|
190
281
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
282
|
+
function buildRepositoryGraph(root, repositorySources, sourceOverrides) {
|
|
283
|
+
const sourceSet = new Set(repositorySources);
|
|
284
|
+
const graph = new Map(repositorySources.map((file) => [file, new Set()]));
|
|
285
|
+
for (const sourcePath of repositorySources) {
|
|
194
286
|
const source = sourceOverrides.has(sourcePath)
|
|
195
287
|
? sourceOverrides.get(sourcePath)
|
|
196
288
|
: fs.readFileSync(path.resolve(root, sourcePath), "utf8");
|
|
197
289
|
for (const specifier of relativeImports(source)) {
|
|
198
|
-
const target = resolveGraphTarget(root, sourcePath, specifier,
|
|
290
|
+
const target = resolveGraphTarget(root, sourcePath, specifier, sourceSet);
|
|
199
291
|
if (target) graph.get(sourcePath).add(target);
|
|
200
292
|
}
|
|
201
293
|
}
|
|
294
|
+
return graph;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function checkInternalArchitecture({
|
|
298
|
+
root = process.cwd(),
|
|
299
|
+
index = JSON.parse(
|
|
300
|
+
fs.readFileSync(
|
|
301
|
+
path.join(root, "architecture", "internal-capabilities.json"),
|
|
302
|
+
"utf8",
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
sourceOverrides = new Map(),
|
|
306
|
+
} = {}) {
|
|
307
|
+
assertIndexShape(index);
|
|
308
|
+
const issues = [];
|
|
309
|
+
const repositorySources = repositoryJavaScriptFiles(root);
|
|
310
|
+
const ownership = evaluateSourceOwnership(index, repositorySources);
|
|
311
|
+
const capabilities = evaluateCapabilities(root, index);
|
|
312
|
+
issues.push(...ownership.issues, ...capabilities.issues);
|
|
313
|
+
issues.push(...evaluateDependencyDirections(root, index, sourceOverrides));
|
|
314
|
+
const graph = buildRepositoryGraph(root, repositorySources, sourceOverrides);
|
|
202
315
|
const cycles = dependencyCycles(graph);
|
|
203
316
|
for (const cycle of cycles) {
|
|
204
317
|
issues.push(`internal dependency cycle: ${cycle.join(" -> ")}`);
|
|
@@ -212,7 +325,14 @@ function checkInternalArchitecture({
|
|
|
212
325
|
return {
|
|
213
326
|
schemaVersion: index.schemaVersion,
|
|
214
327
|
capabilities: index.capabilities.length,
|
|
215
|
-
implementations: expectedImplementation.size,
|
|
328
|
+
implementations: capabilities.expectedImplementation.size,
|
|
329
|
+
repositorySources: repositorySources.length,
|
|
330
|
+
ownedSources: ownership.ownedSources,
|
|
331
|
+
excludedSources: ownership.excludedSources,
|
|
332
|
+
dependencyEdges: [...graph.values()].reduce(
|
|
333
|
+
(total, targets) => total + targets.size,
|
|
334
|
+
0,
|
|
335
|
+
),
|
|
216
336
|
dependencyRules: index.dependencyRules.length,
|
|
217
337
|
dependencyCycles: cycles.length,
|
|
218
338
|
};
|
|
@@ -226,7 +346,8 @@ if (
|
|
|
226
346
|
const report = checkInternalArchitecture();
|
|
227
347
|
console.log(
|
|
228
348
|
`internal architecture check passed: ${report.capabilities} capabilities, ` +
|
|
229
|
-
`${report.implementations} implementations, ${report.
|
|
349
|
+
`${report.implementations} capability implementations, ${report.ownedSources}/${report.repositorySources} owned sources, ` +
|
|
350
|
+
`${report.dependencyEdges} dependency edges, ${report.dependencyRules} dependency rules, ` +
|
|
230
351
|
`${report.dependencyCycles} cycles`,
|
|
231
352
|
);
|
|
232
353
|
} catch (error) {
|
|
@@ -235,4 +356,9 @@ if (
|
|
|
235
356
|
}
|
|
236
357
|
}
|
|
237
358
|
|
|
238
|
-
export {
|
|
359
|
+
export {
|
|
360
|
+
checkInternalArchitecture,
|
|
361
|
+
dependencyCycles,
|
|
362
|
+
relativeImports,
|
|
363
|
+
repositoryJavaScriptFiles,
|
|
364
|
+
};
|
|
@@ -166,14 +166,23 @@ function evaluatePublicSurface({ root, revision, policy }) {
|
|
|
166
166
|
);
|
|
167
167
|
}
|
|
168
168
|
const previous = baselineByKey.get(entry[definition.key]);
|
|
169
|
+
const currentContract = publicSurfaceContract(entry, definition.kind);
|
|
169
170
|
if (
|
|
170
171
|
previous &&
|
|
171
|
-
JSON.stringify(
|
|
172
|
+
JSON.stringify(currentContract) !==
|
|
172
173
|
JSON.stringify(publicSurfaceContract(previous, definition.kind))
|
|
173
174
|
) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
const approval = policy.approvedPublicSurfaceTransitions?.[label];
|
|
176
|
+
const approved =
|
|
177
|
+
approval?.fromRevision === revision &&
|
|
178
|
+
String(approval?.rationale || "").trim() &&
|
|
179
|
+
JSON.stringify(approval?.contract) ===
|
|
180
|
+
JSON.stringify(currentContract);
|
|
181
|
+
if (!approved) {
|
|
182
|
+
issues.push(
|
|
183
|
+
`${label}: existing public contract drifted from ${revision}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
177
186
|
}
|
|
178
187
|
if (!previous && !entry.nonDuplicationRationale) {
|
|
179
188
|
issues.push(
|
|
@@ -193,16 +202,19 @@ function evaluateAddedFunctionBudgets({
|
|
|
193
202
|
budgets,
|
|
194
203
|
}) {
|
|
195
204
|
const issues = [];
|
|
196
|
-
const
|
|
197
|
-
(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
const identity = (entry) =>
|
|
206
|
+
entry.name.startsWith("<anonymous@") ? "<anonymous>" : entry.name;
|
|
207
|
+
const baselineFunctionCounts = new Map();
|
|
208
|
+
for (const entry of baseline.functions || []) {
|
|
209
|
+
const key = identity(entry);
|
|
210
|
+
baselineFunctionCounts.set(key, (baselineFunctionCounts.get(key) || 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
const currentFunctionCounts = new Map();
|
|
201
213
|
for (const entry of metrics.functions) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
) {
|
|
214
|
+
const identityKey = identity(entry);
|
|
215
|
+
const occurrence = (currentFunctionCounts.get(identityKey) || 0) + 1;
|
|
216
|
+
currentFunctionCounts.set(identityKey, occurrence);
|
|
217
|
+
if (occurrence <= (baselineFunctionCounts.get(identityKey) || 0)) {
|
|
206
218
|
continue;
|
|
207
219
|
}
|
|
208
220
|
const key = `${file}#${entry.name}`;
|
|
@@ -227,6 +239,43 @@ function evaluateAddedFunctionBudgets({
|
|
|
227
239
|
return issues;
|
|
228
240
|
}
|
|
229
241
|
|
|
242
|
+
function evaluateRepositoryBudgets({ current, policy }) {
|
|
243
|
+
const issues = [];
|
|
244
|
+
const budgets = policy.repositoryBudgets;
|
|
245
|
+
if (!budgets) return issues;
|
|
246
|
+
if (!String(budgets.rationale || "").trim()) {
|
|
247
|
+
issues.push("repository growth budget requires a rationale");
|
|
248
|
+
}
|
|
249
|
+
const checks = [
|
|
250
|
+
[
|
|
251
|
+
"handMaintainedSourceFiles",
|
|
252
|
+
"maxHandMaintainedSourceFiles",
|
|
253
|
+
"source files",
|
|
254
|
+
],
|
|
255
|
+
[
|
|
256
|
+
"handMaintainedSourceLines",
|
|
257
|
+
"maxHandMaintainedSourceLines",
|
|
258
|
+
"source lines",
|
|
259
|
+
],
|
|
260
|
+
["workflowFiles", "maxWorkflowFiles", "workflow files"],
|
|
261
|
+
["workflowLines", "maxWorkflowLines", "workflow lines"],
|
|
262
|
+
];
|
|
263
|
+
for (const [metric, ceiling, label] of checks) {
|
|
264
|
+
if (!Number.isInteger(budgets[ceiling]) || budgets[ceiling] < 0) {
|
|
265
|
+
issues.push(
|
|
266
|
+
`repository growth budget ${ceiling} must be a non-negative integer`,
|
|
267
|
+
);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (current.repository[metric] > budgets[ceiling]) {
|
|
271
|
+
issues.push(
|
|
272
|
+
`repository ${label} are ${current.repository[metric]}; approved ceiling is ${budgets[ceiling]}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return issues;
|
|
277
|
+
}
|
|
278
|
+
|
|
230
279
|
function evaluateMaintainability({ current, baselineFiles, policy }) {
|
|
231
280
|
const issues = [];
|
|
232
281
|
const budgets = policy.sourceBudgets;
|
|
@@ -239,14 +288,22 @@ function evaluateMaintainability({ current, baselineFiles, policy }) {
|
|
|
239
288
|
);
|
|
240
289
|
}
|
|
241
290
|
for (const entry of metrics.functions) {
|
|
242
|
-
|
|
291
|
+
const key = `${file}#${entry.name}`;
|
|
292
|
+
const approval = policy.approvedExtractedDebt?.[key];
|
|
293
|
+
if (approval && !String(approval.rationale || "").trim()) {
|
|
294
|
+
issues.push(`${key}: approved extracted debt requires a rationale`);
|
|
295
|
+
}
|
|
296
|
+
const allowedLines = approval?.maxLines ?? budgets.newFunctionLines;
|
|
297
|
+
const allowedComplexity =
|
|
298
|
+
approval?.maxComplexity ?? budgets.newFunctionComplexity;
|
|
299
|
+
if (entry.lines > allowedLines) {
|
|
243
300
|
issues.push(
|
|
244
|
-
`${file}:${entry.start} ${entry.name} has ${entry.lines} lines; new-function budget is ${
|
|
301
|
+
`${file}:${entry.start} ${entry.name} has ${entry.lines} lines; new-function budget is ${allowedLines}`,
|
|
245
302
|
);
|
|
246
303
|
}
|
|
247
|
-
if (entry.complexity >
|
|
304
|
+
if (entry.complexity > allowedComplexity) {
|
|
248
305
|
issues.push(
|
|
249
|
-
`${file}:${entry.start} ${entry.name} has complexity ${entry.complexity}; new-function budget is ${
|
|
306
|
+
`${file}:${entry.start} ${entry.name} has complexity ${entry.complexity}; new-function budget is ${allowedComplexity}`,
|
|
250
307
|
);
|
|
251
308
|
}
|
|
252
309
|
}
|
|
@@ -326,6 +383,7 @@ function checkMaintainability({ root = process.cwd() } = {}) {
|
|
|
326
383
|
const current = collectMaintainabilityMetrics({ root });
|
|
327
384
|
const baselineFiles = sourceMetricsAtRevision(root, enforcementRevision);
|
|
328
385
|
const issues = evaluateMaintainability({ current, baselineFiles, policy });
|
|
386
|
+
issues.push(...evaluateRepositoryBudgets({ current, policy }));
|
|
329
387
|
issues.push(
|
|
330
388
|
...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
|
|
331
389
|
);
|
|
@@ -366,6 +424,7 @@ export {
|
|
|
366
424
|
ensureMaintainabilityRevisionsAvailable,
|
|
367
425
|
ensureRevisionAvailable,
|
|
368
426
|
evaluateMaintainability,
|
|
427
|
+
evaluateRepositoryBudgets,
|
|
369
428
|
evaluatePublicSurface,
|
|
370
429
|
sourceMetricsAtRevision,
|
|
371
430
|
};
|
|
@@ -621,6 +621,21 @@ function createPublicationReleaseRegistry({ packageJson, timestampPolicy }) {
|
|
|
621
621
|
};
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
const RELEASE_PROPAGATION_MODEL = {
|
|
625
|
+
graphContract: "kungfu-buildchain-release-propagation-graph",
|
|
626
|
+
planContract: "kungfu-buildchain-release-propagation-plan",
|
|
627
|
+
lockContract: "kungfu-buildchain-release-propagation-lock",
|
|
628
|
+
workContract: "kungfu-buildchain-release-propagation-work",
|
|
629
|
+
stageReceiptContract: "kungfu-buildchain-release-propagation-stage-receipt",
|
|
630
|
+
workControlBindings: [
|
|
631
|
+
"kungfu.assignment-graph.work-ref/v1",
|
|
632
|
+
"kungfu.work-control.initiative-family-state/v2",
|
|
633
|
+
],
|
|
634
|
+
completionBoundary: "production-online-readback-plus-accepted-work-control-decision",
|
|
635
|
+
defaultChannelPolicy: "preserve",
|
|
636
|
+
defaultChannelMap: { alpha: "alpha", release: "release" },
|
|
637
|
+
};
|
|
638
|
+
|
|
624
639
|
function buildSiteBundle() {
|
|
625
640
|
const packageJson = readJson("package.json");
|
|
626
641
|
const inventory = readJson("tests/buildchain-inventory.json");
|
|
@@ -857,13 +872,7 @@ function buildSiteBundle() {
|
|
|
857
872
|
schema: "schemas/release-passport-v1.schema.json",
|
|
858
873
|
checkManifest: "release-passport-check-manifest.json",
|
|
859
874
|
},
|
|
860
|
-
releasePropagation:
|
|
861
|
-
graphContract: "kungfu-buildchain-release-propagation-graph",
|
|
862
|
-
planContract: "kungfu-buildchain-release-propagation-plan",
|
|
863
|
-
lockContract: "kungfu-buildchain-release-propagation-lock",
|
|
864
|
-
defaultChannelPolicy: "preserve",
|
|
865
|
-
defaultChannelMap: { alpha: "alpha", release: "release" },
|
|
866
|
-
},
|
|
875
|
+
releasePropagation: RELEASE_PROPAGATION_MODEL,
|
|
867
876
|
npm: {
|
|
868
877
|
package: packageJson.name,
|
|
869
878
|
command: packageJson.bin?.buildchain || "",
|
|
@@ -174,10 +174,12 @@ function architectureSummary(root, revision = "") {
|
|
|
174
174
|
const implementationPaths = new Set(
|
|
175
175
|
index.capabilities.flatMap((entry) => entry.implementation || []),
|
|
176
176
|
);
|
|
177
|
-
const
|
|
178
|
-
|
|
177
|
+
const repositorySources = trackedFiles(root, revision).filter(
|
|
178
|
+
isHandMaintainedSource,
|
|
179
179
|
);
|
|
180
|
-
|
|
180
|
+
const repositorySourceSet = new Set(repositorySources);
|
|
181
|
+
const graph = new Map(repositorySources.map((file) => [file, new Set()]));
|
|
182
|
+
for (const file of repositorySources) {
|
|
181
183
|
if (!JS_EXTENSIONS.has(path.extname(file))) continue;
|
|
182
184
|
for (const specifier of relativeImports(
|
|
183
185
|
readTrackedFile(root, file, revision),
|
|
@@ -192,10 +194,20 @@ function architectureSummary(root, revision = "") {
|
|
|
192
194
|
`${base}.cjs`,
|
|
193
195
|
`${base}/index.js`,
|
|
194
196
|
`${base}/index.mjs`,
|
|
195
|
-
].find((candidate) =>
|
|
197
|
+
].find((candidate) => repositorySourceSet.has(candidate));
|
|
196
198
|
if (target) graph.get(file).add(target);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
201
|
+
const exclusions = new Set(
|
|
202
|
+
(index.ownershipExclusions || []).map((entry) => entry.path),
|
|
203
|
+
);
|
|
204
|
+
const ownedSources = repositorySources.filter((file) =>
|
|
205
|
+
index.ownershipRules?.some((rule) =>
|
|
206
|
+
(rule.paths || []).some(
|
|
207
|
+
(prefix) => file === prefix || file.startsWith(`${prefix}/`),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
).length;
|
|
199
211
|
return {
|
|
200
212
|
capabilities: index.capabilities.length,
|
|
201
213
|
implementationMappings: index.capabilities.reduce(
|
|
@@ -207,6 +219,14 @@ function architectureSummary(root, revision = "") {
|
|
|
207
219
|
0,
|
|
208
220
|
),
|
|
209
221
|
dependencyRules: index.dependencyRules.length,
|
|
222
|
+
repositorySources: repositorySources.length,
|
|
223
|
+
ownedSources,
|
|
224
|
+
excludedSources: repositorySources.filter((file) => exclusions.has(file))
|
|
225
|
+
.length,
|
|
226
|
+
dependencyEdges: [...graph.values()].reduce(
|
|
227
|
+
(total, targets) => total + targets.size,
|
|
228
|
+
0,
|
|
229
|
+
),
|
|
210
230
|
dependencyCycles: dependencyCycles(graph).length,
|
|
211
231
|
};
|
|
212
232
|
}
|