@optave/codegraph 3.1.0 → 3.1.2

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 (83) 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/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. package/src/watcher.js +7 -4
package/src/cli.js CHANGED
@@ -3,9 +3,10 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { Command } from 'commander';
6
- import { audit } from './audit.js';
7
- import { BATCH_COMMANDS, batch, multiBatchData, splitTargets } from './batch.js';
6
+ import { BATCH_COMMANDS, multiBatchData, splitTargets } from './batch.js';
8
7
  import { buildGraph } from './builder.js';
8
+ import { audit } from './commands/audit.js';
9
+ import { batch } from './commands/batch.js';
9
10
  import { loadConfig } from './config.js';
10
11
  import { findCycles, formatCycles } from './cycles.js';
11
12
  import { openReadonlyOrFail } from './db.js';
@@ -24,13 +25,13 @@ import {
24
25
  exportMermaid,
25
26
  exportNeo4jCSV,
26
27
  } from './export.js';
28
+ import { outputResult } from './infrastructure/result-formatter.js';
27
29
  import { setVerbose } from './logger.js';
28
- import { printNdjson } from './paginate.js';
30
+ import { EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
29
31
  import {
30
32
  children,
31
33
  context,
32
34
  diffImpact,
33
- EVERY_SYMBOL_KIND,
34
35
  explain,
35
36
  fileDeps,
36
37
  fileExports,
@@ -41,9 +42,8 @@ import {
41
42
  roles,
42
43
  stats,
43
44
  symbolPath,
44
- VALID_ROLES,
45
45
  where,
46
- } from './queries.js';
46
+ } from './queries-cli.js';
47
47
  import {
48
48
  listRepos,
49
49
  pruneRegistry,
@@ -95,6 +95,17 @@ function resolveNoTests(opts) {
95
95
  return config.query?.excludeTests || false;
96
96
  }
97
97
 
98
+ /** Attach the common query options shared by most analysis commands. */
99
+ const QUERY_OPTS = (cmd) =>
100
+ cmd
101
+ .option('-d, --db <path>', 'Path to graph.db')
102
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
103
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
104
+ .option('-j, --json', 'Output as JSON')
105
+ .option('--limit <number>', 'Max results to return')
106
+ .option('--offset <number>', 'Skip N results (default: 0)')
107
+ .option('--ndjson', 'Newline-delimited JSON output');
108
+
98
109
  function formatSize(bytes) {
99
110
  if (bytes < 1024) return `${bytes} B`;
100
111
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -122,10 +133,11 @@ program
122
133
  });
123
134
  });
124
135
 
125
- program
126
- .command('query <name>')
127
- .description('Function-level dependency chain or shortest path between symbols')
128
- .option('-d, --db <path>', 'Path to graph.db')
136
+ QUERY_OPTS(
137
+ program
138
+ .command('query <name>')
139
+ .description('Function-level dependency chain or shortest path between symbols'),
140
+ )
129
141
  .option('--depth <n>', 'Transitive caller depth', '3')
130
142
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
131
143
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
@@ -134,12 +146,6 @@ program
134
146
  .option('--reverse', 'Path mode: follow edges backward')
135
147
  .option('--from-file <path>', 'Path mode: disambiguate source symbol by file')
136
148
  .option('--to-file <path>', 'Path mode: disambiguate target symbol by file')
137
- .option('-T, --no-tests', 'Exclude test/spec files from results')
138
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
139
- .option('-j, --json', 'Output as JSON')
140
- .option('--limit <number>', 'Max results to return')
141
- .option('--offset <number>', 'Skip N results (default: 0)')
142
- .option('--ndjson', 'Newline-delimited JSON output')
143
149
  .action((name, opts) => {
144
150
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
145
151
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -201,25 +207,17 @@ program
201
207
  });
202
208
  });
203
209
 
204
- program
205
- .command('impact <file>')
206
- .description('Show what depends on this file (transitive)')
207
- .option('-d, --db <path>', 'Path to graph.db')
208
- .option('-T, --no-tests', 'Exclude test/spec files from results')
209
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
210
- .option('-j, --json', 'Output as JSON')
211
- .option('--limit <number>', 'Max results to return')
212
- .option('--offset <number>', 'Skip N results (default: 0)')
213
- .option('--ndjson', 'Newline-delimited JSON output')
214
- .action((file, opts) => {
215
- impactAnalysis(file, opts.db, {
216
- noTests: resolveNoTests(opts),
217
- json: opts.json,
218
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
219
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
220
- ndjson: opts.ndjson,
221
- });
210
+ QUERY_OPTS(
211
+ program.command('impact <file>').description('Show what depends on this file (transitive)'),
212
+ ).action((file, opts) => {
213
+ impactAnalysis(file, opts.db, {
214
+ noTests: resolveNoTests(opts),
215
+ json: opts.json,
216
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
217
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
218
+ ndjson: opts.ndjson,
222
219
  });
220
+ });
223
221
 
224
222
  program
225
223
  .command('map')
@@ -247,61 +245,43 @@ program
247
245
  await stats(opts.db, { noTests: resolveNoTests(opts), json: opts.json });
248
246
  });
249
247
 
250
- program
251
- .command('deps <file>')
252
- .description('Show what this file imports and what imports it')
253
- .option('-d, --db <path>', 'Path to graph.db')
254
- .option('-T, --no-tests', 'Exclude test/spec files from results')
255
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
256
- .option('-j, --json', 'Output as JSON')
257
- .option('--limit <number>', 'Max results to return')
258
- .option('--offset <number>', 'Skip N results (default: 0)')
259
- .option('--ndjson', 'Newline-delimited JSON output')
260
- .action((file, opts) => {
261
- fileDeps(file, opts.db, {
262
- noTests: resolveNoTests(opts),
263
- json: opts.json,
264
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
265
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
266
- ndjson: opts.ndjson,
267
- });
248
+ QUERY_OPTS(
249
+ program.command('deps <file>').description('Show what this file imports and what imports it'),
250
+ ).action((file, opts) => {
251
+ fileDeps(file, opts.db, {
252
+ noTests: resolveNoTests(opts),
253
+ json: opts.json,
254
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
255
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
256
+ ndjson: opts.ndjson,
268
257
  });
269
-
270
- program
271
- .command('exports <file>')
272
- .description('Show exported symbols with per-symbol consumers (who calls each export)')
273
- .option('-d, --db <path>', 'Path to graph.db')
274
- .option('-T, --no-tests', 'Exclude test/spec files from results')
275
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
276
- .option('-j, --json', 'Output as JSON')
277
- .option('--limit <number>', 'Max results to return')
278
- .option('--offset <number>', 'Skip N results (default: 0)')
279
- .option('--ndjson', 'Newline-delimited JSON output')
280
- .option('--unused', 'Show only exports with zero consumers')
258
+ });
259
+
260
+ QUERY_OPTS(
261
+ program
262
+ .command('exports <file>')
263
+ .description('Show exported symbols with per-symbol consumers (who calls each export)'),
264
+ )
265
+ .option('--unused', 'Show only exports with zero consumers (dead exports)')
281
266
  .action((file, opts) => {
282
267
  fileExports(file, opts.db, {
283
268
  noTests: resolveNoTests(opts),
284
269
  json: opts.json,
270
+ unused: opts.unused || false,
285
271
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
286
272
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
287
273
  ndjson: opts.ndjson,
288
- unused: opts.unused,
289
274
  });
290
275
  });
291
276
 
292
- program
293
- .command('fn-impact <name>')
294
- .description('Function-level impact: what functions break if this one changes')
295
- .option('-d, --db <path>', 'Path to graph.db')
277
+ QUERY_OPTS(
278
+ program
279
+ .command('fn-impact <name>')
280
+ .description('Function-level impact: what functions break if this one changes'),
281
+ )
296
282
  .option('--depth <n>', 'Max transitive depth', '5')
297
283
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
298
284
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
299
- .option('-T, --no-tests', 'Exclude test/spec files from results')
300
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
301
- .option('-j, --json', 'Output as JSON')
302
- .option('--limit <number>', 'Max results to return')
303
- .option('--offset <number>', 'Skip N results (default: 0)')
304
- .option('--ndjson', 'Newline-delimited JSON output')
305
285
  .action((name, opts) => {
306
286
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
307
287
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -319,21 +299,16 @@ program
319
299
  });
320
300
  });
321
301
 
322
- program
323
- .command('context <name>')
324
- .description('Full context for a function: source, deps, callers, tests, signature')
325
- .option('-d, --db <path>', 'Path to graph.db')
302
+ QUERY_OPTS(
303
+ program
304
+ .command('context <name>')
305
+ .description('Full context for a function: source, deps, callers, tests, signature'),
306
+ )
326
307
  .option('--depth <n>', 'Include callee source up to N levels deep', '0')
327
308
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
328
309
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
329
310
  .option('--no-source', 'Metadata only (skip source extraction)')
330
311
  .option('--with-test-source', 'Include test source code')
331
- .option('-T, --no-tests', 'Exclude test/spec files from results')
332
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
333
- .option('-j, --json', 'Output as JSON')
334
- .option('--limit <number>', 'Max results to return')
335
- .option('--offset <number>', 'Skip N results (default: 0)')
336
- .option('--ndjson', 'Newline-delimited JSON output')
337
312
  .action((name, opts) => {
338
313
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
339
314
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -417,17 +392,12 @@ program
417
392
  });
418
393
  });
419
394
 
420
- program
421
- .command('where [name]')
422
- .description('Find where a symbol is defined and used (minimal, fast lookup)')
423
- .option('-d, --db <path>', 'Path to graph.db')
395
+ QUERY_OPTS(
396
+ program
397
+ .command('where [name]')
398
+ .description('Find where a symbol is defined and used (minimal, fast lookup)'),
399
+ )
424
400
  .option('-f, --file <path>', 'File overview: list symbols, imports, exports')
425
- .option('-T, --no-tests', 'Exclude test/spec files from results')
426
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
427
- .option('-j, --json', 'Output as JSON')
428
- .option('--limit <number>', 'Max results to return')
429
- .option('--offset <number>', 'Skip N results (default: 0)')
430
- .option('--ndjson', 'Newline-delimited JSON output')
431
401
  .action((name, opts) => {
432
402
  if (!name && !opts.file) {
433
403
  console.error('Provide a symbol name or use --file <path>');
@@ -448,15 +418,14 @@ program
448
418
  .command('diff-impact [ref]')
449
419
  .description('Show impact of git changes (unstaged, staged, or vs a ref)')
450
420
  .option('-d, --db <path>', 'Path to graph.db')
451
- .option('--staged', 'Analyze staged changes instead of unstaged')
452
- .option('--depth <n>', 'Max transitive caller depth', '3')
453
421
  .option('-T, --no-tests', 'Exclude test/spec files from results')
454
422
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
455
- .option('-j, --json', 'Output as JSON')
456
- .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
457
423
  .option('--limit <number>', 'Max results to return')
458
424
  .option('--offset <number>', 'Skip N results (default: 0)')
459
425
  .option('--ndjson', 'Newline-delimited JSON output')
426
+ .option('--staged', 'Analyze staged changes instead of unstaged')
427
+ .option('--depth <n>', 'Max transitive caller depth', '3')
428
+ .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
460
429
  .action((ref, opts) => {
461
430
  diffImpact(opts.db, {
462
431
  ref,
@@ -501,7 +470,7 @@ program
501
470
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
502
471
  process.exit(1);
503
472
  }
504
- const { manifesto } = await import('./manifesto.js');
473
+ const { manifesto } = await import('./commands/manifesto.js');
505
474
  manifesto(opts.db, {
506
475
  file: opts.file,
507
476
  kind: opts.kind,
@@ -515,7 +484,7 @@ program
515
484
  }
516
485
 
517
486
  // Diff predicates mode
518
- const { check } = await import('./check.js');
487
+ const { check } = await import('./commands/check.js');
519
488
  check(opts.db, {
520
489
  ref,
521
490
  staged: opts.staged,
@@ -534,7 +503,7 @@ program
534
503
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
535
504
  process.exit(1);
536
505
  }
537
- const { manifesto } = await import('./manifesto.js');
506
+ const { manifesto } = await import('./commands/manifesto.js');
538
507
  manifesto(opts.db, {
539
508
  file: opts.file,
540
509
  kind: opts.kind,
@@ -984,7 +953,7 @@ program
984
953
  .option('--offset <number>', 'Skip N results (default: 0)')
985
954
  .option('--ndjson', 'Newline-delimited JSON output')
986
955
  .action(async (dir, opts) => {
987
- const { structureData, formatStructure } = await import('./structure.js');
956
+ const { structureData, formatStructure } = await import('./commands/structure.js');
988
957
  const data = structureData(opts.db, {
989
958
  directory: dir,
990
959
  depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
@@ -994,11 +963,7 @@ program
994
963
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
995
964
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
996
965
  });
997
- if (opts.ndjson) {
998
- printNdjson(data, 'directories');
999
- } else if (opts.json) {
1000
- console.log(JSON.stringify(data, null, 2));
1001
- } else {
966
+ if (!outputResult(data, 'directories', opts)) {
1002
967
  console.log(formatStructure(data));
1003
968
  }
1004
969
  });
@@ -1049,8 +1014,8 @@ program
1049
1014
  .option('--offset <number>', 'Skip N results (default: 0)')
1050
1015
  .option('--ndjson', 'Newline-delimited JSON output')
1051
1016
  .action(async (file, opts) => {
1052
- const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
1053
- await import('./cochange.js');
1017
+ const { analyzeCoChanges, coChangeData, coChangeTopData } = await import('./cochange.js');
1018
+ const { formatCoChange, formatCoChangeTop } = await import('./commands/cochange.js');
1054
1019
 
1055
1020
  if (opts.analyze) {
1056
1021
  const result = analyzeCoChanges(opts.db, {
@@ -1081,41 +1046,28 @@ program
1081
1046
 
1082
1047
  if (file) {
1083
1048
  const data = coChangeData(file, opts.db, queryOpts);
1084
- if (opts.ndjson) {
1085
- printNdjson(data, 'partners');
1086
- } else if (opts.json) {
1087
- console.log(JSON.stringify(data, null, 2));
1088
- } else {
1049
+ if (!outputResult(data, 'partners', opts)) {
1089
1050
  console.log(formatCoChange(data));
1090
1051
  }
1091
1052
  } else {
1092
1053
  const data = coChangeTopData(opts.db, queryOpts);
1093
- if (opts.ndjson) {
1094
- printNdjson(data, 'pairs');
1095
- } else if (opts.json) {
1096
- console.log(JSON.stringify(data, null, 2));
1097
- } else {
1054
+ if (!outputResult(data, 'pairs', opts)) {
1098
1055
  console.log(formatCoChangeTop(data));
1099
1056
  }
1100
1057
  }
1101
1058
  });
1102
1059
 
1103
- program
1104
- .command('flow [name]')
1105
- .description(
1106
- 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
1107
- )
1060
+ QUERY_OPTS(
1061
+ program
1062
+ .command('flow [name]')
1063
+ .description(
1064
+ 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
1065
+ ),
1066
+ )
1108
1067
  .option('--list', 'List all entry points grouped by type')
1109
1068
  .option('--depth <n>', 'Max forward traversal depth', '10')
1110
- .option('-d, --db <path>', 'Path to graph.db')
1111
1069
  .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1112
1070
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1113
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1114
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1115
- .option('-j, --json', 'Output as JSON')
1116
- .option('--limit <number>', 'Max results to return')
1117
- .option('--offset <number>', 'Skip N results (default: 0)')
1118
- .option('--ndjson', 'Newline-delimited JSON output')
1119
1071
  .action(async (name, opts) => {
1120
1072
  if (!name && !opts.list) {
1121
1073
  console.error('Provide a function/entry point name or use --list to see all entry points.');
@@ -1125,7 +1077,7 @@ program
1125
1077
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1126
1078
  process.exit(1);
1127
1079
  }
1128
- const { flow } = await import('./flow.js');
1080
+ const { flow } = await import('./commands/flow.js');
1129
1081
  flow(name, opts.db, {
1130
1082
  list: opts.list,
1131
1083
  depth: parseInt(opts.depth, 10),
@@ -1139,26 +1091,23 @@ program
1139
1091
  });
1140
1092
  });
1141
1093
 
1142
- program
1143
- .command('sequence <name>')
1144
- .description('Generate a Mermaid sequence diagram from call graph edges (participants = files)')
1094
+ QUERY_OPTS(
1095
+ program
1096
+ .command('sequence <name>')
1097
+ .description(
1098
+ 'Generate a Mermaid sequence diagram from call graph edges (participants = files)',
1099
+ ),
1100
+ )
1145
1101
  .option('--depth <n>', 'Max forward traversal depth', '10')
1146
1102
  .option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
1147
- .option('-d, --db <path>', 'Path to graph.db')
1148
1103
  .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1149
1104
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1150
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1151
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1152
- .option('-j, --json', 'Output as JSON')
1153
- .option('--limit <number>', 'Max results to return')
1154
- .option('--offset <number>', 'Skip N results (default: 0)')
1155
- .option('--ndjson', 'Newline-delimited JSON output')
1156
1105
  .action(async (name, opts) => {
1157
1106
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1158
1107
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1159
1108
  process.exit(1);
1160
1109
  }
1161
- const { sequence } = await import('./sequence.js');
1110
+ const { sequence } = await import('./commands/sequence.js');
1162
1111
  sequence(name, opts.db, {
1163
1112
  depth: parseInt(opts.depth, 10),
1164
1113
  file: opts.file,
@@ -1172,18 +1121,13 @@ program
1172
1121
  });
1173
1122
  });
1174
1123
 
1175
- program
1176
- .command('dataflow <name>')
1177
- .description('Show data flow for a function: parameters, return consumers, mutations')
1178
- .option('-d, --db <path>', 'Path to graph.db')
1124
+ QUERY_OPTS(
1125
+ program
1126
+ .command('dataflow <name>')
1127
+ .description('Show data flow for a function: parameters, return consumers, mutations'),
1128
+ )
1179
1129
  .option('-f, --file <path>', 'Scope to file (partial match)')
1180
1130
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1181
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1182
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1183
- .option('-j, --json', 'Output as JSON')
1184
- .option('--ndjson', 'Newline-delimited JSON output')
1185
- .option('--limit <number>', 'Max results to return')
1186
- .option('--offset <number>', 'Skip N results (default: 0)')
1187
1131
  .option('--impact', 'Show data-dependent blast radius')
1188
1132
  .option('--depth <n>', 'Max traversal depth', '5')
1189
1133
  .action(async (name, opts) => {
@@ -1191,7 +1135,7 @@ program
1191
1135
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1192
1136
  process.exit(1);
1193
1137
  }
1194
- const { dataflow } = await import('./dataflow.js');
1138
+ const { dataflow } = await import('./commands/dataflow.js');
1195
1139
  dataflow(name, opts.db, {
1196
1140
  file: opts.file,
1197
1141
  kind: opts.kind,
@@ -1205,25 +1149,16 @@ program
1205
1149
  });
1206
1150
  });
1207
1151
 
1208
- program
1209
- .command('cfg <name>')
1210
- .description('Show control flow graph for a function')
1211
- .option('-d, --db <path>', 'Path to graph.db')
1152
+ QUERY_OPTS(program.command('cfg <name>').description('Show control flow graph for a function'))
1212
1153
  .option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
1213
1154
  .option('-f, --file <path>', 'Scope to file (partial match)')
1214
1155
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1215
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1216
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1217
- .option('-j, --json', 'Output as JSON')
1218
- .option('--ndjson', 'Newline-delimited JSON output')
1219
- .option('--limit <number>', 'Max results to return')
1220
- .option('--offset <number>', 'Skip N results (default: 0)')
1221
1156
  .action(async (name, opts) => {
1222
1157
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1223
1158
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1224
1159
  process.exit(1);
1225
1160
  }
1226
- const { cfg } = await import('./cfg.js');
1161
+ const { cfg } = await import('./commands/cfg.js');
1227
1162
  cfg(name, opts.db, {
1228
1163
  format: opts.format,
1229
1164
  file: opts.file,
@@ -1260,7 +1195,7 @@ program
1260
1195
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1261
1196
  process.exit(1);
1262
1197
  }
1263
- const { complexity } = await import('./complexity.js');
1198
+ const { complexity } = await import('./commands/complexity.js');
1264
1199
  complexity(opts.db, {
1265
1200
  target,
1266
1201
  limit: parseInt(opts.limit, 10),
@@ -1276,18 +1211,13 @@ program
1276
1211
  });
1277
1212
  });
1278
1213
 
1279
- program
1280
- .command('ast [pattern]')
1281
- .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern')
1282
- .option('-d, --db <path>', 'Path to graph.db')
1214
+ QUERY_OPTS(
1215
+ program
1216
+ .command('ast [pattern]')
1217
+ .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern'),
1218
+ )
1283
1219
  .option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
1284
1220
  .option('-f, --file <path>', 'Scope to file (partial match)')
1285
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1286
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1287
- .option('-j, --json', 'Output as JSON')
1288
- .option('--ndjson', 'Newline-delimited JSON output')
1289
- .option('--limit <number>', 'Max results to return')
1290
- .option('--offset <number>', 'Skip N results (default: 0)')
1291
1221
  .action(async (pattern, opts) => {
1292
1222
  const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
1293
1223
  if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
@@ -1305,21 +1235,16 @@ program
1305
1235
  });
1306
1236
  });
1307
1237
 
1308
- program
1309
- .command('communities')
1310
- .description('Detect natural module boundaries using Louvain community detection')
1238
+ QUERY_OPTS(
1239
+ program
1240
+ .command('communities')
1241
+ .description('Detect natural module boundaries using Louvain community detection'),
1242
+ )
1311
1243
  .option('--functions', 'Function-level instead of file-level')
1312
1244
  .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
1313
1245
  .option('--drift', 'Show only drift analysis')
1314
- .option('-d, --db <path>', 'Path to graph.db')
1315
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1316
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1317
- .option('-j, --json', 'Output as JSON')
1318
- .option('--limit <number>', 'Max results to return')
1319
- .option('--offset <number>', 'Skip N results (default: 0)')
1320
- .option('--ndjson', 'Newline-delimited JSON output')
1321
1246
  .action(async (opts) => {
1322
- const { communities } = await import('./communities.js');
1247
+ const { communities } = await import('./commands/communities.js');
1323
1248
  communities(opts.db, {
1324
1249
  functions: opts.functions,
1325
1250
  resolution: parseFloat(opts.resolution),
@@ -1362,7 +1287,7 @@ program
1362
1287
  .action(async (opts) => {
1363
1288
  if (opts.level === 'file' || opts.level === 'directory') {
1364
1289
  // Delegate to hotspots for file/directory level
1365
- const { hotspotsData, formatHotspots } = await import('./structure.js');
1290
+ const { hotspotsData, formatHotspots } = await import('./commands/structure.js');
1366
1291
  const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
1367
1292
  const data = hotspotsData(opts.db, {
1368
1293
  metric,
@@ -1371,11 +1296,7 @@ program
1371
1296
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1372
1297
  noTests: resolveNoTests(opts),
1373
1298
  });
1374
- if (opts.ndjson) {
1375
- printNdjson(data, 'hotspots');
1376
- } else if (opts.json) {
1377
- console.log(JSON.stringify(data, null, 2));
1378
- } else {
1299
+ if (!outputResult(data, 'hotspots', opts)) {
1379
1300
  console.log(formatHotspots(data));
1380
1301
  }
1381
1302
  return;
@@ -1398,7 +1319,7 @@ program
1398
1319
  process.exit(1);
1399
1320
  }
1400
1321
  }
1401
- const { triage } = await import('./triage.js');
1322
+ const { triage } = await import('./commands/triage.js');
1402
1323
  triage(opts.db, {
1403
1324
  limit: parseInt(opts.limit, 10),
1404
1325
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
@@ -1426,7 +1347,7 @@ program
1426
1347
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1427
1348
  .option('-j, --json', 'Output as JSON')
1428
1349
  .action(async (target, opts) => {
1429
- const { owners } = await import('./owners.js');
1350
+ const { owners } = await import('./commands/owners.js');
1430
1351
  owners(opts.db, {
1431
1352
  owner: opts.owner,
1432
1353
  boundary: opts.boundary,
@@ -1446,7 +1367,7 @@ program
1446
1367
  .option('-j, --json', 'Output as JSON')
1447
1368
  .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
1448
1369
  .action(async (base, target, opts) => {
1449
- const { branchCompare } = await import('./branch-compare.js');
1370
+ const { branchCompare } = await import('./commands/branch-compare.js');
1450
1371
  await branchCompare(base, target, {
1451
1372
  engine: program.opts().engine,
1452
1373
  depth: parseInt(opts.depth, 10),
package/src/cochange.js CHANGED
@@ -10,9 +10,9 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { normalizePath } from './constants.js';
12
12
  import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
+ import { isTestFile } from './infrastructure/test-filter.js';
13
14
  import { warn } from './logger.js';
14
15
  import { paginateResult } from './paginate.js';
15
- import { isTestFile } from './queries.js';
16
16
 
17
17
  /**
18
18
  * Scan git history and return parsed commit data.
@@ -419,44 +419,6 @@ export function coChangeForFiles(files, db, opts = {}) {
419
419
  return results;
420
420
  }
421
421
 
422
- /**
423
- * Format co-change data for CLI output (single file).
424
- */
425
- export function formatCoChange(data) {
426
- if (data.error) return data.error;
427
- if (data.partners.length === 0) return `No co-change partners found for ${data.file}`;
428
-
429
- const lines = [`\nCo-change partners for ${data.file}:\n`];
430
- for (const p of data.partners) {
431
- const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
432
- const commits = `${p.commitCount} commits`.padStart(12);
433
- lines.push(` ${pct} ${commits} ${p.file}`);
434
- }
435
- if (data.meta?.analyzedAt) {
436
- lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
437
- }
438
- return lines.join('\n');
439
- }
440
-
441
- /**
442
- * Format top co-change pairs for CLI output (global view).
443
- */
444
- export function formatCoChangeTop(data) {
445
- if (data.error) return data.error;
446
- if (data.pairs.length === 0) return 'No co-change pairs found.';
447
-
448
- const lines = ['\nTop co-change pairs:\n'];
449
- for (const p of data.pairs) {
450
- const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
451
- const commits = `${p.commitCount} commits`.padStart(12);
452
- lines.push(` ${pct} ${commits} ${p.fileA} <-> ${p.fileB}`);
453
- }
454
- if (data.meta?.analyzedAt) {
455
- lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
456
- }
457
- return lines.join('\n');
458
- }
459
-
460
422
  // ─── Internal Helpers ────────────────────────────────────────────────────
461
423
 
462
424
  function resolveCoChangeFile(db, file) {