@kungfu-tech/buildchain 3.0.4-alpha.2 → 3.0.4-alpha.3

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 (49) hide show
  1. package/bin/buildchain.mjs +126 -72
  2. package/bin/internal/trust-release-cli.mjs +15 -537
  3. package/bin/internal/trust-release-command-handlers.mjs +14 -0
  4. package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
  5. package/bin/internal/trust-release-release-handlers.mjs +317 -0
  6. package/bin/internal/trust-release-verification-handlers.mjs +306 -0
  7. package/dist/site/buildchain-contract.json +35 -24
  8. package/dist/site/buildchain-site.json +22 -10
  9. package/dist/site/controller-registry.json +16 -3
  10. package/dist/site/kfd-claims.json +9 -7
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +2 -2
  13. package/dist/site/node-api-registry.json +7 -7
  14. package/dist/site/page-registry.json +9 -4
  15. package/dist/site/public-surface-audit.json +56 -8
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-model.json +7 -0
  18. package/dist/site/site-manifest.json +6 -6
  19. package/dist/site/workflow-registry.json +13 -5
  20. package/docs/MAP.md +1 -1
  21. package/docs/release-propagation.md +166 -8
  22. package/package.json +1 -1
  23. package/packages/core/buildchain-kfd-claims.js +1 -1
  24. package/packages/core/controller-evidence.js +8 -1
  25. package/packages/core/index.js +1 -13
  26. package/packages/core/paper-npm-bootstrap.js +492 -0
  27. package/packages/core/paper.js +271 -669
  28. package/packages/core/public-surface-cli.js +12 -1
  29. package/packages/core/release-passport.js +67 -71
  30. package/packages/core/release-propagation-common.js +64 -0
  31. package/packages/core/release-propagation-execution-profile.js +59 -0
  32. package/packages/core/release-propagation-release.js +196 -0
  33. package/packages/core/release-propagation-stage-evidence.js +364 -0
  34. package/packages/core/release-propagation-work-capture.js +64 -0
  35. package/packages/core/release-propagation-work-constants.js +34 -0
  36. package/packages/core/release-propagation-work-control.js +203 -0
  37. package/packages/core/release-propagation-work-transitions.js +145 -0
  38. package/packages/core/release-propagation-work.js +517 -0
  39. package/packages/core/release-propagation.js +34 -158
  40. package/scripts/aws-windows-jit-controller-core.mjs +269 -0
  41. package/scripts/aws-windows-jit-controller.mjs +502 -0
  42. package/scripts/check-internal-architecture.mjs +178 -52
  43. package/scripts/check-maintainability.mjs +76 -17
  44. package/scripts/generate-site-bundle.mjs +16 -7
  45. package/scripts/maintainability-metrics.mjs +24 -4
  46. package/scripts/release-propagation.mjs +126 -0
  47. package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
  48. package/scripts/web-surface-core.mjs +46 -207
  49. 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.relative(root, path.resolve(root, value)).split(path.sep).join("/");
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)) ? [absolute] : [];
29
+ return implementationExtensions.has(path.extname(absolute))
30
+ ? [absolute]
31
+ : [];
26
32
  }
27
- return fs.readdirSync(absolute, { withFileTypes: true }).flatMap((item) =>
28
- collectImplementationFiles(root, path.join(entry, item.name))
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(path.dirname(path.resolve(root, sourcePath)), specifier);
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
- ...members.slice(0, index),
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 ["coverageRoots", "dependencyRules", "capabilities"]) {
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(`internal architecture index requires non-empty ${field}`);
112
+ throw new Error(
113
+ `internal architecture index requires non-empty ${field}`,
114
+ );
98
115
  }
99
116
  }
100
117
  }
101
118
 
102
- function checkInternalArchitecture({
103
- root = process.cwd(),
104
- index = JSON.parse(
105
- fs.readFileSync(
106
- path.join(root, "architecture", "internal-capabilities.json"),
107
- "utf8",
108
- ),
109
- ),
110
- sourceOverrides = new Map(),
111
- } = {}) {
112
- assertIndexShape(index);
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(`capability id must be present and unique: ${capability?.id || "<empty>"}`);
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 (!Array.isArray(capability.implementation) || capability.implementation.length === 0) {
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
- const absolutePath = path.resolve(root, relativePath);
135
- if (!fs.existsSync(absolutePath)) {
136
- issues.push(`${capability.id}: ${field} path is missing: ${relativePath}`);
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 === "implementation") {
139
- const owner = coveredImplementation.get(relativePath);
140
- if (owner) {
141
- issues.push(
142
- `${relativePath}: implementation is mapped by both ${owner} and ${capability.id}`,
143
- );
144
- } else {
145
- coveredImplementation.set(relativePath, capability.id);
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(`capability-to-test mapping is missing implementation: ${relativePath}`);
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(`capability implementation is outside coverageRoots: ${relativePath}`);
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(root, sourceEntry)) {
174
- if (![".js", ".mjs", ".cjs"].includes(path.extname(absoluteSource))) continue;
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
- const graph = new Map([...expectedImplementation].map((file) => [file, new Set()]));
192
- for (const sourcePath of expectedImplementation) {
193
- if (![".js", ".mjs", ".cjs"].includes(path.extname(sourcePath))) continue;
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, expectedImplementation);
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.dependencyRules} dependency rules, ` +
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 { checkInternalArchitecture, dependencyCycles, relativeImports };
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(publicSurfaceContract(entry, definition.kind)) !==
172
+ JSON.stringify(currentContract) !==
172
173
  JSON.stringify(publicSurfaceContract(previous, definition.kind))
173
174
  ) {
174
- issues.push(
175
- `${label}: existing public contract drifted from ${revision}`,
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 baselineFunctionNames = new Set(
197
- (baseline.functions || [])
198
- .map((entry) => entry.name)
199
- .filter((name) => !name.startsWith("<anonymous@")),
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
- if (
203
- entry.name.startsWith("<anonymous@") ||
204
- baselineFunctionNames.has(entry.name)
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
- if (entry.lines > budgets.newFunctionLines) {
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 ${budgets.newFunctionLines}`,
301
+ `${file}:${entry.start} ${entry.name} has ${entry.lines} lines; new-function budget is ${allowedLines}`,
245
302
  );
246
303
  }
247
- if (entry.complexity > budgets.newFunctionComplexity) {
304
+ if (entry.complexity > allowedComplexity) {
248
305
  issues.push(
249
- `${file}:${entry.start} ${entry.name} has complexity ${entry.complexity}; new-function budget is ${budgets.newFunctionComplexity}`,
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 graph = new Map(
178
- [...implementationPaths].map((file) => [file, new Set()]),
177
+ const repositorySources = trackedFiles(root, revision).filter(
178
+ isHandMaintainedSource,
179
179
  );
180
- for (const file of implementationPaths) {
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) => implementationPaths.has(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
  }