auditor-lambda 0.6.12 → 0.8.0

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 (56) hide show
  1. package/README.md +0 -21
  2. package/audit-code-wrapper-lib.mjs +44 -1
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.js +8 -0
  5. package/dist/cli/auditStep.js +7 -1
  6. package/dist/cli/dispatch.js +14 -3
  7. package/dist/cli/nextStepCommand.js +37 -0
  8. package/dist/cli/prompts.js +2 -0
  9. package/dist/cli.d.ts +0 -1
  10. package/dist/cli.js +22 -15
  11. package/dist/extractors/fileInventory.js +15 -2
  12. package/dist/extractors/graph.js +12 -2
  13. package/dist/io/artifacts.d.ts +3 -1
  14. package/dist/io/artifacts.js +18 -2
  15. package/dist/orchestrator/advance.js +2 -1
  16. package/dist/orchestrator/artifactFreshness.js +12 -2
  17. package/dist/orchestrator/artifactMetadata.d.ts +1 -0
  18. package/dist/orchestrator/artifactMetadata.js +15 -0
  19. package/dist/orchestrator/autoFixExecutor.d.ts +1 -1
  20. package/dist/orchestrator/autoFixExecutor.js +10 -0
  21. package/dist/orchestrator/executorResult.d.ts +12 -0
  22. package/dist/orchestrator/executorResult.js +1 -0
  23. package/dist/orchestrator/fileIntegrity.d.ts +1 -0
  24. package/dist/orchestrator/fileIntegrity.js +12 -3
  25. package/dist/orchestrator/flowRequeue.js +1 -14
  26. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +1 -1
  27. package/dist/orchestrator/graphEnrichmentExecutor.js +3 -1
  28. package/dist/orchestrator/internalExecutors.d.ts +1 -18
  29. package/dist/orchestrator/internalExecutors.js +1 -158
  30. package/dist/orchestrator/reviewPacketGraph.d.ts +31 -0
  31. package/dist/orchestrator/reviewPacketGraph.js +691 -0
  32. package/dist/orchestrator/reviewPacketSizing.d.ts +25 -0
  33. package/dist/orchestrator/reviewPacketSizing.js +60 -0
  34. package/dist/orchestrator/reviewPackets.d.ts +3 -28
  35. package/dist/orchestrator/reviewPackets.js +6 -740
  36. package/dist/orchestrator/runtimeCommand.d.ts +11 -0
  37. package/dist/orchestrator/runtimeCommand.js +79 -0
  38. package/dist/orchestrator/scope.js +1 -1
  39. package/dist/orchestrator/syntaxResolutionExecutor.d.ts +1 -1
  40. package/dist/orchestrator/synthesisExecutors.d.ts +12 -0
  41. package/dist/orchestrator/synthesisExecutors.js +90 -0
  42. package/dist/orchestrator.js +1 -4
  43. package/dist/quota/index.d.ts +1 -1
  44. package/dist/quota/index.js +1 -1
  45. package/dist/types/workerSession.d.ts +1 -3
  46. package/dist/types.d.ts +6 -0
  47. package/dist/types.js +20 -1
  48. package/docs/development.md +35 -139
  49. package/docs/history.md +26 -0
  50. package/docs/product.md +41 -108
  51. package/package.json +1 -1
  52. package/schemas/audit_findings.schema.json +3 -2
  53. package/schemas/dispatch_quota.schema.json +2 -0
  54. package/schemas/external_analyzer_results.schema.json +2 -2
  55. package/schemas/repo_manifest.schema.json +1 -1
  56. package/docs/handoff.md +0 -204
@@ -1,76 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { estimateTokensFromBytes, isRecord } from "@audit-tools/shared";
3
2
  import { LENS_ORDER, priorityRank, sortLenses } from "./auditTaskUtils.js";
4
- import { UnionFind } from "./unionFind.js";
5
- import { normalizeGraphPath, isPackageManifestPath, isTypescriptProjectConfigPath, isGoModuleManifestPath, isCargoManifestPath, isMavenPomPath, } from "../extractors/graphPathUtils.js";
3
+ import { normalizeGraphPath } from "../extractors/graphPathUtils.js";
4
+ import { DEFAULT_MAX_TASKS_PER_PACKET, DEFAULT_TARGET_PACKET_TOKENS, ESTIMATED_TOKENS_PER_LINE, ESTIMATED_PACKET_PROMPT_TOKENS, sizeIndexFromManifest, fileGroupContentTokens, taskContentTokens, estimateTaskGroupTokens, } from "./reviewPacketSizing.js";
5
+ import { HIGH_FAN_DEGREE_THRESHOLD, collectGraphEdges, buildGraphDegreeIndex, isPacketExpansionEdge, buildFileToGroupKeys, isConcreteGraphEdge, unionFindFromGroups, roundQuality, buildPlanningGraphEdges, buildPacketGraphContext, } from "./reviewPacketGraph.js";
6
6
  // Re-exported for scope.ts, which imports the canonical path normalizer here.
7
7
  export { normalizeGraphPath };
8
- const DEFAULT_MAX_TASKS_PER_PACKET = 0;
9
- const DEFAULT_TARGET_PACKET_LINES = 8000;
10
- export const ESTIMATED_TOKENS_PER_LINE = 4;
11
- export const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
12
- // Default per-packet content-token budget. Kept equal to the legacy
13
- // line-target × per-line estimate so byte-derived sizing lands on the same
14
- // thresholds as the old line-based sizing when the line fallback is in effect.
15
- const DEFAULT_TARGET_PACKET_TOKENS = DEFAULT_TARGET_PACKET_LINES * ESTIMATED_TOKENS_PER_LINE;
16
- /**
17
- * Build a path → size_bytes index from a repo manifest. Byte counts are
18
- * recorded during intake, so this never reads files. Review packet token
19
- * estimates are derived from these bytes (Phase 2) instead of counted lines.
20
- */
21
- export function sizeIndexFromManifest(repoManifest) {
22
- if (!repoManifest)
23
- return {};
24
- return Object.fromEntries(repoManifest.files.map((file) => [file.path, file.size_bytes]));
25
- }
26
- /**
27
- * Estimated content tokens for a single file. Prefers a byte-based estimate
28
- * from `sizeIndex` (sourced from the repo manifest); falls back to the legacy
29
- * line-based estimate when no positive byte count is available (e.g. manually
30
- * built tasks in tests, or paths absent from the manifest).
31
- */
32
- function pathContentTokens(owner, path, sizeIndex, lineIndex) {
33
- const bytes = sizeIndex?.[path];
34
- if (typeof bytes === "number" && bytes > 0) {
35
- return estimateTokensFromBytes(bytes);
36
- }
37
- const lines = owner?.file_line_counts?.[path] ?? lineIndex?.[path] ?? 0;
38
- return lines * ESTIMATED_TOKENS_PER_LINE;
39
- }
40
- /** Estimated content tokens for one task across all of its files. */
41
- function taskContentTokens(task, sizeIndex, lineIndex) {
42
- return task.file_paths.reduce((sum, path) => sum + pathContentTokens(task, path, sizeIndex, lineIndex), 0);
43
- }
44
- /**
45
- * Estimated content tokens across a set of file paths, resolving an owning task
46
- * per path so the line fallback can read its `file_line_counts`. Shared files
47
- * are counted once.
48
- */
49
- function fileGroupContentTokens(filePaths, tasks, sizeIndex, lineIndex) {
50
- let total = 0;
51
- for (const path of filePaths) {
52
- const owner = tasks.find((task) => task.file_paths.includes(path));
53
- total += pathContentTokens(owner, path, sizeIndex, lineIndex);
54
- }
55
- return total;
56
- }
57
- export function estimateTaskGroupTokens(tasks, sizeIndex, lineIndex) {
58
- let contentTokens = 0;
59
- for (const task of tasks) {
60
- contentTokens += taskContentTokens(task, sizeIndex, lineIndex);
61
- }
62
- return ESTIMATED_PACKET_PROMPT_TOKENS + contentTokens;
63
- }
64
- const PACKET_EXPANSION_MIN_CONFIDENCE = 0.65;
65
- /**
66
- * Fan-in / fan-out degree above which a node is treated as a hub. Exported so
67
- * the Phase 3 delta-scope expansion skips the same hubs that packet planning
68
- * skips, preventing scope blow-up through highly-connected modules.
69
- */
70
- export const HIGH_FAN_DEGREE_THRESHOLD = 12;
71
- const HIGH_FAN_EXPANSION_CONFIDENCE = 0.99;
72
- const MAX_PACKET_KEY_EDGES = 8;
73
- const MAX_PACKET_BOUNDARY_FILES = 12;
8
+ // Sizing / token-budget arithmetic moved to reviewPacketSizing.ts; re-exported
9
+ // here for the modules that import it from reviewPackets.
10
+ export { ESTIMATED_TOKENS_PER_LINE, ESTIMATED_PACKET_PROMPT_TOKENS, sizeIndexFromManifest, estimateTaskGroupTokens, };
74
11
  const MAX_WEAK_PACKET_SAMPLES = 12;
75
12
  const MAX_WEAK_PACKET_SAMPLE_FILES = 8;
76
13
  const WEAK_PACKET_GAP_ORDER = [
@@ -78,36 +15,6 @@ const WEAK_PACKET_GAP_ORDER = [
78
15
  "unexplained_files",
79
16
  "partial_cohesion",
80
17
  ];
81
- const MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS = 3;
82
- const MAX_ENTRYPOINT_FLOW_BRANCHES = 8;
83
- const SUBSYSTEM_CLUSTER_CONFIDENCE = 0.7;
84
- const PACKAGE_OWNERSHIP_CLUSTER_CONFIDENCE = 0.68;
85
- const MODULE_OWNERSHIP_CLUSTER_CONFIDENCE = 0.66;
86
- const ANALYZER_OWNERSHIP_EDGE_KIND = "analyzer-ownership-root-link";
87
- const MAX_SUBSYSTEM_CLUSTER_GROUPS = 4;
88
- const MAX_SUBSYSTEM_CLUSTER_TASKS = 8;
89
- const MAX_SUBSYSTEM_CLUSTER_FILES = 8;
90
- const MODULE_OWNERSHIP_EDGE_KINDS = new Set([
91
- "typescript-project-reference-link",
92
- "go-workspace-module-link",
93
- "cargo-workspace-member-link",
94
- "maven-module-link",
95
- ANALYZER_OWNERSHIP_EDGE_KIND,
96
- ]);
97
- const BROAD_ANALYZER_OWNERSHIP_ROOTS = new Set([
98
- "src",
99
- "lib",
100
- "app",
101
- "apps",
102
- "packages",
103
- "services",
104
- "crates",
105
- "modules",
106
- "test",
107
- "tests",
108
- "spec",
109
- "specs",
110
- ]);
111
18
  function normalizePriority(priority) {
112
19
  return priority ?? "low";
113
20
  }
@@ -135,647 +42,6 @@ function buildTaskGroups(tasks) {
135
42
  }
136
43
  return groups;
137
44
  }
138
- export function collectGraphEdges(graphBundle) {
139
- if (!graphBundle?.graphs) {
140
- return [];
141
- }
142
- const edges = [];
143
- for (const key of ["imports", "calls", "references"]) {
144
- const raw = graphBundle.graphs[key];
145
- if (!Array.isArray(raw)) {
146
- continue;
147
- }
148
- for (const item of raw) {
149
- if (!isRecord(item)) {
150
- continue;
151
- }
152
- if (typeof item.from !== "string" || typeof item.to !== "string") {
153
- continue;
154
- }
155
- const edge = {
156
- from: item.from,
157
- to: item.to,
158
- kind: typeof item.kind === "string" ? item.kind : undefined,
159
- };
160
- if (item.direction === "directed" || item.direction === "undirected") {
161
- edge.direction = item.direction;
162
- }
163
- if (typeof item.confidence === "number" && Number.isFinite(item.confidence)) {
164
- edge.confidence = Math.min(1, Math.max(0, item.confidence));
165
- }
166
- if (typeof item.reason === "string" && item.reason.trim().length > 0) {
167
- edge.reason = item.reason.trim();
168
- }
169
- edges.push(edge);
170
- }
171
- }
172
- return edges;
173
- }
174
- export function graphEdgeConfidence(edge) {
175
- if (typeof edge.confidence === "number" && Number.isFinite(edge.confidence)) {
176
- return Math.min(1, Math.max(0, edge.confidence));
177
- }
178
- if (edge.kind === "heuristic-container-edge") {
179
- return 0.25;
180
- }
181
- if (edge.kind?.startsWith("heuristic-")) {
182
- return 0.5;
183
- }
184
- return 0.8;
185
- }
186
- function isConcreteGraphEdge(edge) {
187
- return edge.kind !== "heuristic-container-edge";
188
- }
189
- export function buildGraphDegreeIndex(edges) {
190
- const fanIn = new Map();
191
- const fanOut = new Map();
192
- for (const edge of edges) {
193
- if (!isConcreteGraphEdge(edge)) {
194
- continue;
195
- }
196
- const from = normalizeGraphPath(edge.from);
197
- const to = normalizeGraphPath(edge.to);
198
- fanOut.set(from, (fanOut.get(from) ?? 0) + 1);
199
- fanIn.set(to, (fanIn.get(to) ?? 0) + 1);
200
- }
201
- return { fanIn, fanOut };
202
- }
203
- function isPacketExpansionEdge(edge, degreeIndex) {
204
- if (!isConcreteGraphEdge(edge)) {
205
- return false;
206
- }
207
- const confidence = graphEdgeConfidence(edge);
208
- if (confidence < PACKET_EXPANSION_MIN_CONFIDENCE) {
209
- return false;
210
- }
211
- const fromFanOut = degreeIndex.fanOut.get(normalizeGraphPath(edge.from)) ?? 0;
212
- const toFanIn = degreeIndex.fanIn.get(normalizeGraphPath(edge.to)) ?? 0;
213
- const highFanEdge = fromFanOut > HIGH_FAN_DEGREE_THRESHOLD ||
214
- toFanIn > HIGH_FAN_DEGREE_THRESHOLD;
215
- return !highFanEdge || confidence >= HIGH_FAN_EXPANSION_CONFIDENCE;
216
- }
217
- function buildFileToGroupKeys(groups) {
218
- const fileToGroupKeys = new Map();
219
- for (const [key, tasks] of groups) {
220
- for (const path of new Set(tasks.flatMap((task) => task.file_paths))) {
221
- const normalized = normalizeGraphPath(path);
222
- const existing = fileToGroupKeys.get(normalized) ?? new Set();
223
- existing.add(key);
224
- fileToGroupKeys.set(normalized, existing);
225
- }
226
- }
227
- return fileToGroupKeys;
228
- }
229
- function collectEntrypointFlowRoots(graphEdges, graphBundle) {
230
- const roots = new Set();
231
- const routes = Array.isArray(graphBundle?.graphs.routes)
232
- ? graphBundle.graphs.routes
233
- : [];
234
- for (const route of routes) {
235
- if (isRecord(route) && typeof route.handler === "string") {
236
- roots.add(normalizeGraphPath(route.handler));
237
- }
238
- }
239
- for (const edge of graphEdges) {
240
- if (edge.kind === "route-handler-link") {
241
- roots.add(normalizeGraphPath(edge.from));
242
- roots.add(normalizeGraphPath(edge.to));
243
- }
244
- else if (edge.kind === "package-entrypoint-link") {
245
- roots.add(normalizeGraphPath(edge.to));
246
- }
247
- }
248
- return roots;
249
- }
250
- function buildRepresentativePathIndex(groups, graphEdges, graphBundle) {
251
- const representatives = new Map();
252
- const addPath = (path) => {
253
- const normalized = normalizeGraphPath(path);
254
- if (!representatives.has(normalized)) {
255
- representatives.set(normalized, path);
256
- }
257
- };
258
- for (const tasks of groups.values()) {
259
- for (const path of tasks.flatMap((task) => task.file_paths)) {
260
- addPath(path);
261
- }
262
- }
263
- for (const edge of graphEdges) {
264
- addPath(edge.from);
265
- addPath(edge.to);
266
- }
267
- const routes = Array.isArray(graphBundle?.graphs.routes)
268
- ? graphBundle.graphs.routes
269
- : [];
270
- for (const route of routes) {
271
- if (isRecord(route) && typeof route.handler === "string") {
272
- addPath(route.handler);
273
- }
274
- }
275
- return representatives;
276
- }
277
- function uniqueTaskFilePaths(tasks) {
278
- return [...new Set(tasks.flatMap((task) => task.file_paths))].sort((a, b) => a.localeCompare(b));
279
- }
280
- function groupsOverlap(a, b) {
281
- for (const key of a) {
282
- if (b.has(key)) {
283
- return true;
284
- }
285
- }
286
- return false;
287
- }
288
- function unionFindFromGroups(groups, graphEdges) {
289
- const uf = new UnionFind(groups.keys());
290
- const fileToGroupKeys = buildFileToGroupKeys(groups);
291
- const degreeIndex = buildGraphDegreeIndex(graphEdges);
292
- for (const keys of fileToGroupKeys.values()) {
293
- const [first, ...rest] = [...keys].sort((a, b) => a.localeCompare(b));
294
- if (!first)
295
- continue;
296
- for (const key of rest) {
297
- uf.union(first, key);
298
- }
299
- }
300
- for (const edge of graphEdges) {
301
- if (!isPacketExpansionEdge(edge, degreeIndex)) {
302
- continue;
303
- }
304
- const fromGroups = fileToGroupKeys.get(normalizeGraphPath(edge.from));
305
- const toGroups = fileToGroupKeys.get(normalizeGraphPath(edge.to));
306
- if (!fromGroups || !toGroups) {
307
- continue;
308
- }
309
- for (const fromKey of fromGroups) {
310
- for (const toKey of toGroups) {
311
- uf.union(fromKey, toKey);
312
- }
313
- }
314
- }
315
- return uf;
316
- }
317
- function buildGraphConnectedComponentIndex(groups, graphEdges) {
318
- const uf = unionFindFromGroups(groups, graphEdges);
319
- return new Map([...groups.keys()].map((key) => [key, uf.find(key)]));
320
- }
321
- function subsystemRootForPath(path) {
322
- const segments = normalizeGraphPath(path).split("/").filter(Boolean);
323
- const directories = segments.slice(0, -1);
324
- if (directories.length < 2) {
325
- return undefined;
326
- }
327
- const namespace = directories[0];
328
- const depth = namespace === "apps" || namespace === "packages"
329
- ? 4
330
- : namespace === "src" ||
331
- namespace === "lib" ||
332
- namespace === "app" ||
333
- namespace === "tests" ||
334
- namespace === "test" ||
335
- namespace === "spec"
336
- ? 3
337
- : 2;
338
- if (directories.length < depth) {
339
- return undefined;
340
- }
341
- return directories.slice(0, depth).join("/");
342
- }
343
- function subsystemRootForTasks(tasks) {
344
- const rootsForFiles = uniqueTaskFilePaths(tasks).map(subsystemRootForPath);
345
- if (rootsForFiles.some((root) => root === undefined)) {
346
- return undefined;
347
- }
348
- const roots = new Set(rootsForFiles);
349
- return roots.size === 1 ? [...roots][0] : undefined;
350
- }
351
- function buildBoundedClusterEdges(params) {
352
- const groupToComponent = buildGraphConnectedComponentIndex(params.groups, params.graphEdges);
353
- const clusters = new Map();
354
- for (const [key, tasks] of params.groups) {
355
- const root = params.rootForTasks(tasks);
356
- if (!root) {
357
- continue;
358
- }
359
- const filePaths = uniqueTaskFilePaths(tasks);
360
- const cluster = clusters.get(root) ?? [];
361
- cluster.push({
362
- component: groupToComponent.get(key) ?? key,
363
- tasks,
364
- filePaths,
365
- representativePath: filePaths[0] ?? root,
366
- });
367
- clusters.set(root, cluster);
368
- }
369
- const clusterEdges = [];
370
- for (const [root, entries] of [...clusters.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
371
- const components = new Map();
372
- for (const entry of entries) {
373
- const component = components.get(entry.component) ?? {
374
- taskCount: 0,
375
- filePaths: new Set(),
376
- representativePath: entry.representativePath,
377
- };
378
- component.taskCount += entry.tasks.length;
379
- for (const filePath of entry.filePaths) {
380
- component.filePaths.add(filePath);
381
- }
382
- if (entry.representativePath.localeCompare(component.representativePath) < 0) {
383
- component.representativePath = entry.representativePath;
384
- }
385
- components.set(entry.component, component);
386
- }
387
- const componentEntries = [...components.values()].sort((a, b) => a.representativePath.localeCompare(b.representativePath));
388
- if (componentEntries.length < 2 ||
389
- componentEntries.length > MAX_SUBSYSTEM_CLUSTER_GROUPS) {
390
- continue;
391
- }
392
- const allFiles = new Set(componentEntries.flatMap((entry) => [...entry.filePaths]));
393
- const totalTasks = componentEntries.reduce((sum, entry) => sum + entry.taskCount, 0);
394
- const clusterTasks = entries.flatMap((entry) => entry.tasks);
395
- const totalContentTokens = fileGroupContentTokens(allFiles, clusterTasks, params.sizeIndex, params.lineIndex);
396
- if (allFiles.size > MAX_SUBSYSTEM_CLUSTER_FILES ||
397
- totalTasks > MAX_SUBSYSTEM_CLUSTER_TASKS ||
398
- totalContentTokens >
399
- (params.targetPacketTokens ?? DEFAULT_TARGET_PACKET_TOKENS)) {
400
- continue;
401
- }
402
- for (let index = 1; index < componentEntries.length; index++) {
403
- const previous = componentEntries[index - 1];
404
- const current = componentEntries[index];
405
- clusterEdges.push({
406
- from: previous.representativePath,
407
- to: current.representativePath,
408
- kind: params.edgeKind,
409
- direction: "undirected",
410
- confidence: params.edgeConfidence,
411
- reason: params.reasonForCluster(root, allFiles.size),
412
- });
413
- }
414
- }
415
- return clusterEdges.sort(compareGraphEdges);
416
- }
417
- function buildSubsystemClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
418
- return buildBoundedClusterEdges({
419
- groups,
420
- graphEdges,
421
- rootForTasks: subsystemRootForTasks,
422
- edgeKind: "subsystem-cluster-link",
423
- edgeConfidence: SUBSYSTEM_CLUSTER_CONFIDENCE,
424
- reasonForCluster: (root, fileCount) => `Bounded subsystem cluster '${root}' groups ${fileCount} file(s) without stronger graph evidence.`,
425
- lineIndex,
426
- sizeIndex,
427
- targetPacketTokens,
428
- });
429
- }
430
- function packageManifestRoot(path) {
431
- const segments = normalizeGraphPath(path).split("/").filter(Boolean);
432
- if (!isPackageManifestPath(path) || segments.length < 2) {
433
- return undefined;
434
- }
435
- return segments.slice(0, -1).join("/");
436
- }
437
- function configFileRoot(path, predicate) {
438
- const segments = normalizeGraphPath(path).split("/").filter(Boolean);
439
- if (!predicate(path) || segments.length < 2) {
440
- return undefined;
441
- }
442
- return segments.slice(0, -1).join("/");
443
- }
444
- function moduleConfigRoot(path) {
445
- return (configFileRoot(path, isTypescriptProjectConfigPath) ??
446
- configFileRoot(path, isGoModuleManifestPath) ??
447
- configFileRoot(path, isCargoManifestPath) ??
448
- configFileRoot(path, isMavenPomPath));
449
- }
450
- function analyzerOwnershipRoot(path) {
451
- const root = normalizeGraphPath(path).replace(/\/+$/, "");
452
- if (root.length === 0 ||
453
- root === "." ||
454
- root === ".." ||
455
- root.startsWith("../") ||
456
- root.startsWith("/")) {
457
- return undefined;
458
- }
459
- const segments = root.split("/").filter(Boolean);
460
- if (segments.length === 1 &&
461
- BROAD_ANALYZER_OWNERSHIP_ROOTS.has(segments[0])) {
462
- return undefined;
463
- }
464
- return root;
465
- }
466
- function collectPackageOwnershipRoots(groups, graphEdges) {
467
- const roots = new Set();
468
- const addRoot = (path) => {
469
- const root = packageManifestRoot(path);
470
- if (root) {
471
- roots.add(root);
472
- }
473
- };
474
- for (const tasks of groups.values()) {
475
- for (const path of tasks.flatMap((task) => task.file_paths)) {
476
- addRoot(path);
477
- }
478
- }
479
- for (const edge of graphEdges) {
480
- addRoot(edge.from);
481
- addRoot(edge.to);
482
- }
483
- return roots;
484
- }
485
- function ownershipRootForPath(path, ownershipRoots) {
486
- const normalized = normalizeGraphPath(path);
487
- let bestMatch;
488
- for (const root of ownershipRoots) {
489
- if (normalized === `${root}/package.json` ||
490
- normalized === `${root}/tsconfig.json` ||
491
- normalized.startsWith(`${root}/`)) {
492
- if (!bestMatch || root.length > bestMatch.length) {
493
- bestMatch = root;
494
- }
495
- }
496
- }
497
- return bestMatch;
498
- }
499
- function packageOwnershipRootForTasks(tasks, packageRoots) {
500
- if (packageRoots.size === 0) {
501
- return undefined;
502
- }
503
- const rootsForFiles = uniqueTaskFilePaths(tasks).map((path) => ownershipRootForPath(path, packageRoots));
504
- if (rootsForFiles.some((root) => root === undefined)) {
505
- return undefined;
506
- }
507
- const roots = new Set(rootsForFiles);
508
- return roots.size === 1 ? [...roots][0] : undefined;
509
- }
510
- function buildPackageOwnershipClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
511
- const packageRoots = collectPackageOwnershipRoots(groups, graphEdges);
512
- if (packageRoots.size === 0) {
513
- return [];
514
- }
515
- return buildBoundedClusterEdges({
516
- groups,
517
- graphEdges,
518
- rootForTasks: (tasks) => packageOwnershipRootForTasks(tasks, packageRoots),
519
- edgeKind: "package-ownership-link",
520
- edgeConfidence: PACKAGE_OWNERSHIP_CLUSTER_CONFIDENCE,
521
- reasonForCluster: (root, fileCount) => `Package ownership root '${root}' groups ${fileCount} file(s) across bounded package subdirectories.`,
522
- lineIndex,
523
- sizeIndex,
524
- targetPacketTokens,
525
- });
526
- }
527
- function collectModuleOwnershipRoots(groups, graphEdges) {
528
- const roots = new Map();
529
- const addRoot = (path) => {
530
- const root = moduleConfigRoot(path);
531
- if (root) {
532
- roots.set(root, roots.get(root) ?? "project configuration");
533
- }
534
- };
535
- const addAnalyzerRoot = (path) => {
536
- const root = analyzerOwnershipRoot(path);
537
- if (root) {
538
- roots.set(root, roots.get(root) ?? "analyzer ownership hint");
539
- }
540
- };
541
- for (const tasks of groups.values()) {
542
- for (const path of tasks.flatMap((task) => task.file_paths)) {
543
- addRoot(path);
544
- }
545
- }
546
- for (const edge of graphEdges) {
547
- if (edge.kind === ANALYZER_OWNERSHIP_EDGE_KIND) {
548
- addAnalyzerRoot(edge.from);
549
- continue;
550
- }
551
- if (!MODULE_OWNERSHIP_EDGE_KINDS.has(edge.kind ?? "")) {
552
- continue;
553
- }
554
- addRoot(edge.from);
555
- addRoot(edge.to);
556
- }
557
- return roots;
558
- }
559
- function moduleOwnershipRootForTasks(tasks, moduleRoots) {
560
- if (moduleRoots.size === 0) {
561
- return undefined;
562
- }
563
- const rootsForFiles = uniqueTaskFilePaths(tasks).map((path) => ownershipRootForPath(path, moduleRoots));
564
- if (rootsForFiles.some((root) => root === undefined)) {
565
- return undefined;
566
- }
567
- const roots = new Set(rootsForFiles);
568
- return roots.size === 1 ? [...roots][0] : undefined;
569
- }
570
- function buildModuleOwnershipClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
571
- const moduleRoots = collectModuleOwnershipRoots(groups, graphEdges);
572
- if (moduleRoots.size === 0) {
573
- return [];
574
- }
575
- const moduleRootSet = new Set(moduleRoots.keys());
576
- return buildBoundedClusterEdges({
577
- groups,
578
- graphEdges,
579
- rootForTasks: (tasks) => moduleOwnershipRootForTasks(tasks, moduleRootSet),
580
- edgeKind: "module-ownership-link",
581
- edgeConfidence: MODULE_OWNERSHIP_CLUSTER_CONFIDENCE,
582
- reasonForCluster: (root, fileCount) => {
583
- const source = moduleRoots.get(root) ?? "project configuration";
584
- return source === "analyzer ownership hint"
585
- ? `Module ownership root '${root}' from analyzer ownership hint groups ${fileCount} file(s) across bounded subdirectories.`
586
- : `Module ownership root '${root}' from project configuration groups ${fileCount} file(s) across bounded subdirectories.`;
587
- },
588
- lineIndex,
589
- sizeIndex,
590
- targetPacketTokens,
591
- });
592
- }
593
- function buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle) {
594
- const roots = collectEntrypointFlowRoots(graphEdges, graphBundle);
595
- if (roots.size === 0) {
596
- return [];
597
- }
598
- const fileToGroupKeys = buildFileToGroupKeys(groups);
599
- const degreeIndex = buildGraphDegreeIndex(graphEdges);
600
- const representatives = buildRepresentativePathIndex(groups, graphEdges, graphBundle);
601
- const adjacency = new Map();
602
- for (const edge of graphEdges) {
603
- if (edge.direction === "undirected" ||
604
- !isPacketExpansionEdge(edge, degreeIndex)) {
605
- continue;
606
- }
607
- const from = normalizeGraphPath(edge.from);
608
- const edges = adjacency.get(from) ?? [];
609
- edges.push(edge);
610
- adjacency.set(from, edges);
611
- }
612
- for (const edges of adjacency.values()) {
613
- edges.sort(compareGraphEdges);
614
- }
615
- const bridgeEdges = new Map();
616
- const displayPath = (normalized) => representatives.get(normalized) ?? normalized;
617
- for (const root of [...roots].sort((a, b) => a.localeCompare(b))) {
618
- const rootGroups = fileToGroupKeys.get(root);
619
- if (!rootGroups) {
620
- continue;
621
- }
622
- const queue = [
623
- { node: root, path: [root], edges: [] },
624
- ];
625
- const visited = new Set([root]);
626
- while (queue.length > 0) {
627
- const current = queue.shift();
628
- if (!current || current.edges.length >= MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS) {
629
- continue;
630
- }
631
- const outgoing = adjacency.get(current.node) ?? [];
632
- if (outgoing.length > MAX_ENTRYPOINT_FLOW_BRANCHES) {
633
- continue;
634
- }
635
- for (const edge of outgoing) {
636
- const target = normalizeGraphPath(edge.to);
637
- if (current.path.includes(target)) {
638
- continue;
639
- }
640
- const nextEdges = [...current.edges, edge];
641
- const nextPath = [...current.path, target];
642
- const targetGroups = fileToGroupKeys.get(target);
643
- if (targetGroups &&
644
- nextEdges.length > 1 &&
645
- !groupsOverlap(rootGroups, targetGroups)) {
646
- const from = displayPath(root);
647
- const to = displayPath(target);
648
- const intermediates = nextPath.slice(1, -1).map(displayPath);
649
- const confidence = Math.min(...nextEdges.map(graphEdgeConfidence));
650
- const bridgeEdge = {
651
- from,
652
- to,
653
- kind: "entrypoint-flow-link",
654
- direction: "directed",
655
- confidence,
656
- reason: intermediates.length > 0
657
- ? `Entrypoint flow from '${from}' reaches '${to}' via ${intermediates.join(" -> ")}.`
658
- : `Entrypoint flow from '${from}' reaches '${to}'.`,
659
- };
660
- bridgeEdges.set(`${from}\0${to}\0${bridgeEdge.kind}`, bridgeEdge);
661
- }
662
- if (!targetGroups &&
663
- nextEdges.length < MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS &&
664
- !visited.has(target)) {
665
- visited.add(target);
666
- queue.push({
667
- node: target,
668
- path: nextPath,
669
- edges: nextEdges,
670
- });
671
- }
672
- }
673
- }
674
- }
675
- return [...bridgeEdges.values()].sort(compareGraphEdges);
676
- }
677
- function buildPlanningGraphEdges(groups, graphEdges, graphBundle, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
678
- const bridgeEdges = buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle);
679
- const graphWithBridges = bridgeEdges.length > 0 ? [...graphEdges, ...bridgeEdges] : graphEdges;
680
- const subsystemEdges = buildSubsystemClusterEdges(groups, graphWithBridges, lineIndex, sizeIndex, targetPacketTokens);
681
- const graphWithSubsystems = subsystemEdges.length > 0
682
- ? [...graphWithBridges, ...subsystemEdges]
683
- : graphWithBridges;
684
- const packageOwnershipEdges = buildPackageOwnershipClusterEdges(groups, graphWithSubsystems, lineIndex, sizeIndex, targetPacketTokens);
685
- const graphWithPackageOwnership = packageOwnershipEdges.length > 0
686
- ? [...graphWithSubsystems, ...packageOwnershipEdges]
687
- : graphWithSubsystems;
688
- const moduleOwnershipEdges = buildModuleOwnershipClusterEdges(groups, graphWithPackageOwnership, lineIndex, sizeIndex, targetPacketTokens);
689
- return moduleOwnershipEdges.length > 0
690
- ? [...graphWithPackageOwnership, ...moduleOwnershipEdges]
691
- : graphWithPackageOwnership;
692
- }
693
- function compareGraphEdges(a, b) {
694
- const confidenceDelta = graphEdgeConfidence(b) - graphEdgeConfidence(a);
695
- if (confidenceDelta !== 0)
696
- return confidenceDelta;
697
- return (a.from.localeCompare(b.from) ||
698
- a.to.localeCompare(b.to) ||
699
- (a.kind ?? "").localeCompare(b.kind ?? ""));
700
- }
701
- function reviewPacketGraphEdge(edge) {
702
- const result = {
703
- from: edge.from,
704
- to: edge.to,
705
- confidence: graphEdgeConfidence(edge),
706
- };
707
- if (edge.kind)
708
- result.kind = edge.kind;
709
- if (edge.reason)
710
- result.reason = edge.reason;
711
- return result;
712
- }
713
- function roundQuality(value) {
714
- return Math.round(value * 1000) / 1000;
715
- }
716
- function packetEntrypoints(filePaths, graphBundle) {
717
- const fileSet = new Set(filePaths.map(normalizeGraphPath));
718
- const routes = Array.isArray(graphBundle?.graphs.routes)
719
- ? graphBundle.graphs.routes
720
- : [];
721
- return routes
722
- .filter((route) => isRecord(route) &&
723
- typeof route.handler === "string" &&
724
- typeof route.path === "string" &&
725
- fileSet.has(normalizeGraphPath(route.handler)))
726
- .map((route) => {
727
- const method = typeof route.method === "string" ? `${route.method} ` : "";
728
- return `${method}${route.path} -> ${route.handler}`;
729
- })
730
- .sort((a, b) => a.localeCompare(b));
731
- }
732
- function buildPacketGraphContext(filePaths, graphEdges, graphBundle) {
733
- const fileSet = new Set(filePaths.map(normalizeGraphPath));
734
- const internalEdges = [];
735
- const boundaryFiles = new Set();
736
- let boundaryEdgeCount = 0;
737
- for (const edge of graphEdges) {
738
- if (!isConcreteGraphEdge(edge)) {
739
- continue;
740
- }
741
- const fromInPacket = fileSet.has(normalizeGraphPath(edge.from));
742
- const toInPacket = fileSet.has(normalizeGraphPath(edge.to));
743
- if (fromInPacket && toInPacket) {
744
- internalEdges.push(edge);
745
- }
746
- else if (fromInPacket !== toInPacket) {
747
- boundaryEdgeCount += 1;
748
- boundaryFiles.add(fromInPacket ? edge.to : edge.from);
749
- }
750
- }
751
- const internallyConnectedFiles = new Set();
752
- for (const edge of internalEdges) {
753
- internallyConnectedFiles.add(normalizeGraphPath(edge.from));
754
- internallyConnectedFiles.add(normalizeGraphPath(edge.to));
755
- }
756
- const unexplainedFileCount = filePaths.length <= 1
757
- ? 0
758
- : filePaths.filter((path) => !internallyConnectedFiles.has(normalizeGraphPath(path))).length;
759
- const cohesionScore = filePaths.length <= 1
760
- ? 1
761
- : Math.min(1, internalEdges.length / (filePaths.length - 1));
762
- return {
763
- keyEdges: internalEdges
764
- .sort(compareGraphEdges)
765
- .slice(0, MAX_PACKET_KEY_EDGES)
766
- .map(reviewPacketGraphEdge),
767
- boundaryFiles: [...boundaryFiles]
768
- .sort((a, b) => a.localeCompare(b))
769
- .slice(0, MAX_PACKET_BOUNDARY_FILES),
770
- entrypoints: packetEntrypoints(filePaths, graphBundle),
771
- quality: {
772
- cohesion_score: roundQuality(cohesionScore),
773
- internal_edge_count: internalEdges.length,
774
- boundary_edge_count: boundaryEdgeCount,
775
- unexplained_file_count: unexplainedFileCount,
776
- },
777
- };
778
- }
779
45
  function sanitizeSegment(value) {
780
46
  const sanitized = value
781
47
  .replace(/[^a-zA-Z0-9_-]+/g, "-")