@optave/codegraph 2.5.1 → 2.6.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 +119 -49
- package/package.json +8 -7
- package/src/audit.js +423 -0
- package/src/batch.js +90 -0
- package/src/boundaries.js +346 -0
- package/src/builder.js +66 -2
- package/src/check.js +432 -0
- package/src/cli.js +361 -6
- package/src/cochange.js +5 -2
- package/src/communities.js +7 -1
- package/src/complexity.js +116 -9
- package/src/config.js +10 -0
- package/src/embedder.js +350 -38
- package/src/flow.js +4 -4
- package/src/index.js +28 -1
- package/src/manifesto.js +69 -1
- package/src/mcp.js +347 -19
- package/src/owners.js +359 -0
- package/src/paginate.js +35 -0
- package/src/queries.js +233 -19
- package/src/snapshot.js +149 -0
- package/src/structure.js +5 -2
- package/src/triage.js +273 -0
package/src/audit.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit.js — Composite report: explain + impact + health metrics per function.
|
|
3
|
+
*
|
|
4
|
+
* Combines explainData (structure, callers, callees, basic complexity),
|
|
5
|
+
* full function_complexity health metrics, BFS impact analysis, and
|
|
6
|
+
* manifesto threshold breach detection into a single call.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
11
|
+
import { openReadonlyOrFail } from './db.js';
|
|
12
|
+
import { RULE_DEFS } from './manifesto.js';
|
|
13
|
+
import { explainData, isTestFile, kindIcon } from './queries.js';
|
|
14
|
+
|
|
15
|
+
// ─── Threshold resolution ───────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const FUNCTION_RULES = RULE_DEFS.filter((d) => d.level === 'function');
|
|
18
|
+
|
|
19
|
+
function resolveThresholds(customDbPath) {
|
|
20
|
+
try {
|
|
21
|
+
const dbDir = path.dirname(customDbPath);
|
|
22
|
+
const repoRoot = path.resolve(dbDir, '..');
|
|
23
|
+
const cfg = loadConfig(repoRoot);
|
|
24
|
+
const userRules = cfg.manifesto || {};
|
|
25
|
+
const resolved = {};
|
|
26
|
+
for (const def of FUNCTION_RULES) {
|
|
27
|
+
const user = userRules[def.name];
|
|
28
|
+
resolved[def.name] = {
|
|
29
|
+
metric: def.metric,
|
|
30
|
+
warn: user?.warn !== undefined ? user.warn : def.defaults.warn,
|
|
31
|
+
fail: def.reportOnly ? null : user?.fail !== undefined ? user.fail : def.defaults.fail,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
} catch {
|
|
36
|
+
// Fall back to defaults if config loading fails
|
|
37
|
+
const resolved = {};
|
|
38
|
+
for (const def of FUNCTION_RULES) {
|
|
39
|
+
resolved[def.name] = {
|
|
40
|
+
metric: def.metric,
|
|
41
|
+
warn: def.defaults.warn,
|
|
42
|
+
fail: def.reportOnly ? null : def.defaults.fail,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Column name in DB → threshold rule name mapping
|
|
50
|
+
const METRIC_TO_RULE = {
|
|
51
|
+
cognitive: 'cognitive',
|
|
52
|
+
cyclomatic: 'cyclomatic',
|
|
53
|
+
max_nesting: 'maxNesting',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function checkBreaches(row, thresholds) {
|
|
57
|
+
const breaches = [];
|
|
58
|
+
for (const [col, ruleName] of Object.entries(METRIC_TO_RULE)) {
|
|
59
|
+
const t = thresholds[ruleName];
|
|
60
|
+
if (!t) continue;
|
|
61
|
+
const value = row[col];
|
|
62
|
+
if (value == null) continue;
|
|
63
|
+
if (t.fail != null && value >= t.fail) {
|
|
64
|
+
breaches.push({ metric: ruleName, value, threshold: t.fail, level: 'fail' });
|
|
65
|
+
} else if (t.warn != null && value >= t.warn) {
|
|
66
|
+
breaches.push({ metric: ruleName, value, threshold: t.warn, level: 'warn' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return breaches;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── BFS impact (inline, same algorithm as fnImpactData) ────────────
|
|
73
|
+
|
|
74
|
+
function computeImpact(db, nodeId, noTests, maxDepth) {
|
|
75
|
+
const visited = new Set([nodeId]);
|
|
76
|
+
const levels = {};
|
|
77
|
+
let frontier = [nodeId];
|
|
78
|
+
|
|
79
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
80
|
+
const nextFrontier = [];
|
|
81
|
+
for (const fid of frontier) {
|
|
82
|
+
const callers = db
|
|
83
|
+
.prepare(
|
|
84
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
85
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
86
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
87
|
+
)
|
|
88
|
+
.all(fid);
|
|
89
|
+
for (const c of callers) {
|
|
90
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
91
|
+
visited.add(c.id);
|
|
92
|
+
nextFrontier.push(c.id);
|
|
93
|
+
if (!levels[d]) levels[d] = [];
|
|
94
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
frontier = nextFrontier;
|
|
99
|
+
if (frontier.length === 0) break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { totalDependents: visited.size - 1, levels };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Phase 4.4 fields (graceful null fallback) ─────────────────────
|
|
106
|
+
|
|
107
|
+
function readPhase44(db, nodeId) {
|
|
108
|
+
try {
|
|
109
|
+
const row = db
|
|
110
|
+
.prepare('SELECT risk_score, complexity_notes, side_effects FROM nodes WHERE id = ?')
|
|
111
|
+
.get(nodeId);
|
|
112
|
+
if (row) {
|
|
113
|
+
return {
|
|
114
|
+
riskScore: row.risk_score ?? null,
|
|
115
|
+
complexityNotes: row.complexity_notes ?? null,
|
|
116
|
+
sideEffects: row.side_effects ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
/* columns don't exist yet */
|
|
121
|
+
}
|
|
122
|
+
return { riskScore: null, complexityNotes: null, sideEffects: null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── auditData ──────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export function auditData(target, customDbPath, opts = {}) {
|
|
128
|
+
const noTests = opts.noTests || false;
|
|
129
|
+
const maxDepth = opts.depth || 3;
|
|
130
|
+
const file = opts.file;
|
|
131
|
+
const kind = opts.kind;
|
|
132
|
+
|
|
133
|
+
// 1. Get structure via explainData
|
|
134
|
+
const explained = explainData(target, customDbPath, { noTests, depth: 0 });
|
|
135
|
+
|
|
136
|
+
// Apply --file and --kind filters for function targets
|
|
137
|
+
let results = explained.results;
|
|
138
|
+
if (explained.kind === 'function') {
|
|
139
|
+
if (file) results = results.filter((r) => r.file.includes(file));
|
|
140
|
+
if (kind) results = results.filter((r) => r.kind === kind);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (results.length === 0) {
|
|
144
|
+
return { target, kind: explained.kind, functions: [] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Open DB for enrichment
|
|
148
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
149
|
+
const thresholds = resolveThresholds(customDbPath);
|
|
150
|
+
|
|
151
|
+
let functions;
|
|
152
|
+
try {
|
|
153
|
+
if (explained.kind === 'file') {
|
|
154
|
+
// File target: explainData returns file-level info with publicApi + internal
|
|
155
|
+
// We need to enrich each symbol
|
|
156
|
+
functions = [];
|
|
157
|
+
for (const fileResult of results) {
|
|
158
|
+
const allSymbols = [...(fileResult.publicApi || []), ...(fileResult.internal || [])];
|
|
159
|
+
if (kind) {
|
|
160
|
+
const filtered = allSymbols.filter((s) => s.kind === kind);
|
|
161
|
+
for (const sym of filtered) {
|
|
162
|
+
functions.push(enrichSymbol(db, sym, fileResult.file, noTests, maxDepth, thresholds));
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
for (const sym of allSymbols) {
|
|
166
|
+
functions.push(enrichSymbol(db, sym, fileResult.file, noTests, maxDepth, thresholds));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// Function target: explainData returns per-function results
|
|
172
|
+
functions = results.map((r) => enrichFunction(db, r, noTests, maxDepth, thresholds));
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
db.close();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { target, kind: explained.kind, functions };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Enrich a function result from explainData ──────────────────────
|
|
182
|
+
|
|
183
|
+
function enrichFunction(db, r, noTests, maxDepth, thresholds) {
|
|
184
|
+
const nodeRow = db
|
|
185
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND file = ? AND line = ?')
|
|
186
|
+
.get(r.name, r.file, r.line);
|
|
187
|
+
|
|
188
|
+
const nodeId = nodeRow?.id;
|
|
189
|
+
const health = nodeId ? buildHealth(db, nodeId, thresholds) : defaultHealth();
|
|
190
|
+
const impact = nodeId
|
|
191
|
+
? computeImpact(db, nodeId, noTests, maxDepth)
|
|
192
|
+
: { totalDependents: 0, levels: {} };
|
|
193
|
+
const phase44 = nodeId
|
|
194
|
+
? readPhase44(db, nodeId)
|
|
195
|
+
: { riskScore: null, complexityNotes: null, sideEffects: null };
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
name: r.name,
|
|
199
|
+
kind: r.kind,
|
|
200
|
+
file: r.file,
|
|
201
|
+
line: r.line,
|
|
202
|
+
endLine: r.endLine,
|
|
203
|
+
role: r.role,
|
|
204
|
+
lineCount: r.lineCount,
|
|
205
|
+
summary: r.summary,
|
|
206
|
+
signature: r.signature,
|
|
207
|
+
callees: r.callees,
|
|
208
|
+
callers: r.callers,
|
|
209
|
+
relatedTests: r.relatedTests,
|
|
210
|
+
impact,
|
|
211
|
+
health,
|
|
212
|
+
...phase44,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Enrich a symbol from file-level explainData ────────────────────
|
|
217
|
+
|
|
218
|
+
function enrichSymbol(db, sym, file, noTests, maxDepth, thresholds) {
|
|
219
|
+
const nodeRow = db
|
|
220
|
+
.prepare('SELECT id, end_line FROM nodes WHERE name = ? AND file = ? AND line = ?')
|
|
221
|
+
.get(sym.name, file, sym.line);
|
|
222
|
+
|
|
223
|
+
const nodeId = nodeRow?.id;
|
|
224
|
+
const endLine = nodeRow?.end_line || null;
|
|
225
|
+
const lineCount = endLine ? endLine - sym.line + 1 : null;
|
|
226
|
+
|
|
227
|
+
// Get callers/callees for this symbol
|
|
228
|
+
let callees = [];
|
|
229
|
+
let callers = [];
|
|
230
|
+
let relatedTests = [];
|
|
231
|
+
if (nodeId) {
|
|
232
|
+
callees = db
|
|
233
|
+
.prepare(
|
|
234
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
235
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
236
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
237
|
+
)
|
|
238
|
+
.all(nodeId)
|
|
239
|
+
.map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
|
|
240
|
+
|
|
241
|
+
callers = db
|
|
242
|
+
.prepare(
|
|
243
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
244
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
245
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
246
|
+
)
|
|
247
|
+
.all(nodeId)
|
|
248
|
+
.map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
|
|
249
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
250
|
+
|
|
251
|
+
const testCallerRows = db
|
|
252
|
+
.prepare(
|
|
253
|
+
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
254
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
255
|
+
)
|
|
256
|
+
.all(nodeId);
|
|
257
|
+
relatedTests = testCallerRows.filter((r) => isTestFile(r.file)).map((r) => ({ file: r.file }));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const health = nodeId ? buildHealth(db, nodeId, thresholds) : defaultHealth();
|
|
261
|
+
const impact = nodeId
|
|
262
|
+
? computeImpact(db, nodeId, noTests, maxDepth)
|
|
263
|
+
: { totalDependents: 0, levels: {} };
|
|
264
|
+
const phase44 = nodeId
|
|
265
|
+
? readPhase44(db, nodeId)
|
|
266
|
+
: { riskScore: null, complexityNotes: null, sideEffects: null };
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
name: sym.name,
|
|
270
|
+
kind: sym.kind,
|
|
271
|
+
file,
|
|
272
|
+
line: sym.line,
|
|
273
|
+
endLine,
|
|
274
|
+
role: sym.role || null,
|
|
275
|
+
lineCount,
|
|
276
|
+
summary: sym.summary || null,
|
|
277
|
+
signature: sym.signature || null,
|
|
278
|
+
callees,
|
|
279
|
+
callers,
|
|
280
|
+
relatedTests,
|
|
281
|
+
impact,
|
|
282
|
+
health,
|
|
283
|
+
...phase44,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── Build health metrics from function_complexity ──────────────────
|
|
288
|
+
|
|
289
|
+
function buildHealth(db, nodeId, thresholds) {
|
|
290
|
+
try {
|
|
291
|
+
const row = db
|
|
292
|
+
.prepare(
|
|
293
|
+
`SELECT cognitive, cyclomatic, max_nesting, maintainability_index,
|
|
294
|
+
halstead_volume, halstead_difficulty, halstead_effort, halstead_bugs,
|
|
295
|
+
loc, sloc, comment_lines
|
|
296
|
+
FROM function_complexity WHERE node_id = ?`,
|
|
297
|
+
)
|
|
298
|
+
.get(nodeId);
|
|
299
|
+
|
|
300
|
+
if (!row) return defaultHealth();
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
cognitive: row.cognitive,
|
|
304
|
+
cyclomatic: row.cyclomatic,
|
|
305
|
+
maxNesting: row.max_nesting,
|
|
306
|
+
maintainabilityIndex: row.maintainability_index || 0,
|
|
307
|
+
halstead: {
|
|
308
|
+
volume: row.halstead_volume || 0,
|
|
309
|
+
difficulty: row.halstead_difficulty || 0,
|
|
310
|
+
effort: row.halstead_effort || 0,
|
|
311
|
+
bugs: row.halstead_bugs || 0,
|
|
312
|
+
},
|
|
313
|
+
loc: row.loc || 0,
|
|
314
|
+
sloc: row.sloc || 0,
|
|
315
|
+
commentLines: row.comment_lines || 0,
|
|
316
|
+
thresholdBreaches: checkBreaches(row, thresholds),
|
|
317
|
+
};
|
|
318
|
+
} catch {
|
|
319
|
+
/* table may not exist */
|
|
320
|
+
return defaultHealth();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function defaultHealth() {
|
|
325
|
+
return {
|
|
326
|
+
cognitive: null,
|
|
327
|
+
cyclomatic: null,
|
|
328
|
+
maxNesting: null,
|
|
329
|
+
maintainabilityIndex: null,
|
|
330
|
+
halstead: { volume: 0, difficulty: 0, effort: 0, bugs: 0 },
|
|
331
|
+
loc: 0,
|
|
332
|
+
sloc: 0,
|
|
333
|
+
commentLines: 0,
|
|
334
|
+
thresholdBreaches: [],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── CLI formatter ──────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
export function audit(target, customDbPath, opts = {}) {
|
|
341
|
+
const data = auditData(target, customDbPath, opts);
|
|
342
|
+
|
|
343
|
+
if (opts.json) {
|
|
344
|
+
console.log(JSON.stringify(data, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (data.functions.length === 0) {
|
|
349
|
+
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`\n# Audit: ${target} (${data.kind})`);
|
|
354
|
+
console.log(` ${data.functions.length} function(s) analyzed\n`);
|
|
355
|
+
|
|
356
|
+
for (const fn of data.functions) {
|
|
357
|
+
const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
|
|
358
|
+
const roleTag = fn.role ? ` [${fn.role}]` : '';
|
|
359
|
+
console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
|
|
360
|
+
console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
|
|
361
|
+
if (fn.summary) console.log(` ${fn.summary}`);
|
|
362
|
+
if (fn.signature) {
|
|
363
|
+
if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
|
|
364
|
+
if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Health metrics
|
|
368
|
+
if (fn.health.cognitive != null) {
|
|
369
|
+
console.log(`\n Health:`);
|
|
370
|
+
console.log(
|
|
371
|
+
` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
|
|
372
|
+
);
|
|
373
|
+
console.log(` MI: ${fn.health.maintainabilityIndex}`);
|
|
374
|
+
if (fn.health.halstead.volume) {
|
|
375
|
+
console.log(
|
|
376
|
+
` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (fn.health.loc) {
|
|
380
|
+
console.log(
|
|
381
|
+
` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Threshold breaches
|
|
387
|
+
if (fn.health.thresholdBreaches.length > 0) {
|
|
388
|
+
console.log(`\n Threshold Breaches:`);
|
|
389
|
+
for (const b of fn.health.thresholdBreaches) {
|
|
390
|
+
const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
|
|
391
|
+
console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Impact
|
|
396
|
+
console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
|
|
397
|
+
for (const [level, nodes] of Object.entries(fn.impact.levels)) {
|
|
398
|
+
console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Call edges
|
|
402
|
+
if (fn.callees.length > 0) {
|
|
403
|
+
console.log(`\n Calls (${fn.callees.length}):`);
|
|
404
|
+
for (const c of fn.callees) {
|
|
405
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (fn.callers.length > 0) {
|
|
409
|
+
console.log(`\n Called by (${fn.callers.length}):`);
|
|
410
|
+
for (const c of fn.callers) {
|
|
411
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (fn.relatedTests.length > 0) {
|
|
415
|
+
console.log(`\n Tests (${fn.relatedTests.length}):`);
|
|
416
|
+
for (const t of fn.relatedTests) {
|
|
417
|
+
console.log(` ${t.file}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log();
|
|
422
|
+
}
|
|
423
|
+
}
|
package/src/batch.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch query orchestration — run the same query command against multiple targets
|
|
3
|
+
* and return all results in a single JSON payload.
|
|
4
|
+
*
|
|
5
|
+
* Designed for multi-agent swarms that need to dispatch 20+ queries in one call.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { complexityData } from './complexity.js';
|
|
9
|
+
import { flowData } from './flow.js';
|
|
10
|
+
import {
|
|
11
|
+
contextData,
|
|
12
|
+
explainData,
|
|
13
|
+
fileDepsData,
|
|
14
|
+
fnDepsData,
|
|
15
|
+
fnImpactData,
|
|
16
|
+
impactAnalysisData,
|
|
17
|
+
queryNameData,
|
|
18
|
+
whereData,
|
|
19
|
+
} from './queries.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map of supported batch commands → their data function + first-arg semantics.
|
|
23
|
+
* `sig` describes how the target string is passed to the data function:
|
|
24
|
+
* - 'name' → dataFn(target, dbPath, opts)
|
|
25
|
+
* - 'target' → dataFn(target, dbPath, opts)
|
|
26
|
+
* - 'file' → dataFn(target, dbPath, opts)
|
|
27
|
+
* - 'dbOnly' → dataFn(dbPath, { ...opts, target }) (target goes into opts)
|
|
28
|
+
*/
|
|
29
|
+
export const BATCH_COMMANDS = {
|
|
30
|
+
'fn-impact': { fn: fnImpactData, sig: 'name' },
|
|
31
|
+
context: { fn: contextData, sig: 'name' },
|
|
32
|
+
explain: { fn: explainData, sig: 'target' },
|
|
33
|
+
where: { fn: whereData, sig: 'target' },
|
|
34
|
+
query: { fn: queryNameData, sig: 'name' },
|
|
35
|
+
fn: { fn: fnDepsData, sig: 'name' },
|
|
36
|
+
impact: { fn: impactAnalysisData, sig: 'file' },
|
|
37
|
+
deps: { fn: fileDepsData, sig: 'file' },
|
|
38
|
+
flow: { fn: flowData, sig: 'name' },
|
|
39
|
+
complexity: { fn: complexityData, sig: 'dbOnly' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a query command against multiple targets, returning all results.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} command - One of the keys in BATCH_COMMANDS
|
|
46
|
+
* @param {string[]} targets - List of target names/paths
|
|
47
|
+
* @param {string} [customDbPath] - Path to graph.db
|
|
48
|
+
* @param {object} [opts] - Shared options passed to every invocation
|
|
49
|
+
* @returns {{ command: string, total: number, succeeded: number, failed: number, results: object[] }}
|
|
50
|
+
*/
|
|
51
|
+
export function batchData(command, targets, customDbPath, opts = {}) {
|
|
52
|
+
const entry = BATCH_COMMANDS[command];
|
|
53
|
+
if (!entry) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Unknown batch command "${command}". Valid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const results = [];
|
|
60
|
+
let succeeded = 0;
|
|
61
|
+
let failed = 0;
|
|
62
|
+
|
|
63
|
+
for (const target of targets) {
|
|
64
|
+
try {
|
|
65
|
+
let data;
|
|
66
|
+
if (entry.sig === 'dbOnly') {
|
|
67
|
+
// complexityData(dbPath, { ...opts, target })
|
|
68
|
+
data = entry.fn(customDbPath, { ...opts, target });
|
|
69
|
+
} else {
|
|
70
|
+
// All other: dataFn(target, dbPath, opts)
|
|
71
|
+
data = entry.fn(target, customDbPath, opts);
|
|
72
|
+
}
|
|
73
|
+
results.push({ target, ok: true, data });
|
|
74
|
+
succeeded++;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
results.push({ target, ok: false, error: err.message });
|
|
77
|
+
failed++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { command, total: targets.length, succeeded, failed, results };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* CLI wrapper — calls batchData and prints JSON to stdout.
|
|
86
|
+
*/
|
|
87
|
+
export function batch(command, targets, customDbPath, opts = {}) {
|
|
88
|
+
const data = batchData(command, targets, customDbPath, opts);
|
|
89
|
+
console.log(JSON.stringify(data, null, 2));
|
|
90
|
+
}
|