auditor-lambda 0.3.12 → 0.3.14
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 +20 -24
- package/audit-code-wrapper-lib.mjs +52 -53
- package/dist/cli.js +43 -6
- package/dist/coverage.js +3 -1
- package/dist/extractors/disposition.js +8 -1
- package/dist/extractors/graph.d.ts +3 -1
- package/dist/extractors/graph.js +1147 -67
- package/dist/extractors/graphManifestEdges.d.ts +14 -0
- package/dist/extractors/graphManifestEdges.js +1158 -0
- package/dist/extractors/graphPathUtils.d.ts +5 -0
- package/dist/extractors/graphPathUtils.js +75 -0
- package/dist/extractors/pathPatterns.d.ts +1 -0
- package/dist/extractors/pathPatterns.js +3 -0
- package/dist/io/artifacts.d.ts +10 -1
- package/dist/io/artifacts.js +23 -3
- package/dist/orchestrator/internalExecutors.d.ts +4 -0
- package/dist/orchestrator/internalExecutors.js +35 -6
- package/dist/orchestrator/reviewPackets.js +1003 -31
- package/dist/orchestrator/syntaxResolutionExecutor.js +34 -0
- package/dist/types/externalAnalyzer.d.ts +9 -0
- package/dist/types/graph.d.ts +3 -0
- package/dist/types/reviewPlanning.d.ts +39 -0
- package/docs/contracts.md +215 -0
- package/docs/development.md +210 -0
- package/docs/handoff.md +204 -0
- package/docs/history.md +40 -0
- package/docs/operator-guide.md +189 -0
- package/docs/product.md +185 -0
- package/docs/release.md +131 -0
- package/package.json +1 -1
- package/schemas/audit_plan_metrics.schema.json +347 -0
- package/schemas/external_analyzer_results.schema.json +35 -0
- package/schemas/graph_bundle.schema.json +47 -2
- package/schemas/review_packets.schema.json +160 -0
- package/skills/audit-code/SKILL.md +7 -3
- package/skills/audit-code/audit-code.prompt.md +4 -1
- package/docs/agent-integrations.md +0 -317
- package/docs/agent-roles.md +0 -69
- package/docs/architecture.md +0 -90
- package/docs/artifacts.md +0 -36
- package/docs/bootstrap-install.md +0 -139
- package/docs/contract.md +0 -54
- package/docs/dispatch-implementation-plan.md +0 -302
- package/docs/field-trial-bug-report.md +0 -237
- package/docs/github-copilot.md +0 -66
- package/docs/model-selection.md +0 -97
- package/docs/next-steps.md +0 -202
- package/docs/packaging.md +0 -120
- package/docs/pipeline.md +0 -152
- package/docs/product-direction.md +0 -154
- package/docs/production-launch-bar.md +0 -92
- package/docs/production-readiness.md +0 -58
- package/docs/releasing.md +0 -145
- package/docs/remediation-baseline.md +0 -75
- package/docs/repo-layout.md +0 -30
- package/docs/run-flow.md +0 -56
- package/docs/session-config.md +0 -319
- package/docs/supervisor.md +0 -100
- package/docs/usage.md +0 -215
- package/docs/windows-setup.md +0 -146
- package/docs/workflow-refactor-brief.md +0 -124
|
@@ -4,6 +4,48 @@ const DEFAULT_MAX_TASKS_PER_PACKET = 0;
|
|
|
4
4
|
const DEFAULT_TARGET_PACKET_LINES = 8000;
|
|
5
5
|
const ESTIMATED_TOKENS_PER_LINE = 4;
|
|
6
6
|
const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
|
|
7
|
+
const PACKET_EXPANSION_MIN_CONFIDENCE = 0.65;
|
|
8
|
+
const HIGH_FAN_DEGREE_THRESHOLD = 12;
|
|
9
|
+
const HIGH_FAN_EXPANSION_CONFIDENCE = 0.99;
|
|
10
|
+
const MAX_PACKET_KEY_EDGES = 8;
|
|
11
|
+
const MAX_PACKET_BOUNDARY_FILES = 12;
|
|
12
|
+
const MAX_WEAK_PACKET_SAMPLES = 12;
|
|
13
|
+
const MAX_WEAK_PACKET_SAMPLE_FILES = 8;
|
|
14
|
+
const WEAK_PACKET_GAP_ORDER = [
|
|
15
|
+
"missing_internal_edges",
|
|
16
|
+
"unexplained_files",
|
|
17
|
+
"partial_cohesion",
|
|
18
|
+
];
|
|
19
|
+
const MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS = 3;
|
|
20
|
+
const MAX_ENTRYPOINT_FLOW_BRANCHES = 8;
|
|
21
|
+
const SUBSYSTEM_CLUSTER_CONFIDENCE = 0.7;
|
|
22
|
+
const PACKAGE_OWNERSHIP_CLUSTER_CONFIDENCE = 0.68;
|
|
23
|
+
const MODULE_OWNERSHIP_CLUSTER_CONFIDENCE = 0.66;
|
|
24
|
+
const ANALYZER_OWNERSHIP_EDGE_KIND = "analyzer-ownership-root-link";
|
|
25
|
+
const MAX_SUBSYSTEM_CLUSTER_GROUPS = 4;
|
|
26
|
+
const MAX_SUBSYSTEM_CLUSTER_TASKS = 8;
|
|
27
|
+
const MAX_SUBSYSTEM_CLUSTER_FILES = 8;
|
|
28
|
+
const MODULE_OWNERSHIP_EDGE_KINDS = new Set([
|
|
29
|
+
"typescript-project-reference-link",
|
|
30
|
+
"go-workspace-module-link",
|
|
31
|
+
"cargo-workspace-member-link",
|
|
32
|
+
"maven-module-link",
|
|
33
|
+
ANALYZER_OWNERSHIP_EDGE_KIND,
|
|
34
|
+
]);
|
|
35
|
+
const BROAD_ANALYZER_OWNERSHIP_ROOTS = new Set([
|
|
36
|
+
"src",
|
|
37
|
+
"lib",
|
|
38
|
+
"app",
|
|
39
|
+
"apps",
|
|
40
|
+
"packages",
|
|
41
|
+
"services",
|
|
42
|
+
"crates",
|
|
43
|
+
"modules",
|
|
44
|
+
"test",
|
|
45
|
+
"tests",
|
|
46
|
+
"spec",
|
|
47
|
+
"specs",
|
|
48
|
+
]);
|
|
7
49
|
function priorityRank(priority) {
|
|
8
50
|
switch (priority) {
|
|
9
51
|
case "high":
|
|
@@ -36,6 +78,16 @@ function packetGroupingKey(task) {
|
|
|
36
78
|
const scope = criticalFlowTag ?? task.unit_id;
|
|
37
79
|
return `${scope}\0${taskFileSignature(task)}`;
|
|
38
80
|
}
|
|
81
|
+
function buildTaskGroups(tasks) {
|
|
82
|
+
const groups = new Map();
|
|
83
|
+
for (const task of tasks) {
|
|
84
|
+
const key = packetGroupingKey(task);
|
|
85
|
+
const group = groups.get(key) ?? [];
|
|
86
|
+
group.push(task);
|
|
87
|
+
groups.set(key, group);
|
|
88
|
+
}
|
|
89
|
+
return groups;
|
|
90
|
+
}
|
|
39
91
|
function normalizeGraphPath(path) {
|
|
40
92
|
return path.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
41
93
|
}
|
|
@@ -59,18 +111,680 @@ function collectGraphEdges(graphBundle) {
|
|
|
59
111
|
if (typeof item.from !== "string" || typeof item.to !== "string") {
|
|
60
112
|
continue;
|
|
61
113
|
}
|
|
62
|
-
|
|
114
|
+
const edge = {
|
|
63
115
|
from: item.from,
|
|
64
116
|
to: item.to,
|
|
65
117
|
kind: typeof item.kind === "string" ? item.kind : undefined,
|
|
66
|
-
}
|
|
118
|
+
};
|
|
119
|
+
if (item.direction === "directed" || item.direction === "undirected") {
|
|
120
|
+
edge.direction = item.direction;
|
|
121
|
+
}
|
|
122
|
+
if (typeof item.confidence === "number" && Number.isFinite(item.confidence)) {
|
|
123
|
+
edge.confidence = Math.min(1, Math.max(0, item.confidence));
|
|
124
|
+
}
|
|
125
|
+
if (typeof item.reason === "string" && item.reason.trim().length > 0) {
|
|
126
|
+
edge.reason = item.reason.trim();
|
|
127
|
+
}
|
|
128
|
+
edges.push(edge);
|
|
67
129
|
}
|
|
68
130
|
}
|
|
69
131
|
return edges;
|
|
70
132
|
}
|
|
71
|
-
function
|
|
133
|
+
function graphEdgeConfidence(edge) {
|
|
134
|
+
if (typeof edge.confidence === "number" && Number.isFinite(edge.confidence)) {
|
|
135
|
+
return Math.min(1, Math.max(0, edge.confidence));
|
|
136
|
+
}
|
|
137
|
+
if (edge.kind === "heuristic-container-edge") {
|
|
138
|
+
return 0.25;
|
|
139
|
+
}
|
|
140
|
+
if (edge.kind?.startsWith("heuristic-")) {
|
|
141
|
+
return 0.5;
|
|
142
|
+
}
|
|
143
|
+
return 0.8;
|
|
144
|
+
}
|
|
145
|
+
function isConcreteGraphEdge(edge) {
|
|
72
146
|
return edge.kind !== "heuristic-container-edge";
|
|
73
147
|
}
|
|
148
|
+
function buildGraphDegreeIndex(edges) {
|
|
149
|
+
const fanIn = new Map();
|
|
150
|
+
const fanOut = new Map();
|
|
151
|
+
for (const edge of edges) {
|
|
152
|
+
if (!isConcreteGraphEdge(edge)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const from = normalizeGraphPath(edge.from);
|
|
156
|
+
const to = normalizeGraphPath(edge.to);
|
|
157
|
+
fanOut.set(from, (fanOut.get(from) ?? 0) + 1);
|
|
158
|
+
fanIn.set(to, (fanIn.get(to) ?? 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
return { fanIn, fanOut };
|
|
161
|
+
}
|
|
162
|
+
function isPacketExpansionEdge(edge, degreeIndex) {
|
|
163
|
+
if (!isConcreteGraphEdge(edge)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const confidence = graphEdgeConfidence(edge);
|
|
167
|
+
if (confidence < PACKET_EXPANSION_MIN_CONFIDENCE) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const fromFanOut = degreeIndex.fanOut.get(normalizeGraphPath(edge.from)) ?? 0;
|
|
171
|
+
const toFanIn = degreeIndex.fanIn.get(normalizeGraphPath(edge.to)) ?? 0;
|
|
172
|
+
const highFanEdge = fromFanOut > HIGH_FAN_DEGREE_THRESHOLD ||
|
|
173
|
+
toFanIn > HIGH_FAN_DEGREE_THRESHOLD;
|
|
174
|
+
return !highFanEdge || confidence >= HIGH_FAN_EXPANSION_CONFIDENCE;
|
|
175
|
+
}
|
|
176
|
+
function buildFileToGroupKeys(groups) {
|
|
177
|
+
const fileToGroupKeys = new Map();
|
|
178
|
+
for (const [key, tasks] of groups) {
|
|
179
|
+
for (const path of new Set(tasks.flatMap((task) => task.file_paths))) {
|
|
180
|
+
const normalized = normalizeGraphPath(path);
|
|
181
|
+
const existing = fileToGroupKeys.get(normalized) ?? new Set();
|
|
182
|
+
existing.add(key);
|
|
183
|
+
fileToGroupKeys.set(normalized, existing);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return fileToGroupKeys;
|
|
187
|
+
}
|
|
188
|
+
function collectEntrypointFlowRoots(graphEdges, graphBundle) {
|
|
189
|
+
const roots = new Set();
|
|
190
|
+
const routes = Array.isArray(graphBundle?.graphs.routes)
|
|
191
|
+
? graphBundle.graphs.routes
|
|
192
|
+
: [];
|
|
193
|
+
for (const route of routes) {
|
|
194
|
+
if (isRecord(route) && typeof route.handler === "string") {
|
|
195
|
+
roots.add(normalizeGraphPath(route.handler));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const edge of graphEdges) {
|
|
199
|
+
if (edge.kind === "route-handler-link") {
|
|
200
|
+
roots.add(normalizeGraphPath(edge.from));
|
|
201
|
+
roots.add(normalizeGraphPath(edge.to));
|
|
202
|
+
}
|
|
203
|
+
else if (edge.kind === "package-entrypoint-link") {
|
|
204
|
+
roots.add(normalizeGraphPath(edge.to));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return roots;
|
|
208
|
+
}
|
|
209
|
+
function buildRepresentativePathIndex(groups, graphEdges, graphBundle) {
|
|
210
|
+
const representatives = new Map();
|
|
211
|
+
const addPath = (path) => {
|
|
212
|
+
const normalized = normalizeGraphPath(path);
|
|
213
|
+
if (!representatives.has(normalized)) {
|
|
214
|
+
representatives.set(normalized, path);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
for (const tasks of groups.values()) {
|
|
218
|
+
for (const path of tasks.flatMap((task) => task.file_paths)) {
|
|
219
|
+
addPath(path);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const edge of graphEdges) {
|
|
223
|
+
addPath(edge.from);
|
|
224
|
+
addPath(edge.to);
|
|
225
|
+
}
|
|
226
|
+
const routes = Array.isArray(graphBundle?.graphs.routes)
|
|
227
|
+
? graphBundle.graphs.routes
|
|
228
|
+
: [];
|
|
229
|
+
for (const route of routes) {
|
|
230
|
+
if (isRecord(route) && typeof route.handler === "string") {
|
|
231
|
+
addPath(route.handler);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return representatives;
|
|
235
|
+
}
|
|
236
|
+
function uniqueTaskFilePaths(tasks) {
|
|
237
|
+
return [...new Set(tasks.flatMap((task) => task.file_paths))].sort((a, b) => a.localeCompare(b));
|
|
238
|
+
}
|
|
239
|
+
function groupsOverlap(a, b) {
|
|
240
|
+
for (const key of a) {
|
|
241
|
+
if (b.has(key)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
function buildGraphConnectedComponentIndex(groups, graphEdges) {
|
|
248
|
+
const groupKeys = [...groups.keys()];
|
|
249
|
+
const parent = new Map(groupKeys.map((key) => [key, key]));
|
|
250
|
+
const fileToGroupKeys = buildFileToGroupKeys(groups);
|
|
251
|
+
const degreeIndex = buildGraphDegreeIndex(graphEdges);
|
|
252
|
+
function find(key) {
|
|
253
|
+
const current = parent.get(key) ?? key;
|
|
254
|
+
if (current === key)
|
|
255
|
+
return key;
|
|
256
|
+
const root = find(current);
|
|
257
|
+
parent.set(key, root);
|
|
258
|
+
return root;
|
|
259
|
+
}
|
|
260
|
+
function union(a, b) {
|
|
261
|
+
const rootA = find(a);
|
|
262
|
+
const rootB = find(b);
|
|
263
|
+
if (rootA === rootB)
|
|
264
|
+
return;
|
|
265
|
+
const [keep, move] = rootA.localeCompare(rootB) <= 0 ? [rootA, rootB] : [rootB, rootA];
|
|
266
|
+
parent.set(move, keep);
|
|
267
|
+
}
|
|
268
|
+
for (const keys of fileToGroupKeys.values()) {
|
|
269
|
+
const [first, ...rest] = [...keys].sort((a, b) => a.localeCompare(b));
|
|
270
|
+
if (!first)
|
|
271
|
+
continue;
|
|
272
|
+
for (const key of rest) {
|
|
273
|
+
union(first, key);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const edge of graphEdges) {
|
|
277
|
+
if (!isPacketExpansionEdge(edge, degreeIndex)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const fromGroups = fileToGroupKeys.get(normalizeGraphPath(edge.from));
|
|
281
|
+
const toGroups = fileToGroupKeys.get(normalizeGraphPath(edge.to));
|
|
282
|
+
if (!fromGroups || !toGroups) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const fromKey of fromGroups) {
|
|
286
|
+
for (const toKey of toGroups) {
|
|
287
|
+
union(fromKey, toKey);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return new Map(groupKeys.map((key) => [key, find(key)]));
|
|
292
|
+
}
|
|
293
|
+
function subsystemRootForPath(path) {
|
|
294
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
295
|
+
const directories = segments.slice(0, -1);
|
|
296
|
+
if (directories.length < 2) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const namespace = directories[0];
|
|
300
|
+
const depth = namespace === "apps" || namespace === "packages"
|
|
301
|
+
? 4
|
|
302
|
+
: namespace === "src" ||
|
|
303
|
+
namespace === "lib" ||
|
|
304
|
+
namespace === "app" ||
|
|
305
|
+
namespace === "tests" ||
|
|
306
|
+
namespace === "test" ||
|
|
307
|
+
namespace === "spec"
|
|
308
|
+
? 3
|
|
309
|
+
: 2;
|
|
310
|
+
if (directories.length < depth) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
return directories.slice(0, depth).join("/");
|
|
314
|
+
}
|
|
315
|
+
function subsystemRootForTasks(tasks) {
|
|
316
|
+
const rootsForFiles = uniqueTaskFilePaths(tasks).map(subsystemRootForPath);
|
|
317
|
+
if (rootsForFiles.some((root) => root === undefined)) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
const roots = new Set(rootsForFiles);
|
|
321
|
+
return roots.size === 1 ? [...roots][0] : undefined;
|
|
322
|
+
}
|
|
323
|
+
function buildBoundedClusterEdges(params) {
|
|
324
|
+
const groupToComponent = buildGraphConnectedComponentIndex(params.groups, params.graphEdges);
|
|
325
|
+
const clusters = new Map();
|
|
326
|
+
for (const [key, tasks] of params.groups) {
|
|
327
|
+
const root = params.rootForTasks(tasks);
|
|
328
|
+
if (!root) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const filePaths = uniqueTaskFilePaths(tasks);
|
|
332
|
+
const cluster = clusters.get(root) ?? [];
|
|
333
|
+
cluster.push({
|
|
334
|
+
component: groupToComponent.get(key) ?? key,
|
|
335
|
+
tasks,
|
|
336
|
+
filePaths,
|
|
337
|
+
representativePath: filePaths[0] ?? root,
|
|
338
|
+
});
|
|
339
|
+
clusters.set(root, cluster);
|
|
340
|
+
}
|
|
341
|
+
const clusterEdges = [];
|
|
342
|
+
for (const [root, entries] of [...clusters.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
343
|
+
const components = new Map();
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
const component = components.get(entry.component) ?? {
|
|
346
|
+
taskCount: 0,
|
|
347
|
+
filePaths: new Set(),
|
|
348
|
+
representativePath: entry.representativePath,
|
|
349
|
+
};
|
|
350
|
+
component.taskCount += entry.tasks.length;
|
|
351
|
+
for (const filePath of entry.filePaths) {
|
|
352
|
+
component.filePaths.add(filePath);
|
|
353
|
+
}
|
|
354
|
+
if (entry.representativePath.localeCompare(component.representativePath) < 0) {
|
|
355
|
+
component.representativePath = entry.representativePath;
|
|
356
|
+
}
|
|
357
|
+
components.set(entry.component, component);
|
|
358
|
+
}
|
|
359
|
+
const componentEntries = [...components.values()].sort((a, b) => a.representativePath.localeCompare(b.representativePath));
|
|
360
|
+
if (componentEntries.length < 2 ||
|
|
361
|
+
componentEntries.length > MAX_SUBSYSTEM_CLUSTER_GROUPS) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const allFiles = new Set(componentEntries.flatMap((entry) => [...entry.filePaths]));
|
|
365
|
+
const totalTasks = componentEntries.reduce((sum, entry) => sum + entry.taskCount, 0);
|
|
366
|
+
const clusterTasks = entries.flatMap((entry) => entry.tasks);
|
|
367
|
+
const totalLines = [...allFiles].reduce((sum, path) => {
|
|
368
|
+
const owner = clusterTasks.find((task) => task.file_paths.includes(path));
|
|
369
|
+
return sum + (owner ? lineCountForPath(owner, path, params.lineIndex) : 0);
|
|
370
|
+
}, 0);
|
|
371
|
+
if (allFiles.size > MAX_SUBSYSTEM_CLUSTER_FILES ||
|
|
372
|
+
totalTasks > MAX_SUBSYSTEM_CLUSTER_TASKS ||
|
|
373
|
+
totalLines > (params.targetPacketLines ?? DEFAULT_TARGET_PACKET_LINES)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (let index = 1; index < componentEntries.length; index++) {
|
|
377
|
+
const previous = componentEntries[index - 1];
|
|
378
|
+
const current = componentEntries[index];
|
|
379
|
+
clusterEdges.push({
|
|
380
|
+
from: previous.representativePath,
|
|
381
|
+
to: current.representativePath,
|
|
382
|
+
kind: params.edgeKind,
|
|
383
|
+
direction: "undirected",
|
|
384
|
+
confidence: params.edgeConfidence,
|
|
385
|
+
reason: params.reasonForCluster(root, allFiles.size),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return clusterEdges.sort(compareGraphEdges);
|
|
390
|
+
}
|
|
391
|
+
function buildSubsystemClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
|
|
392
|
+
return buildBoundedClusterEdges({
|
|
393
|
+
groups,
|
|
394
|
+
graphEdges,
|
|
395
|
+
rootForTasks: subsystemRootForTasks,
|
|
396
|
+
edgeKind: "subsystem-cluster-link",
|
|
397
|
+
edgeConfidence: SUBSYSTEM_CLUSTER_CONFIDENCE,
|
|
398
|
+
reasonForCluster: (root, fileCount) => `Bounded subsystem cluster '${root}' groups ${fileCount} file(s) without stronger graph evidence.`,
|
|
399
|
+
lineIndex,
|
|
400
|
+
targetPacketLines,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function packageManifestRoot(path) {
|
|
404
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
405
|
+
if (segments.at(-1) !== "package.json" || segments.length < 2) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
return segments.slice(0, -1).join("/");
|
|
409
|
+
}
|
|
410
|
+
function isTypescriptProjectConfigPath(path) {
|
|
411
|
+
const basename = normalizeGraphPath(path).split("/").at(-1);
|
|
412
|
+
if (!basename) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return (basename === "tsconfig.json" ||
|
|
416
|
+
(basename.startsWith("tsconfig.") && basename.endsWith(".json")));
|
|
417
|
+
}
|
|
418
|
+
function isGoModuleManifestPath(path) {
|
|
419
|
+
return normalizeGraphPath(path).split("/").at(-1) === "go.mod";
|
|
420
|
+
}
|
|
421
|
+
function isCargoManifestPath(path) {
|
|
422
|
+
return normalizeGraphPath(path).split("/").at(-1) === "cargo.toml";
|
|
423
|
+
}
|
|
424
|
+
function isMavenPomPath(path) {
|
|
425
|
+
return normalizeGraphPath(path).split("/").at(-1) === "pom.xml";
|
|
426
|
+
}
|
|
427
|
+
function typescriptProjectRoot(path) {
|
|
428
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
429
|
+
if (!isTypescriptProjectConfigPath(path) || segments.length < 2) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
return segments.slice(0, -1).join("/");
|
|
433
|
+
}
|
|
434
|
+
function goModuleRoot(path) {
|
|
435
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
436
|
+
if (!isGoModuleManifestPath(path) || segments.length < 2) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
return segments.slice(0, -1).join("/");
|
|
440
|
+
}
|
|
441
|
+
function cargoModuleRoot(path) {
|
|
442
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
443
|
+
if (!isCargoManifestPath(path) || segments.length < 2) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
return segments.slice(0, -1).join("/");
|
|
447
|
+
}
|
|
448
|
+
function mavenModuleRoot(path) {
|
|
449
|
+
const segments = normalizeGraphPath(path).split("/").filter(Boolean);
|
|
450
|
+
if (!isMavenPomPath(path) || segments.length < 2) {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
return segments.slice(0, -1).join("/");
|
|
454
|
+
}
|
|
455
|
+
function moduleConfigRoot(path) {
|
|
456
|
+
return (typescriptProjectRoot(path) ??
|
|
457
|
+
goModuleRoot(path) ??
|
|
458
|
+
cargoModuleRoot(path) ??
|
|
459
|
+
mavenModuleRoot(path));
|
|
460
|
+
}
|
|
461
|
+
function analyzerOwnershipRoot(path) {
|
|
462
|
+
const root = normalizeGraphPath(path).replace(/\/+$/, "");
|
|
463
|
+
if (root.length === 0 ||
|
|
464
|
+
root === "." ||
|
|
465
|
+
root === ".." ||
|
|
466
|
+
root.startsWith("../") ||
|
|
467
|
+
root.startsWith("/")) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
const segments = root.split("/").filter(Boolean);
|
|
471
|
+
if (segments.length === 1 &&
|
|
472
|
+
BROAD_ANALYZER_OWNERSHIP_ROOTS.has(segments[0])) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
return root;
|
|
476
|
+
}
|
|
477
|
+
function collectPackageOwnershipRoots(groups, graphEdges) {
|
|
478
|
+
const roots = new Set();
|
|
479
|
+
const addRoot = (path) => {
|
|
480
|
+
const root = packageManifestRoot(path);
|
|
481
|
+
if (root) {
|
|
482
|
+
roots.add(root);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
for (const tasks of groups.values()) {
|
|
486
|
+
for (const path of tasks.flatMap((task) => task.file_paths)) {
|
|
487
|
+
addRoot(path);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const edge of graphEdges) {
|
|
491
|
+
addRoot(edge.from);
|
|
492
|
+
addRoot(edge.to);
|
|
493
|
+
}
|
|
494
|
+
return roots;
|
|
495
|
+
}
|
|
496
|
+
function ownershipRootForPath(path, ownershipRoots) {
|
|
497
|
+
const normalized = normalizeGraphPath(path);
|
|
498
|
+
let bestMatch;
|
|
499
|
+
for (const root of ownershipRoots) {
|
|
500
|
+
if (normalized === `${root}/package.json` ||
|
|
501
|
+
normalized === `${root}/tsconfig.json` ||
|
|
502
|
+
normalized.startsWith(`${root}/`)) {
|
|
503
|
+
if (!bestMatch || root.length > bestMatch.length) {
|
|
504
|
+
bestMatch = root;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return bestMatch;
|
|
509
|
+
}
|
|
510
|
+
function packageOwnershipRootForTasks(tasks, packageRoots) {
|
|
511
|
+
if (packageRoots.size === 0) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
const rootsForFiles = uniqueTaskFilePaths(tasks).map((path) => ownershipRootForPath(path, packageRoots));
|
|
515
|
+
if (rootsForFiles.some((root) => root === undefined)) {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const roots = new Set(rootsForFiles);
|
|
519
|
+
return roots.size === 1 ? [...roots][0] : undefined;
|
|
520
|
+
}
|
|
521
|
+
function buildPackageOwnershipClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
|
|
522
|
+
const packageRoots = collectPackageOwnershipRoots(groups, graphEdges);
|
|
523
|
+
if (packageRoots.size === 0) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return buildBoundedClusterEdges({
|
|
527
|
+
groups,
|
|
528
|
+
graphEdges,
|
|
529
|
+
rootForTasks: (tasks) => packageOwnershipRootForTasks(tasks, packageRoots),
|
|
530
|
+
edgeKind: "package-ownership-link",
|
|
531
|
+
edgeConfidence: PACKAGE_OWNERSHIP_CLUSTER_CONFIDENCE,
|
|
532
|
+
reasonForCluster: (root, fileCount) => `Package ownership root '${root}' groups ${fileCount} file(s) across bounded package subdirectories.`,
|
|
533
|
+
lineIndex,
|
|
534
|
+
targetPacketLines,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function collectModuleOwnershipRoots(groups, graphEdges) {
|
|
538
|
+
const roots = new Map();
|
|
539
|
+
const addRoot = (path) => {
|
|
540
|
+
const root = moduleConfigRoot(path);
|
|
541
|
+
if (root) {
|
|
542
|
+
roots.set(root, roots.get(root) ?? "project configuration");
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const addAnalyzerRoot = (path) => {
|
|
546
|
+
const root = analyzerOwnershipRoot(path);
|
|
547
|
+
if (root) {
|
|
548
|
+
roots.set(root, roots.get(root) ?? "analyzer ownership hint");
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
for (const tasks of groups.values()) {
|
|
552
|
+
for (const path of tasks.flatMap((task) => task.file_paths)) {
|
|
553
|
+
addRoot(path);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const edge of graphEdges) {
|
|
557
|
+
if (edge.kind === ANALYZER_OWNERSHIP_EDGE_KIND) {
|
|
558
|
+
addAnalyzerRoot(edge.from);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (!MODULE_OWNERSHIP_EDGE_KINDS.has(edge.kind ?? "")) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
addRoot(edge.from);
|
|
565
|
+
addRoot(edge.to);
|
|
566
|
+
}
|
|
567
|
+
return roots;
|
|
568
|
+
}
|
|
569
|
+
function moduleOwnershipRootForTasks(tasks, moduleRoots) {
|
|
570
|
+
if (moduleRoots.size === 0) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
const rootsForFiles = uniqueTaskFilePaths(tasks).map((path) => ownershipRootForPath(path, moduleRoots));
|
|
574
|
+
if (rootsForFiles.some((root) => root === undefined)) {
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
const roots = new Set(rootsForFiles);
|
|
578
|
+
return roots.size === 1 ? [...roots][0] : undefined;
|
|
579
|
+
}
|
|
580
|
+
function buildModuleOwnershipClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
|
|
581
|
+
const moduleRoots = collectModuleOwnershipRoots(groups, graphEdges);
|
|
582
|
+
if (moduleRoots.size === 0) {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
const moduleRootSet = new Set(moduleRoots.keys());
|
|
586
|
+
return buildBoundedClusterEdges({
|
|
587
|
+
groups,
|
|
588
|
+
graphEdges,
|
|
589
|
+
rootForTasks: (tasks) => moduleOwnershipRootForTasks(tasks, moduleRootSet),
|
|
590
|
+
edgeKind: "module-ownership-link",
|
|
591
|
+
edgeConfidence: MODULE_OWNERSHIP_CLUSTER_CONFIDENCE,
|
|
592
|
+
reasonForCluster: (root, fileCount) => {
|
|
593
|
+
const source = moduleRoots.get(root) ?? "project configuration";
|
|
594
|
+
return source === "analyzer ownership hint"
|
|
595
|
+
? `Module ownership root '${root}' from analyzer ownership hint groups ${fileCount} file(s) across bounded subdirectories.`
|
|
596
|
+
: `Module ownership root '${root}' from project configuration groups ${fileCount} file(s) across bounded subdirectories.`;
|
|
597
|
+
},
|
|
598
|
+
lineIndex,
|
|
599
|
+
targetPacketLines,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle) {
|
|
603
|
+
const roots = collectEntrypointFlowRoots(graphEdges, graphBundle);
|
|
604
|
+
if (roots.size === 0) {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
const fileToGroupKeys = buildFileToGroupKeys(groups);
|
|
608
|
+
const degreeIndex = buildGraphDegreeIndex(graphEdges);
|
|
609
|
+
const representatives = buildRepresentativePathIndex(groups, graphEdges, graphBundle);
|
|
610
|
+
const adjacency = new Map();
|
|
611
|
+
for (const edge of graphEdges) {
|
|
612
|
+
if (edge.direction === "undirected" ||
|
|
613
|
+
!isPacketExpansionEdge(edge, degreeIndex)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const from = normalizeGraphPath(edge.from);
|
|
617
|
+
const edges = adjacency.get(from) ?? [];
|
|
618
|
+
edges.push(edge);
|
|
619
|
+
adjacency.set(from, edges);
|
|
620
|
+
}
|
|
621
|
+
for (const edges of adjacency.values()) {
|
|
622
|
+
edges.sort(compareGraphEdges);
|
|
623
|
+
}
|
|
624
|
+
const bridgeEdges = new Map();
|
|
625
|
+
const displayPath = (normalized) => representatives.get(normalized) ?? normalized;
|
|
626
|
+
for (const root of [...roots].sort((a, b) => a.localeCompare(b))) {
|
|
627
|
+
const rootGroups = fileToGroupKeys.get(root);
|
|
628
|
+
if (!rootGroups) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const queue = [
|
|
632
|
+
{ node: root, path: [root], edges: [] },
|
|
633
|
+
];
|
|
634
|
+
const visited = new Set([root]);
|
|
635
|
+
while (queue.length > 0) {
|
|
636
|
+
const current = queue.shift();
|
|
637
|
+
if (!current || current.edges.length >= MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const outgoing = adjacency.get(current.node) ?? [];
|
|
641
|
+
if (outgoing.length > MAX_ENTRYPOINT_FLOW_BRANCHES) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
for (const edge of outgoing) {
|
|
645
|
+
const target = normalizeGraphPath(edge.to);
|
|
646
|
+
if (current.path.includes(target)) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const nextEdges = [...current.edges, edge];
|
|
650
|
+
const nextPath = [...current.path, target];
|
|
651
|
+
const targetGroups = fileToGroupKeys.get(target);
|
|
652
|
+
if (targetGroups &&
|
|
653
|
+
nextEdges.length > 1 &&
|
|
654
|
+
!groupsOverlap(rootGroups, targetGroups)) {
|
|
655
|
+
const from = displayPath(root);
|
|
656
|
+
const to = displayPath(target);
|
|
657
|
+
const intermediates = nextPath.slice(1, -1).map(displayPath);
|
|
658
|
+
const confidence = Math.min(...nextEdges.map(graphEdgeConfidence));
|
|
659
|
+
const bridgeEdge = {
|
|
660
|
+
from,
|
|
661
|
+
to,
|
|
662
|
+
kind: "entrypoint-flow-link",
|
|
663
|
+
direction: "directed",
|
|
664
|
+
confidence,
|
|
665
|
+
reason: intermediates.length > 0
|
|
666
|
+
? `Entrypoint flow from '${from}' reaches '${to}' via ${intermediates.join(" -> ")}.`
|
|
667
|
+
: `Entrypoint flow from '${from}' reaches '${to}'.`,
|
|
668
|
+
};
|
|
669
|
+
bridgeEdges.set(`${from}\0${to}\0${bridgeEdge.kind}`, bridgeEdge);
|
|
670
|
+
}
|
|
671
|
+
if (!targetGroups &&
|
|
672
|
+
nextEdges.length < MAX_ENTRYPOINT_FLOW_BRIDGE_HOPS &&
|
|
673
|
+
!visited.has(target)) {
|
|
674
|
+
visited.add(target);
|
|
675
|
+
queue.push({
|
|
676
|
+
node: target,
|
|
677
|
+
path: nextPath,
|
|
678
|
+
edges: nextEdges,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return [...bridgeEdges.values()].sort(compareGraphEdges);
|
|
685
|
+
}
|
|
686
|
+
function buildPlanningGraphEdges(groups, graphEdges, graphBundle, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
|
|
687
|
+
const bridgeEdges = buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle);
|
|
688
|
+
const graphWithBridges = bridgeEdges.length > 0 ? [...graphEdges, ...bridgeEdges] : graphEdges;
|
|
689
|
+
const subsystemEdges = buildSubsystemClusterEdges(groups, graphWithBridges, lineIndex, targetPacketLines);
|
|
690
|
+
const graphWithSubsystems = subsystemEdges.length > 0
|
|
691
|
+
? [...graphWithBridges, ...subsystemEdges]
|
|
692
|
+
: graphWithBridges;
|
|
693
|
+
const packageOwnershipEdges = buildPackageOwnershipClusterEdges(groups, graphWithSubsystems, lineIndex, targetPacketLines);
|
|
694
|
+
const graphWithPackageOwnership = packageOwnershipEdges.length > 0
|
|
695
|
+
? [...graphWithSubsystems, ...packageOwnershipEdges]
|
|
696
|
+
: graphWithSubsystems;
|
|
697
|
+
const moduleOwnershipEdges = buildModuleOwnershipClusterEdges(groups, graphWithPackageOwnership, lineIndex, targetPacketLines);
|
|
698
|
+
return moduleOwnershipEdges.length > 0
|
|
699
|
+
? [...graphWithPackageOwnership, ...moduleOwnershipEdges]
|
|
700
|
+
: graphWithPackageOwnership;
|
|
701
|
+
}
|
|
702
|
+
function compareGraphEdges(a, b) {
|
|
703
|
+
const confidenceDelta = graphEdgeConfidence(b) - graphEdgeConfidence(a);
|
|
704
|
+
if (confidenceDelta !== 0)
|
|
705
|
+
return confidenceDelta;
|
|
706
|
+
return (a.from.localeCompare(b.from) ||
|
|
707
|
+
a.to.localeCompare(b.to) ||
|
|
708
|
+
(a.kind ?? "").localeCompare(b.kind ?? ""));
|
|
709
|
+
}
|
|
710
|
+
function reviewPacketGraphEdge(edge) {
|
|
711
|
+
const result = {
|
|
712
|
+
from: edge.from,
|
|
713
|
+
to: edge.to,
|
|
714
|
+
confidence: graphEdgeConfidence(edge),
|
|
715
|
+
};
|
|
716
|
+
if (edge.kind)
|
|
717
|
+
result.kind = edge.kind;
|
|
718
|
+
if (edge.reason)
|
|
719
|
+
result.reason = edge.reason;
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
function roundQuality(value) {
|
|
723
|
+
return Math.round(value * 1000) / 1000;
|
|
724
|
+
}
|
|
725
|
+
function packetEntrypoints(filePaths, graphBundle) {
|
|
726
|
+
const fileSet = new Set(filePaths.map(normalizeGraphPath));
|
|
727
|
+
const routes = Array.isArray(graphBundle?.graphs.routes)
|
|
728
|
+
? graphBundle.graphs.routes
|
|
729
|
+
: [];
|
|
730
|
+
return routes
|
|
731
|
+
.filter((route) => isRecord(route) &&
|
|
732
|
+
typeof route.handler === "string" &&
|
|
733
|
+
typeof route.path === "string" &&
|
|
734
|
+
fileSet.has(normalizeGraphPath(route.handler)))
|
|
735
|
+
.map((route) => {
|
|
736
|
+
const method = typeof route.method === "string" ? `${route.method} ` : "";
|
|
737
|
+
return `${method}${route.path} -> ${route.handler}`;
|
|
738
|
+
})
|
|
739
|
+
.sort((a, b) => a.localeCompare(b));
|
|
740
|
+
}
|
|
741
|
+
function buildPacketGraphContext(filePaths, graphEdges, graphBundle) {
|
|
742
|
+
const fileSet = new Set(filePaths.map(normalizeGraphPath));
|
|
743
|
+
const internalEdges = [];
|
|
744
|
+
const boundaryFiles = new Set();
|
|
745
|
+
let boundaryEdgeCount = 0;
|
|
746
|
+
for (const edge of graphEdges) {
|
|
747
|
+
if (!isConcreteGraphEdge(edge)) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const fromInPacket = fileSet.has(normalizeGraphPath(edge.from));
|
|
751
|
+
const toInPacket = fileSet.has(normalizeGraphPath(edge.to));
|
|
752
|
+
if (fromInPacket && toInPacket) {
|
|
753
|
+
internalEdges.push(edge);
|
|
754
|
+
}
|
|
755
|
+
else if (fromInPacket !== toInPacket) {
|
|
756
|
+
boundaryEdgeCount += 1;
|
|
757
|
+
boundaryFiles.add(fromInPacket ? edge.to : edge.from);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const internallyConnectedFiles = new Set();
|
|
761
|
+
for (const edge of internalEdges) {
|
|
762
|
+
internallyConnectedFiles.add(normalizeGraphPath(edge.from));
|
|
763
|
+
internallyConnectedFiles.add(normalizeGraphPath(edge.to));
|
|
764
|
+
}
|
|
765
|
+
const unexplainedFileCount = filePaths.length <= 1
|
|
766
|
+
? 0
|
|
767
|
+
: filePaths.filter((path) => !internallyConnectedFiles.has(normalizeGraphPath(path))).length;
|
|
768
|
+
const cohesionScore = filePaths.length <= 1
|
|
769
|
+
? 1
|
|
770
|
+
: Math.min(1, internalEdges.length / (filePaths.length - 1));
|
|
771
|
+
return {
|
|
772
|
+
keyEdges: internalEdges
|
|
773
|
+
.sort(compareGraphEdges)
|
|
774
|
+
.slice(0, MAX_PACKET_KEY_EDGES)
|
|
775
|
+
.map(reviewPacketGraphEdge),
|
|
776
|
+
boundaryFiles: [...boundaryFiles]
|
|
777
|
+
.sort((a, b) => a.localeCompare(b))
|
|
778
|
+
.slice(0, MAX_PACKET_BOUNDARY_FILES),
|
|
779
|
+
entrypoints: packetEntrypoints(filePaths, graphBundle),
|
|
780
|
+
quality: {
|
|
781
|
+
cohesion_score: roundQuality(cohesionScore),
|
|
782
|
+
internal_edge_count: internalEdges.length,
|
|
783
|
+
boundary_edge_count: boundaryEdgeCount,
|
|
784
|
+
unexplained_file_count: unexplainedFileCount,
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
74
788
|
function sanitizeSegment(value) {
|
|
75
789
|
const sanitized = value
|
|
76
790
|
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
|
@@ -134,10 +848,11 @@ function chunkPacketTasks(tasks, options) {
|
|
|
134
848
|
}
|
|
135
849
|
return chunks;
|
|
136
850
|
}
|
|
137
|
-
function mergeGraphConnectedGroups(groups,
|
|
851
|
+
function mergeGraphConnectedGroups(groups, graphEdges) {
|
|
138
852
|
const groupKeys = [...groups.keys()];
|
|
139
853
|
const parent = new Map(groupKeys.map((key) => [key, key]));
|
|
140
|
-
const fileToGroupKeys =
|
|
854
|
+
const fileToGroupKeys = buildFileToGroupKeys(groups);
|
|
855
|
+
const degreeIndex = buildGraphDegreeIndex(graphEdges);
|
|
141
856
|
function find(key) {
|
|
142
857
|
const current = parent.get(key) ?? key;
|
|
143
858
|
if (current === key)
|
|
@@ -154,14 +869,6 @@ function mergeGraphConnectedGroups(groups, graphBundle) {
|
|
|
154
869
|
const [keep, move] = rootA.localeCompare(rootB) <= 0 ? [rootA, rootB] : [rootB, rootA];
|
|
155
870
|
parent.set(move, keep);
|
|
156
871
|
}
|
|
157
|
-
for (const [key, tasks] of groups) {
|
|
158
|
-
for (const path of new Set(tasks.flatMap((task) => task.file_paths))) {
|
|
159
|
-
const normalized = normalizeGraphPath(path);
|
|
160
|
-
const existing = fileToGroupKeys.get(normalized) ?? new Set();
|
|
161
|
-
existing.add(key);
|
|
162
|
-
fileToGroupKeys.set(normalized, existing);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
872
|
for (const keys of fileToGroupKeys.values()) {
|
|
166
873
|
const [first, ...rest] = [...keys].sort((a, b) => a.localeCompare(b));
|
|
167
874
|
if (!first)
|
|
@@ -170,8 +877,8 @@ function mergeGraphConnectedGroups(groups, graphBundle) {
|
|
|
170
877
|
union(first, key);
|
|
171
878
|
}
|
|
172
879
|
}
|
|
173
|
-
for (const edge of
|
|
174
|
-
if (!isPacketExpansionEdge(edge)) {
|
|
880
|
+
for (const edge of graphEdges) {
|
|
881
|
+
if (!isPacketExpansionEdge(edge, degreeIndex)) {
|
|
175
882
|
continue;
|
|
176
883
|
}
|
|
177
884
|
const fromGroups = fileToGroupKeys.get(normalizeGraphPath(edge.from));
|
|
@@ -194,8 +901,9 @@ function mergeGraphConnectedGroups(groups, graphBundle) {
|
|
|
194
901
|
}
|
|
195
902
|
return [...merged.values()];
|
|
196
903
|
}
|
|
197
|
-
function buildPacket(tasks, packetIndex, lineIndex) {
|
|
904
|
+
function buildPacket(tasks, packetIndex, lineIndex, graphEdges = [], graphBundle) {
|
|
198
905
|
const filePaths = [...new Set(tasks.flatMap((task) => task.file_paths))].sort((a, b) => a.localeCompare(b));
|
|
906
|
+
const graphContext = buildPacketGraphContext(filePaths, graphEdges, graphBundle);
|
|
199
907
|
const fileLineCounts = Object.fromEntries(filePaths.map((path) => {
|
|
200
908
|
const owner = tasks.find((task) => task.file_paths.includes(path));
|
|
201
909
|
return [path, owner ? lineCountForPath(owner, path, lineIndex) : 0];
|
|
@@ -208,6 +916,14 @@ function buildPacket(tasks, packetIndex, lineIndex) {
|
|
|
208
916
|
const tags = [
|
|
209
917
|
...new Set(tasks.flatMap((task) => task.tags ?? [])),
|
|
210
918
|
].sort((a, b) => a.localeCompare(b));
|
|
919
|
+
const baseRationale = tasks.length === 1
|
|
920
|
+
? tasks[0].rationale
|
|
921
|
+
: `Review ${filePaths.length} related file(s) across ${lenses.length} lens(es): ${lenses.join(", ")}.`;
|
|
922
|
+
const graphRationale = graphContext.keyEdges.length > 0
|
|
923
|
+
? ` Key graph edges explain ${graphContext.keyEdges.length} internal relationship(s).`
|
|
924
|
+
: graphContext.boundaryFiles.length > 0
|
|
925
|
+
? ` Boundary context is available for ${graphContext.boundaryFiles.length} adjacent file(s).`
|
|
926
|
+
: "";
|
|
211
927
|
return {
|
|
212
928
|
packet_id: packetIdFor(tasks, packetIndex),
|
|
213
929
|
task_ids: tasks.map((task) => task.task_id),
|
|
@@ -219,25 +935,27 @@ function buildPacket(tasks, packetIndex, lineIndex) {
|
|
|
219
935
|
total_lines: totalLines,
|
|
220
936
|
priority,
|
|
221
937
|
tags: tags.length > 0 ? tags : undefined,
|
|
222
|
-
|
|
223
|
-
?
|
|
224
|
-
:
|
|
938
|
+
entrypoints: graphContext.entrypoints.length > 0
|
|
939
|
+
? graphContext.entrypoints
|
|
940
|
+
: undefined,
|
|
941
|
+
key_edges: graphContext.keyEdges.length > 0 ? graphContext.keyEdges : undefined,
|
|
942
|
+
boundary_files: graphContext.boundaryFiles.length > 0
|
|
943
|
+
? graphContext.boundaryFiles
|
|
944
|
+
: undefined,
|
|
945
|
+
quality: graphContext.quality,
|
|
946
|
+
rationale: `${baseRationale}${graphRationale}`,
|
|
225
947
|
estimated_tokens: ESTIMATED_PACKET_PROMPT_TOKENS + totalLines * ESTIMATED_TOKENS_PER_LINE,
|
|
226
948
|
};
|
|
227
949
|
}
|
|
228
|
-
|
|
950
|
+
function buildReviewPacketPlanningData(tasks, options = {}) {
|
|
229
951
|
const maxTasksPerPacket = options.maxTasksPerPacket ?? DEFAULT_MAX_TASKS_PER_PACKET;
|
|
230
952
|
const targetPacketLines = options.targetPacketLines ?? DEFAULT_TARGET_PACKET_LINES;
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const group = groups.get(key) ?? [];
|
|
235
|
-
group.push(task);
|
|
236
|
-
groups.set(key, group);
|
|
237
|
-
}
|
|
953
|
+
const graphEdges = collectGraphEdges(options.graphBundle);
|
|
954
|
+
const groups = buildTaskGroups(tasks);
|
|
955
|
+
const planningGraphEdges = buildPlanningGraphEdges(groups, graphEdges, options.graphBundle, options.lineIndex, targetPacketLines);
|
|
238
956
|
const packets = [];
|
|
239
957
|
let packetIndex = 0;
|
|
240
|
-
const groupedTasks = mergeGraphConnectedGroups(groups,
|
|
958
|
+
const groupedTasks = mergeGraphConnectedGroups(groups, planningGraphEdges).sort((a, b) => {
|
|
241
959
|
const aPriority = Math.max(...a.map((task) => priorityRank(task.priority)));
|
|
242
960
|
const bPriority = Math.max(...b.map((task) => priorityRank(task.priority)));
|
|
243
961
|
if (aPriority !== bPriority)
|
|
@@ -250,11 +968,19 @@ export function buildReviewPackets(tasks, options = {}) {
|
|
|
250
968
|
maxTasksPerPacket,
|
|
251
969
|
targetPacketLines,
|
|
252
970
|
})) {
|
|
253
|
-
packets.push(buildPacket(chunk, packetIndex, options.lineIndex));
|
|
971
|
+
packets.push(buildPacket(chunk, packetIndex, options.lineIndex, planningGraphEdges, options.graphBundle));
|
|
254
972
|
packetIndex += 1;
|
|
255
973
|
}
|
|
256
974
|
}
|
|
257
|
-
return
|
|
975
|
+
return {
|
|
976
|
+
graphEdges,
|
|
977
|
+
groups,
|
|
978
|
+
planningGraphEdges,
|
|
979
|
+
packets: packets.sort(comparePackets),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
export function buildReviewPackets(tasks, options = {}) {
|
|
983
|
+
return buildReviewPacketPlanningData(tasks, options).packets;
|
|
258
984
|
}
|
|
259
985
|
export function orderTasksForPacketReview(tasks, options = {}) {
|
|
260
986
|
const taskById = new Map(tasks.map((task) => [task.task_id, task]));
|
|
@@ -262,8 +988,253 @@ export function orderTasksForPacketReview(tasks, options = {}) {
|
|
|
262
988
|
.map((taskId) => taskById.get(taskId))
|
|
263
989
|
.filter((task) => task !== undefined));
|
|
264
990
|
}
|
|
991
|
+
function countHighDegreeTaskFiles(degreeMap, taskFiles) {
|
|
992
|
+
let count = 0;
|
|
993
|
+
for (const [path, degree] of degreeMap) {
|
|
994
|
+
if (degree > HIGH_FAN_DEGREE_THRESHOLD && taskFiles.has(path)) {
|
|
995
|
+
count += 1;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return count;
|
|
999
|
+
}
|
|
1000
|
+
function edgeKindKey(edge) {
|
|
1001
|
+
const kind = edge.kind?.trim();
|
|
1002
|
+
return kind && kind.length > 0 ? kind : "unknown";
|
|
1003
|
+
}
|
|
1004
|
+
function edgeIdentity(edge) {
|
|
1005
|
+
return [
|
|
1006
|
+
normalizeGraphPath(edge.from),
|
|
1007
|
+
normalizeGraphPath(edge.to),
|
|
1008
|
+
edgeKindKey(edge),
|
|
1009
|
+
].join("\0");
|
|
1010
|
+
}
|
|
1011
|
+
function incrementEdgeKindCount(counts, edge) {
|
|
1012
|
+
const key = edgeKindKey(edge);
|
|
1013
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1014
|
+
}
|
|
1015
|
+
function sortCountRecord(counts) {
|
|
1016
|
+
return Object.fromEntries(Object.entries(counts).sort((a, b) => a[0].localeCompare(b[0])));
|
|
1017
|
+
}
|
|
1018
|
+
function buildTaskToGroupKey(groups) {
|
|
1019
|
+
const taskToGroupKey = new Map();
|
|
1020
|
+
for (const [groupKey, tasks] of groups) {
|
|
1021
|
+
for (const task of tasks) {
|
|
1022
|
+
taskToGroupKey.set(task.task_id, groupKey);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return taskToGroupKey;
|
|
1026
|
+
}
|
|
1027
|
+
function buildGroupToPacketIds(packets, groups) {
|
|
1028
|
+
const taskToGroupKey = buildTaskToGroupKey(groups);
|
|
1029
|
+
const groupToPacketIds = new Map();
|
|
1030
|
+
for (const packet of packets) {
|
|
1031
|
+
const packetGroupKeys = new Set(packet.task_ids
|
|
1032
|
+
.map((taskId) => taskToGroupKey.get(taskId))
|
|
1033
|
+
.filter((groupKey) => groupKey !== undefined));
|
|
1034
|
+
for (const groupKey of packetGroupKeys) {
|
|
1035
|
+
const packetIds = groupToPacketIds.get(groupKey) ?? new Set();
|
|
1036
|
+
packetIds.add(packet.packet_id);
|
|
1037
|
+
groupToPacketIds.set(groupKey, packetIds);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return groupToPacketIds;
|
|
1041
|
+
}
|
|
1042
|
+
function setsOverlap(a, b) {
|
|
1043
|
+
if (!a || !b) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
for (const value of a) {
|
|
1047
|
+
if (b.has(value)) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
function countMergeEdgeKinds(packets, groups, planningGraphEdges) {
|
|
1054
|
+
const counts = {};
|
|
1055
|
+
const seen = new Set();
|
|
1056
|
+
const fileToGroupKeys = buildFileToGroupKeys(groups);
|
|
1057
|
+
const groupToPacketIds = buildGroupToPacketIds(packets, groups);
|
|
1058
|
+
const degreeIndex = buildGraphDegreeIndex(planningGraphEdges);
|
|
1059
|
+
for (const edge of planningGraphEdges) {
|
|
1060
|
+
if (!isPacketExpansionEdge(edge, degreeIndex)) {
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const fromGroups = fileToGroupKeys.get(normalizeGraphPath(edge.from));
|
|
1064
|
+
const toGroups = fileToGroupKeys.get(normalizeGraphPath(edge.to));
|
|
1065
|
+
if (!fromGroups || !toGroups) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
let mergedDistinctGroups = false;
|
|
1069
|
+
for (const fromKey of fromGroups) {
|
|
1070
|
+
for (const toKey of toGroups) {
|
|
1071
|
+
if (fromKey !== toKey &&
|
|
1072
|
+
setsOverlap(groupToPacketIds.get(fromKey), groupToPacketIds.get(toKey))) {
|
|
1073
|
+
mergedDistinctGroups = true;
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (mergedDistinctGroups) {
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const identity = edgeIdentity(edge);
|
|
1082
|
+
if (mergedDistinctGroups && !seen.has(identity)) {
|
|
1083
|
+
seen.add(identity);
|
|
1084
|
+
incrementEdgeKindCount(counts, edge);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return sortCountRecord(counts);
|
|
1088
|
+
}
|
|
1089
|
+
function buildFileToPacketIds(packets) {
|
|
1090
|
+
const fileToPacketIds = new Map();
|
|
1091
|
+
for (const packet of packets) {
|
|
1092
|
+
for (const path of packet.file_paths) {
|
|
1093
|
+
const normalized = normalizeGraphPath(path);
|
|
1094
|
+
const packetIds = fileToPacketIds.get(normalized) ?? new Set();
|
|
1095
|
+
packetIds.add(packet.packet_id);
|
|
1096
|
+
fileToPacketIds.set(normalized, packetIds);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return fileToPacketIds;
|
|
1100
|
+
}
|
|
1101
|
+
function countBoundaryOnlyEdgeKinds(packets, planningGraphEdges) {
|
|
1102
|
+
const counts = {};
|
|
1103
|
+
const seen = new Set();
|
|
1104
|
+
const fileToPacketIds = buildFileToPacketIds(packets);
|
|
1105
|
+
for (const edge of planningGraphEdges) {
|
|
1106
|
+
if (!isConcreteGraphEdge(edge)) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const fromPacketIds = fileToPacketIds.get(normalizeGraphPath(edge.from));
|
|
1110
|
+
const toPacketIds = fileToPacketIds.get(normalizeGraphPath(edge.to));
|
|
1111
|
+
if (!fromPacketIds && !toPacketIds) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (setsOverlap(fromPacketIds, toPacketIds)) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const identity = edgeIdentity(edge);
|
|
1118
|
+
if (!seen.has(identity)) {
|
|
1119
|
+
seen.add(identity);
|
|
1120
|
+
incrementEdgeKindCount(counts, edge);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return sortCountRecord(counts);
|
|
1124
|
+
}
|
|
1125
|
+
function isWeaklyExplainedPacket(packet) {
|
|
1126
|
+
return (packet.file_paths.length > 1 &&
|
|
1127
|
+
(packet.quality.internal_edge_count === 0 ||
|
|
1128
|
+
packet.quality.cohesion_score < 1 ||
|
|
1129
|
+
packet.quality.unexplained_file_count > 0));
|
|
1130
|
+
}
|
|
1131
|
+
function weaklyExplainedPackets(packets) {
|
|
1132
|
+
return packets.filter(isWeaklyExplainedPacket);
|
|
1133
|
+
}
|
|
1134
|
+
function weaklyExplainedPacketIds(weakPackets) {
|
|
1135
|
+
return weakPackets
|
|
1136
|
+
.map((packet) => packet.packet_id)
|
|
1137
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1138
|
+
}
|
|
1139
|
+
function weakPacketPrimaryGap(packet) {
|
|
1140
|
+
if (packet.quality.internal_edge_count === 0) {
|
|
1141
|
+
return "missing_internal_edges";
|
|
1142
|
+
}
|
|
1143
|
+
if (packet.quality.unexplained_file_count > 0) {
|
|
1144
|
+
return "unexplained_files";
|
|
1145
|
+
}
|
|
1146
|
+
return "partial_cohesion";
|
|
1147
|
+
}
|
|
1148
|
+
function weaklyExplainedGapCounts(weakPackets) {
|
|
1149
|
+
const counts = {
|
|
1150
|
+
missing_internal_edges: 0,
|
|
1151
|
+
unexplained_files: 0,
|
|
1152
|
+
partial_cohesion: 0,
|
|
1153
|
+
};
|
|
1154
|
+
for (const packet of weakPackets) {
|
|
1155
|
+
counts[weakPacketPrimaryGap(packet)] += 1;
|
|
1156
|
+
}
|
|
1157
|
+
return Object.fromEntries(WEAK_PACKET_GAP_ORDER.map((gap) => [gap, counts[gap]]));
|
|
1158
|
+
}
|
|
1159
|
+
function fileExtensionBucket(path) {
|
|
1160
|
+
const basename = normalizeGraphPath(path).split("/").at(-1) ?? "";
|
|
1161
|
+
const extensionStart = basename.lastIndexOf(".");
|
|
1162
|
+
if (extensionStart <= 0 || extensionStart === basename.length - 1) {
|
|
1163
|
+
return "no_extension";
|
|
1164
|
+
}
|
|
1165
|
+
return basename.slice(extensionStart).toLowerCase();
|
|
1166
|
+
}
|
|
1167
|
+
function weaklyExplainedFileExtensionCounts(weakPackets) {
|
|
1168
|
+
const counts = {};
|
|
1169
|
+
const seenPaths = new Set();
|
|
1170
|
+
for (const packet of weakPackets) {
|
|
1171
|
+
for (const path of packet.file_paths) {
|
|
1172
|
+
const normalized = normalizeGraphPath(path);
|
|
1173
|
+
if (seenPaths.has(normalized)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
seenPaths.add(normalized);
|
|
1177
|
+
const extension = fileExtensionBucket(path);
|
|
1178
|
+
counts[extension] = (counts[extension] ?? 0) + 1;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return sortCountRecord(counts);
|
|
1182
|
+
}
|
|
1183
|
+
function weaklyExplainedPacketSamples(weakPackets) {
|
|
1184
|
+
return weakPackets
|
|
1185
|
+
.sort((a, b) => b.quality.unexplained_file_count - a.quality.unexplained_file_count ||
|
|
1186
|
+
a.quality.cohesion_score - b.quality.cohesion_score ||
|
|
1187
|
+
b.file_paths.length - a.file_paths.length ||
|
|
1188
|
+
a.packet_id.localeCompare(b.packet_id))
|
|
1189
|
+
.slice(0, MAX_WEAK_PACKET_SAMPLES)
|
|
1190
|
+
.map((packet) => ({
|
|
1191
|
+
packet_id: packet.packet_id,
|
|
1192
|
+
primary_gap: weakPacketPrimaryGap(packet),
|
|
1193
|
+
file_count: packet.file_paths.length,
|
|
1194
|
+
sample_file_paths: packet.file_paths.slice(0, MAX_WEAK_PACKET_SAMPLE_FILES),
|
|
1195
|
+
cohesion_score: packet.quality.cohesion_score,
|
|
1196
|
+
internal_edge_count: packet.quality.internal_edge_count,
|
|
1197
|
+
boundary_edge_count: packet.quality.boundary_edge_count,
|
|
1198
|
+
unexplained_file_count: packet.quality.unexplained_file_count,
|
|
1199
|
+
}));
|
|
1200
|
+
}
|
|
1201
|
+
function buildPacketQualityMetrics(packets, tasks, graphEdges, planningGraphEdges, groups) {
|
|
1202
|
+
const packetTaskIds = new Set(packets.flatMap((packet) => packet.task_ids));
|
|
1203
|
+
const orphanTaskCount = tasks.filter((task) => !packetTaskIds.has(task.task_id)).length;
|
|
1204
|
+
const degreeIndex = buildGraphDegreeIndex(graphEdges);
|
|
1205
|
+
const taskFiles = new Set(tasks.flatMap((task) => task.file_paths.map(normalizeGraphPath)));
|
|
1206
|
+
const largestUnexplainedPacket = packets.reduce((largest, packet) => !largest ||
|
|
1207
|
+
packet.quality.unexplained_file_count >
|
|
1208
|
+
largest.quality.unexplained_file_count
|
|
1209
|
+
? packet
|
|
1210
|
+
: largest, undefined);
|
|
1211
|
+
const largestUnexplainedFiles = largestUnexplainedPacket?.quality.unexplained_file_count ?? 0;
|
|
1212
|
+
const weakPackets = weaklyExplainedPackets(packets);
|
|
1213
|
+
const weakPacketIds = weaklyExplainedPacketIds(weakPackets);
|
|
1214
|
+
const weakPacketSamples = weaklyExplainedPacketSamples(weakPackets);
|
|
1215
|
+
return {
|
|
1216
|
+
average_cohesion_score: packets.length > 0
|
|
1217
|
+
? roundQuality(packets.reduce((sum, packet) => sum + packet.quality.cohesion_score, 0) / packets.length)
|
|
1218
|
+
: 0,
|
|
1219
|
+
boundary_crossing_count: packets.reduce((sum, packet) => sum + packet.quality.boundary_edge_count, 0),
|
|
1220
|
+
merge_edge_kind_counts: countMergeEdgeKinds(packets, groups, planningGraphEdges),
|
|
1221
|
+
boundary_edge_kind_counts: countBoundaryOnlyEdgeKinds(packets, planningGraphEdges),
|
|
1222
|
+
orphan_task_count: orphanTaskCount,
|
|
1223
|
+
high_fan_in_file_count: countHighDegreeTaskFiles(degreeIndex.fanIn, taskFiles),
|
|
1224
|
+
high_fan_out_file_count: countHighDegreeTaskFiles(degreeIndex.fanOut, taskFiles),
|
|
1225
|
+
weakly_explained_gap_counts: weaklyExplainedGapCounts(weakPackets),
|
|
1226
|
+
weakly_explained_file_extension_counts: weaklyExplainedFileExtensionCounts(weakPackets),
|
|
1227
|
+
weakly_explained_packet_count: weakPacketIds.length,
|
|
1228
|
+
weakly_explained_packet_ids: weakPacketIds,
|
|
1229
|
+
weakly_explained_packet_samples: weakPacketSamples,
|
|
1230
|
+
largest_unexplained_packet_id: largestUnexplainedFiles > 0
|
|
1231
|
+
? largestUnexplainedPacket?.packet_id
|
|
1232
|
+
: undefined,
|
|
1233
|
+
largest_unexplained_packet_files: largestUnexplainedFiles,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
265
1236
|
export function buildAuditPlanMetrics(tasks, options = {}) {
|
|
266
|
-
const packets =
|
|
1237
|
+
const { graphEdges, groups, packets, planningGraphEdges } = buildReviewPacketPlanningData(tasks, options);
|
|
267
1238
|
const taskLineCounts = tasks.map((task) => taskLineCount(task, options.lineIndex));
|
|
268
1239
|
const totalTaskLines = taskLineCounts.reduce((sum, value) => sum + value, 0);
|
|
269
1240
|
const totalPacketLines = packets.reduce((sum, packet) => sum + packet.total_lines, 0);
|
|
@@ -300,6 +1271,7 @@ export function buildAuditPlanMetrics(tasks, options = {}) {
|
|
|
300
1271
|
largest_packet_id: largestPacket?.packet_id,
|
|
301
1272
|
lens_task_counts: lensTaskCounts,
|
|
302
1273
|
priority_task_counts: priorityTaskCounts,
|
|
1274
|
+
packet_quality: buildPacketQualityMetrics(packets, tasks, graphEdges, planningGraphEdges, groups),
|
|
303
1275
|
packet_size: {
|
|
304
1276
|
single_task_packets: packets.filter((packet) => packet.task_ids.length === 1).length,
|
|
305
1277
|
multi_task_packets: packets.filter((packet) => packet.task_ids.length > 1).length,
|