@optave/codegraph 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +0 -5
  19. package/src/cfg.js +106 -338
  20. package/src/check.js +3 -3
  21. package/src/cli.js +99 -179
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +269 -694
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -399
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +161 -162
  35. package/src/index.js +34 -1
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +37 -20
  39. package/src/owners.js +132 -132
  40. package/src/queries-cli.js +866 -0
  41. package/src/queries.js +1323 -2267
  42. package/src/result-formatter.js +21 -0
  43. package/src/sequence.js +177 -182
  44. package/src/structure.js +200 -199
  45. package/src/test-filter.js +7 -0
  46. package/src/triage.js +120 -162
  47. package/src/viewer.js +1 -1
@@ -0,0 +1,866 @@
1
+ /**
2
+ * queries-cli.js — CLI display wrappers for query data functions.
3
+ *
4
+ * Each function calls its corresponding *Data() function from queries.js,
5
+ * handles JSON/NDJSON output via outputResult(), then formats human-readable
6
+ * output for the terminal.
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import {
11
+ childrenData,
12
+ contextData,
13
+ diffImpactData,
14
+ diffImpactMermaid,
15
+ explainData,
16
+ exportsData,
17
+ fileDepsData,
18
+ fnDepsData,
19
+ fnImpactData,
20
+ impactAnalysisData,
21
+ kindIcon,
22
+ moduleMapData,
23
+ pathData,
24
+ queryNameData,
25
+ rolesData,
26
+ statsData,
27
+ whereData,
28
+ } from './queries.js';
29
+ import { outputResult } from './result-formatter.js';
30
+
31
+ // ─── symbolPath ─────────────────────────────────────────────────────────
32
+
33
+ export function symbolPath(from, to, customDbPath, opts = {}) {
34
+ const data = pathData(from, to, customDbPath, opts);
35
+ if (outputResult(data, null, opts)) return;
36
+
37
+ if (data.error) {
38
+ console.log(data.error);
39
+ return;
40
+ }
41
+
42
+ if (!data.found) {
43
+ const dir = data.reverse ? 'reverse ' : '';
44
+ console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
45
+ if (data.fromCandidates.length > 1) {
46
+ console.log(
47
+ `\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
48
+ );
49
+ }
50
+ if (data.toCandidates.length > 1) {
51
+ console.log(
52
+ ` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
53
+ );
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (data.hops === 0) {
59
+ console.log(`\n"${from}" and "${to}" resolve to the same symbol (0 hops):`);
60
+ const n = data.path[0];
61
+ console.log(` ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}\n`);
62
+ return;
63
+ }
64
+
65
+ const dir = data.reverse ? ' (reverse)' : '';
66
+ console.log(
67
+ `\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
68
+ );
69
+ for (let i = 0; i < data.path.length; i++) {
70
+ const n = data.path[i];
71
+ const indent = ' '.repeat(i + 1);
72
+ if (i === 0) {
73
+ console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
74
+ } else {
75
+ console.log(
76
+ `${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
77
+ );
78
+ }
79
+ }
80
+
81
+ if (data.alternateCount > 0) {
82
+ console.log(
83
+ `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
84
+ );
85
+ }
86
+ console.log();
87
+ }
88
+
89
+ // ─── stats ──────────────────────────────────────────────────────────────
90
+
91
+ export async function stats(customDbPath, opts = {}) {
92
+ const data = statsData(customDbPath, { noTests: opts.noTests });
93
+
94
+ // Community detection summary (async import for lazy-loading)
95
+ try {
96
+ const { communitySummaryForStats } = await import('./communities.js');
97
+ data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
98
+ } catch {
99
+ /* graphology may not be available */
100
+ }
101
+
102
+ if (outputResult(data, null, opts)) return;
103
+
104
+ // Human-readable output
105
+ console.log('\n# Codegraph Stats\n');
106
+
107
+ // Nodes
108
+ console.log(`Nodes: ${data.nodes.total} total`);
109
+ const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
110
+ const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
111
+ for (let i = 0; i < kindParts.length; i += 3) {
112
+ const row = kindParts
113
+ .slice(i, i + 3)
114
+ .map((p) => p.padEnd(18))
115
+ .join('');
116
+ console.log(` ${row}`);
117
+ }
118
+
119
+ // Edges
120
+ console.log(`\nEdges: ${data.edges.total} total`);
121
+ const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
122
+ const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
123
+ for (let i = 0; i < edgeParts.length; i += 3) {
124
+ const row = edgeParts
125
+ .slice(i, i + 3)
126
+ .map((p) => p.padEnd(18))
127
+ .join('');
128
+ console.log(` ${row}`);
129
+ }
130
+
131
+ // Files
132
+ console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
133
+ const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
134
+ const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
135
+ for (let i = 0; i < langParts.length; i += 3) {
136
+ const row = langParts
137
+ .slice(i, i + 3)
138
+ .map((p) => p.padEnd(18))
139
+ .join('');
140
+ console.log(` ${row}`);
141
+ }
142
+
143
+ // Cycles
144
+ console.log(
145
+ `\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
146
+ );
147
+
148
+ // Hotspots
149
+ if (data.hotspots.length > 0) {
150
+ console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
151
+ for (let i = 0; i < data.hotspots.length; i++) {
152
+ const h = data.hotspots[i];
153
+ console.log(
154
+ ` ${String(i + 1).padStart(2)}. ${h.file.padEnd(35)} fan-in: ${String(h.fanIn).padStart(3)} fan-out: ${String(h.fanOut).padStart(3)}`,
155
+ );
156
+ }
157
+ }
158
+
159
+ // Embeddings
160
+ if (data.embeddings) {
161
+ const e = data.embeddings;
162
+ console.log(
163
+ `\nEmbeddings: ${e.count} vectors (${e.model || 'unknown'}, ${e.dim || '?'}d) built ${e.builtAt || 'unknown'}`,
164
+ );
165
+ } else {
166
+ console.log('\nEmbeddings: not built');
167
+ }
168
+
169
+ // Quality
170
+ if (data.quality) {
171
+ const q = data.quality;
172
+ const cc = q.callerCoverage;
173
+ const cf = q.callConfidence;
174
+ console.log(`\nGraph Quality: ${q.score}/100`);
175
+ console.log(
176
+ ` Caller coverage: ${(cc.ratio * 100).toFixed(1)}% (${cc.covered}/${cc.total} functions have >=1 caller)`,
177
+ );
178
+ console.log(
179
+ ` Call confidence: ${(cf.ratio * 100).toFixed(1)}% (${cf.highConf}/${cf.total} call edges are high-confidence)`,
180
+ );
181
+ if (q.falsePositiveWarnings.length > 0) {
182
+ console.log(' False-positive warnings:');
183
+ for (const fp of q.falsePositiveWarnings) {
184
+ console.log(` ! ${fp.name} (${fp.callerCount} callers) -- ${fp.file}:${fp.line}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Roles
190
+ if (data.roles && Object.keys(data.roles).length > 0) {
191
+ const total = Object.values(data.roles).reduce((a, b) => a + b, 0);
192
+ console.log(`\nRoles: ${total} classified symbols`);
193
+ const roleParts = Object.entries(data.roles)
194
+ .sort((a, b) => b[1] - a[1])
195
+ .map(([k, v]) => `${k} ${v}`);
196
+ for (let i = 0; i < roleParts.length; i += 3) {
197
+ const row = roleParts
198
+ .slice(i, i + 3)
199
+ .map((p) => p.padEnd(18))
200
+ .join('');
201
+ console.log(` ${row}`);
202
+ }
203
+ }
204
+
205
+ // Complexity
206
+ if (data.complexity) {
207
+ const cx = data.complexity;
208
+ const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
209
+ console.log(
210
+ `\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
211
+ );
212
+ }
213
+
214
+ // Communities
215
+ if (data.communities) {
216
+ const cm = data.communities;
217
+ console.log(
218
+ `\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
219
+ );
220
+ }
221
+
222
+ console.log();
223
+ }
224
+
225
+ // ─── queryName ──────────────────────────────────────────────────────────
226
+
227
+ export function queryName(name, customDbPath, opts = {}) {
228
+ const data = queryNameData(name, customDbPath, {
229
+ noTests: opts.noTests,
230
+ limit: opts.limit,
231
+ offset: opts.offset,
232
+ });
233
+ if (outputResult(data, 'results', opts)) return;
234
+
235
+ if (data.results.length === 0) {
236
+ console.log(`No results for "${name}"`);
237
+ return;
238
+ }
239
+
240
+ console.log(`\nResults for "${name}":\n`);
241
+ for (const r of data.results) {
242
+ console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
243
+ if (r.callees.length > 0) {
244
+ console.log(` -> calls/uses:`);
245
+ for (const c of r.callees.slice(0, 15))
246
+ console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
247
+ if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
248
+ }
249
+ if (r.callers.length > 0) {
250
+ console.log(` <- called by:`);
251
+ for (const c of r.callers.slice(0, 15))
252
+ console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
253
+ if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
254
+ }
255
+ console.log();
256
+ }
257
+ }
258
+
259
+ // ─── impactAnalysis ─────────────────────────────────────────────────────
260
+
261
+ export function impactAnalysis(file, customDbPath, opts = {}) {
262
+ const data = impactAnalysisData(file, customDbPath, opts);
263
+ if (outputResult(data, 'sources', opts)) return;
264
+
265
+ if (data.sources.length === 0) {
266
+ console.log(`No file matching "${file}" in graph`);
267
+ return;
268
+ }
269
+
270
+ console.log(`\nImpact analysis for files matching "${file}":\n`);
271
+ for (const s of data.sources) console.log(` # ${s} (source)`);
272
+
273
+ const levels = data.levels;
274
+ if (Object.keys(levels).length === 0) {
275
+ console.log(` No dependents found.`);
276
+ } else {
277
+ for (const level of Object.keys(levels).sort((a, b) => a - b)) {
278
+ const nodes = levels[level];
279
+ console.log(
280
+ `\n ${'--'.repeat(parseInt(level, 10))} Level ${level} (${nodes.length} files):`,
281
+ );
282
+ for (const n of nodes.slice(0, 30))
283
+ console.log(` ${' '.repeat(parseInt(level, 10))}^ ${n.file}`);
284
+ if (nodes.length > 30) console.log(` ... and ${nodes.length - 30} more`);
285
+ }
286
+ }
287
+ console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`);
288
+ }
289
+
290
+ // ─── moduleMap ──────────────────────────────────────────────────────────
291
+
292
+ export function moduleMap(customDbPath, limit = 20, opts = {}) {
293
+ const data = moduleMapData(customDbPath, limit, { noTests: opts.noTests });
294
+ if (outputResult(data, 'topNodes', opts)) return;
295
+
296
+ console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
297
+ const dirs = new Map();
298
+ for (const n of data.topNodes) {
299
+ if (!dirs.has(n.dir)) dirs.set(n.dir, []);
300
+ dirs.get(n.dir).push(n);
301
+ }
302
+ for (const [dir, files] of [...dirs].sort()) {
303
+ console.log(` [${dir}/]`);
304
+ for (const f of files) {
305
+ const coupling = f.inEdges + f.outEdges;
306
+ const bar = '#'.repeat(Math.min(coupling, 40));
307
+ console.log(
308
+ ` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} =${String(coupling).padStart(3)} ${bar}`,
309
+ );
310
+ }
311
+ }
312
+ console.log(
313
+ `\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
314
+ );
315
+ }
316
+
317
+ // ─── fileDeps ───────────────────────────────────────────────────────────
318
+
319
+ export function fileDeps(file, customDbPath, opts = {}) {
320
+ const data = fileDepsData(file, customDbPath, opts);
321
+ if (outputResult(data, 'results', opts)) return;
322
+
323
+ if (data.results.length === 0) {
324
+ console.log(`No file matching "${file}" in graph`);
325
+ return;
326
+ }
327
+
328
+ for (const r of data.results) {
329
+ console.log(`\n# ${r.file}\n`);
330
+ console.log(` -> Imports (${r.imports.length}):`);
331
+ for (const i of r.imports) {
332
+ const typeTag = i.typeOnly ? ' (type-only)' : '';
333
+ console.log(` -> ${i.file}${typeTag}`);
334
+ }
335
+ console.log(`\n <- Imported by (${r.importedBy.length}):`);
336
+ for (const i of r.importedBy) console.log(` <- ${i.file}`);
337
+ if (r.definitions.length > 0) {
338
+ console.log(`\n Definitions (${r.definitions.length}):`);
339
+ for (const d of r.definitions.slice(0, 30))
340
+ console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
341
+ if (r.definitions.length > 30) console.log(` ... and ${r.definitions.length - 30} more`);
342
+ }
343
+ console.log();
344
+ }
345
+ }
346
+
347
+ // ─── fnDeps ─────────────────────────────────────────────────────────────
348
+
349
+ export function fnDeps(name, customDbPath, opts = {}) {
350
+ const data = fnDepsData(name, customDbPath, opts);
351
+ if (outputResult(data, 'results', opts)) return;
352
+
353
+ if (data.results.length === 0) {
354
+ console.log(`No function/method/class matching "${name}"`);
355
+ return;
356
+ }
357
+
358
+ for (const r of data.results) {
359
+ console.log(`\n${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}\n`);
360
+ if (r.callees.length > 0) {
361
+ console.log(` -> Calls (${r.callees.length}):`);
362
+ for (const c of r.callees)
363
+ console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
364
+ }
365
+ if (r.callers.length > 0) {
366
+ console.log(`\n <- Called by (${r.callers.length}):`);
367
+ for (const c of r.callers) {
368
+ const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
369
+ console.log(` <- ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
370
+ }
371
+ }
372
+ for (const [d, fns] of Object.entries(r.transitiveCallers)) {
373
+ console.log(
374
+ `\n ${'<-'.repeat(parseInt(d, 10))} Transitive callers (depth ${d}, ${fns.length}):`,
375
+ );
376
+ for (const n of fns.slice(0, 20))
377
+ console.log(
378
+ ` ${' '.repeat(parseInt(d, 10) - 1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`,
379
+ );
380
+ if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
381
+ }
382
+ if (r.callees.length === 0 && r.callers.length === 0) {
383
+ console.log(` (no call edges found -- may be invoked dynamically or via re-exports)`);
384
+ }
385
+ console.log();
386
+ }
387
+ }
388
+
389
+ // ─── context ────────────────────────────────────────────────────────────
390
+
391
+ export function context(name, customDbPath, opts = {}) {
392
+ const data = contextData(name, customDbPath, opts);
393
+ if (outputResult(data, 'results', opts)) return;
394
+
395
+ if (data.results.length === 0) {
396
+ console.log(`No function/method/class matching "${name}"`);
397
+ return;
398
+ }
399
+
400
+ for (const r of data.results) {
401
+ const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
402
+ const roleTag = r.role ? ` [${r.role}]` : '';
403
+ console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
404
+
405
+ // Signature
406
+ if (r.signature) {
407
+ console.log('## Type/Shape Info');
408
+ if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
409
+ if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
410
+ console.log();
411
+ }
412
+
413
+ // Children
414
+ if (r.children && r.children.length > 0) {
415
+ console.log(`## Children (${r.children.length})`);
416
+ for (const c of r.children) {
417
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
418
+ }
419
+ console.log();
420
+ }
421
+
422
+ // Complexity
423
+ if (r.complexity) {
424
+ const cx = r.complexity;
425
+ const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
426
+ console.log('## Complexity');
427
+ console.log(
428
+ ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
429
+ );
430
+ console.log();
431
+ }
432
+
433
+ // Source
434
+ if (r.source) {
435
+ console.log('## Source');
436
+ for (const line of r.source.split('\n')) {
437
+ console.log(` ${line}`);
438
+ }
439
+ console.log();
440
+ }
441
+
442
+ // Callees
443
+ if (r.callees.length > 0) {
444
+ console.log(`## Direct Dependencies (${r.callees.length})`);
445
+ for (const c of r.callees) {
446
+ const summary = c.summary ? ` — ${c.summary}` : '';
447
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
448
+ if (c.source) {
449
+ for (const line of c.source.split('\n').slice(0, 10)) {
450
+ console.log(` | ${line}`);
451
+ }
452
+ }
453
+ }
454
+ console.log();
455
+ }
456
+
457
+ // Callers
458
+ if (r.callers.length > 0) {
459
+ console.log(`## Callers (${r.callers.length})`);
460
+ for (const c of r.callers) {
461
+ const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
462
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
463
+ }
464
+ console.log();
465
+ }
466
+
467
+ // Related tests
468
+ if (r.relatedTests.length > 0) {
469
+ console.log('## Related Tests');
470
+ for (const t of r.relatedTests) {
471
+ console.log(` ${t.file} — ${t.testCount} tests`);
472
+ for (const tn of t.testNames) {
473
+ console.log(` - ${tn}`);
474
+ }
475
+ if (t.source) {
476
+ console.log(' Source:');
477
+ for (const line of t.source.split('\n').slice(0, 20)) {
478
+ console.log(` | ${line}`);
479
+ }
480
+ }
481
+ }
482
+ console.log();
483
+ }
484
+
485
+ if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
486
+ console.log(
487
+ ' (no call edges or tests found — may be invoked dynamically or via re-exports)',
488
+ );
489
+ console.log();
490
+ }
491
+ }
492
+ }
493
+
494
+ // ─── children ───────────────────────────────────────────────────────────
495
+
496
+ export function children(name, customDbPath, opts = {}) {
497
+ const data = childrenData(name, customDbPath, opts);
498
+ if (outputResult(data, 'results', opts)) return;
499
+
500
+ if (data.results.length === 0) {
501
+ console.log(`No symbol matching "${name}"`);
502
+ return;
503
+ }
504
+ for (const r of data.results) {
505
+ console.log(`\n${kindIcon(r.kind)} ${r.name} ${r.file}:${r.line}`);
506
+ if (r.children.length === 0) {
507
+ console.log(' (no children)');
508
+ } else {
509
+ for (const c of r.children) {
510
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ // ─── explain ────────────────────────────────────────────────────────────
517
+
518
+ export function explain(target, customDbPath, opts = {}) {
519
+ const data = explainData(target, customDbPath, opts);
520
+ if (outputResult(data, 'results', opts)) return;
521
+
522
+ if (data.results.length === 0) {
523
+ console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
524
+ return;
525
+ }
526
+
527
+ if (data.kind === 'file') {
528
+ for (const r of data.results) {
529
+ const publicCount = r.publicApi.length;
530
+ const internalCount = r.internal.length;
531
+ const lineInfo = r.lineCount ? `${r.lineCount} lines, ` : '';
532
+ console.log(`\n# ${r.file}`);
533
+ console.log(
534
+ ` ${lineInfo}${r.symbolCount} symbols (${publicCount} exported, ${internalCount} internal)`,
535
+ );
536
+
537
+ if (r.imports.length > 0) {
538
+ console.log(` Imports: ${r.imports.map((i) => i.file).join(', ')}`);
539
+ }
540
+ if (r.importedBy.length > 0) {
541
+ console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
542
+ }
543
+
544
+ if (r.publicApi.length > 0) {
545
+ console.log(`\n## Exported`);
546
+ for (const s of r.publicApi) {
547
+ const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
548
+ const roleTag = s.role ? ` [${s.role}]` : '';
549
+ const summary = s.summary ? ` -- ${s.summary}` : '';
550
+ console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
551
+ }
552
+ }
553
+
554
+ if (r.internal.length > 0) {
555
+ console.log(`\n## Internal`);
556
+ for (const s of r.internal) {
557
+ const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
558
+ const roleTag = s.role ? ` [${s.role}]` : '';
559
+ const summary = s.summary ? ` -- ${s.summary}` : '';
560
+ console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
561
+ }
562
+ }
563
+
564
+ if (r.dataFlow.length > 0) {
565
+ console.log(`\n## Data Flow`);
566
+ for (const df of r.dataFlow) {
567
+ console.log(` ${df.caller} -> ${df.callees.join(', ')}`);
568
+ }
569
+ }
570
+ console.log();
571
+ }
572
+ } else {
573
+ function printFunctionExplain(r, indent = '') {
574
+ const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
575
+ const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
576
+ const summaryPart = r.summary ? ` | ${r.summary}` : '';
577
+ const roleTag = r.role ? ` [${r.role}]` : '';
578
+ const depthLevel = r._depth || 0;
579
+ const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
580
+ console.log(`\n${indent}${heading} ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}`);
581
+ if (lineInfo || r.summary) {
582
+ console.log(`${indent} ${lineInfo}${summaryPart}`);
583
+ }
584
+ if (r.signature) {
585
+ if (r.signature.params != null)
586
+ console.log(`${indent} Parameters: (${r.signature.params})`);
587
+ if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
588
+ }
589
+
590
+ if (r.complexity) {
591
+ const cx = r.complexity;
592
+ const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
593
+ console.log(
594
+ `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
595
+ );
596
+ }
597
+
598
+ if (r.callees.length > 0) {
599
+ console.log(`\n${indent} Calls (${r.callees.length}):`);
600
+ for (const c of r.callees) {
601
+ console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
602
+ }
603
+ }
604
+
605
+ if (r.callers.length > 0) {
606
+ console.log(`\n${indent} Called by (${r.callers.length}):`);
607
+ for (const c of r.callers) {
608
+ console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
609
+ }
610
+ }
611
+
612
+ if (r.relatedTests.length > 0) {
613
+ const label = r.relatedTests.length === 1 ? 'file' : 'files';
614
+ console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
615
+ for (const t of r.relatedTests) {
616
+ console.log(`${indent} ${t.file}`);
617
+ }
618
+ }
619
+
620
+ if (r.callees.length === 0 && r.callers.length === 0) {
621
+ console.log(
622
+ `${indent} (no call edges found -- may be invoked dynamically or via re-exports)`,
623
+ );
624
+ }
625
+
626
+ // Render recursive dependency details
627
+ if (r.depDetails && r.depDetails.length > 0) {
628
+ console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
629
+ for (const dep of r.depDetails) {
630
+ printFunctionExplain(dep, `${indent} `);
631
+ }
632
+ }
633
+ console.log();
634
+ }
635
+
636
+ for (const r of data.results) {
637
+ printFunctionExplain(r);
638
+ }
639
+ }
640
+ }
641
+
642
+ // ─── where ──────────────────────────────────────────────────────────────
643
+
644
+ export function where(target, customDbPath, opts = {}) {
645
+ const data = whereData(target, customDbPath, opts);
646
+ if (outputResult(data, 'results', opts)) return;
647
+
648
+ if (data.results.length === 0) {
649
+ console.log(
650
+ data.mode === 'file'
651
+ ? `No file matching "${target}" in graph`
652
+ : `No symbol matching "${target}" in graph`,
653
+ );
654
+ return;
655
+ }
656
+
657
+ if (data.mode === 'symbol') {
658
+ for (const r of data.results) {
659
+ const roleTag = r.role ? ` [${r.role}]` : '';
660
+ const tag = r.exported ? ' (exported)' : '';
661
+ console.log(`\n${kindIcon(r.kind)} ${r.name}${roleTag} ${r.file}:${r.line}${tag}`);
662
+ if (r.uses.length > 0) {
663
+ const useStrs = r.uses.map((u) => `${u.file}:${u.line}`);
664
+ console.log(` Used in: ${useStrs.join(', ')}`);
665
+ } else {
666
+ console.log(' No uses found');
667
+ }
668
+ }
669
+ } else {
670
+ for (const r of data.results) {
671
+ console.log(`\n# ${r.file}`);
672
+ if (r.symbols.length > 0) {
673
+ const symStrs = r.symbols.map((s) => `${s.name}:${s.line}`);
674
+ console.log(` Symbols: ${symStrs.join(', ')}`);
675
+ }
676
+ if (r.imports.length > 0) {
677
+ console.log(` Imports: ${r.imports.join(', ')}`);
678
+ }
679
+ if (r.importedBy.length > 0) {
680
+ console.log(` Imported by: ${r.importedBy.join(', ')}`);
681
+ }
682
+ if (r.exported.length > 0) {
683
+ console.log(` Exported: ${r.exported.join(', ')}`);
684
+ }
685
+ }
686
+ }
687
+ console.log();
688
+ }
689
+
690
+ // ─── roles ──────────────────────────────────────────────────────────────
691
+
692
+ export function roles(customDbPath, opts = {}) {
693
+ const data = rolesData(customDbPath, opts);
694
+ if (outputResult(data, 'symbols', opts)) return;
695
+
696
+ if (data.count === 0) {
697
+ console.log('No classified symbols found. Run "codegraph build" first.');
698
+ return;
699
+ }
700
+
701
+ const total = data.count;
702
+ console.log(`\nNode roles (${total} symbols):\n`);
703
+
704
+ const summaryParts = Object.entries(data.summary)
705
+ .sort((a, b) => b[1] - a[1])
706
+ .map(([role, count]) => `${role}: ${count}`);
707
+ console.log(` ${summaryParts.join(' ')}\n`);
708
+
709
+ const byRole = {};
710
+ for (const s of data.symbols) {
711
+ if (!byRole[s.role]) byRole[s.role] = [];
712
+ byRole[s.role].push(s);
713
+ }
714
+
715
+ for (const [role, symbols] of Object.entries(byRole)) {
716
+ console.log(`## ${role} (${symbols.length})`);
717
+ for (const s of symbols.slice(0, 30)) {
718
+ console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
719
+ }
720
+ if (symbols.length > 30) {
721
+ console.log(` ... and ${symbols.length - 30} more`);
722
+ }
723
+ console.log();
724
+ }
725
+ }
726
+
727
+ // ─── fileExports ────────────────────────────────────────────────────────
728
+
729
+ export function fileExports(file, customDbPath, opts = {}) {
730
+ const data = exportsData(file, customDbPath, opts);
731
+ if (outputResult(data, 'results', opts)) return;
732
+
733
+ if (data.results.length === 0) {
734
+ if (opts.unused) {
735
+ console.log(`No unused exports found for "${file}".`);
736
+ } else {
737
+ console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
738
+ }
739
+ return;
740
+ }
741
+
742
+ if (opts.unused) {
743
+ console.log(
744
+ `\n# ${data.file} — ${data.totalUnused} unused export${data.totalUnused !== 1 ? 's' : ''} (of ${data.totalExported} exported)\n`,
745
+ );
746
+ } else {
747
+ const unusedNote = data.totalUnused > 0 ? ` (${data.totalUnused} unused)` : '';
748
+ console.log(
749
+ `\n# ${data.file} — ${data.totalExported} exported${unusedNote}, ${data.totalInternal} internal\n`,
750
+ );
751
+ }
752
+
753
+ for (const sym of data.results) {
754
+ const icon = kindIcon(sym.kind);
755
+ const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
756
+ const role = sym.role ? ` [${sym.role}]` : '';
757
+ console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
758
+ if (sym.consumers.length === 0) {
759
+ console.log(' (no consumers)');
760
+ } else {
761
+ for (const c of sym.consumers) {
762
+ console.log(` <- ${c.name} (${c.file}:${c.line})`);
763
+ }
764
+ }
765
+ }
766
+
767
+ if (data.reexports.length > 0) {
768
+ console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
769
+ }
770
+ console.log();
771
+ }
772
+
773
+ // ─── fnImpact ───────────────────────────────────────────────────────────
774
+
775
+ export function fnImpact(name, customDbPath, opts = {}) {
776
+ const data = fnImpactData(name, customDbPath, opts);
777
+ if (outputResult(data, 'results', opts)) return;
778
+
779
+ if (data.results.length === 0) {
780
+ console.log(`No function/method/class matching "${name}"`);
781
+ return;
782
+ }
783
+
784
+ for (const r of data.results) {
785
+ console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
786
+ if (Object.keys(r.levels).length === 0) {
787
+ console.log(` No callers found.`);
788
+ } else {
789
+ for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
790
+ const l = parseInt(level, 10);
791
+ console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
792
+ for (const f of fns.slice(0, 20))
793
+ console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
794
+ if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
795
+ }
796
+ }
797
+ console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`);
798
+ }
799
+ }
800
+
801
+ // ─── diffImpact ─────────────────────────────────────────────────────────
802
+
803
+ export function diffImpact(customDbPath, opts = {}) {
804
+ if (opts.format === 'mermaid') {
805
+ console.log(diffImpactMermaid(customDbPath, opts));
806
+ return;
807
+ }
808
+ const data = diffImpactData(customDbPath, opts);
809
+ if (opts.format === 'json') opts = { ...opts, json: true };
810
+ if (outputResult(data, 'affectedFunctions', opts)) return;
811
+
812
+ if (data.error) {
813
+ console.log(data.error);
814
+ return;
815
+ }
816
+ if (data.changedFiles === 0) {
817
+ console.log('No changes detected.');
818
+ return;
819
+ }
820
+ if (data.affectedFunctions.length === 0) {
821
+ console.log(
822
+ ' No function-level changes detected (changes may be in imports, types, or config).',
823
+ );
824
+ return;
825
+ }
826
+
827
+ console.log(`\ndiff-impact: ${data.changedFiles} files changed\n`);
828
+ console.log(` ${data.affectedFunctions.length} functions changed:\n`);
829
+ for (const fn of data.affectedFunctions) {
830
+ console.log(` ${kindIcon(fn.kind)} ${fn.name} -- ${fn.file}:${fn.line}`);
831
+ if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
832
+ }
833
+ if (data.historicallyCoupled && data.historicallyCoupled.length > 0) {
834
+ console.log('\n Historically coupled (not in static graph):\n');
835
+ for (const c of data.historicallyCoupled) {
836
+ const pct = `${(c.jaccard * 100).toFixed(0)}%`;
837
+ console.log(
838
+ ` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
839
+ );
840
+ }
841
+ }
842
+ if (data.ownership) {
843
+ console.log(`\n Affected owners: ${data.ownership.affectedOwners.join(', ')}`);
844
+ console.log(` Suggested reviewers: ${data.ownership.suggestedReviewers.join(', ')}`);
845
+ }
846
+ if (data.boundaryViolations && data.boundaryViolations.length > 0) {
847
+ console.log(`\n Boundary violations (${data.boundaryViolationCount}):\n`);
848
+ for (const v of data.boundaryViolations) {
849
+ console.log(` [${v.name}] ${v.file} -> ${v.targetFile}`);
850
+ if (v.message) console.log(` ${v.message}`);
851
+ }
852
+ }
853
+ if (data.summary) {
854
+ let summaryLine = `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files`;
855
+ if (data.summary.historicallyCoupledCount > 0) {
856
+ summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
857
+ }
858
+ if (data.summary.ownersAffected > 0) {
859
+ summaryLine += `, ${data.summary.ownersAffected} owners affected`;
860
+ }
861
+ if (data.summary.boundaryViolationCount > 0) {
862
+ summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
863
+ }
864
+ console.log(`${summaryLine}\n`);
865
+ }
866
+ }