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