@optave/codegraph 3.0.4 → 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 (49) hide show
  1. package/README.md +59 -52
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +9 -10
  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 +274 -159
  19. package/src/cfg.js +111 -341
  20. package/src/check.js +3 -3
  21. package/src/cli.js +122 -167
  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 +274 -697
  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 -392
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +160 -228
  35. package/src/index.js +36 -2
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +97 -20
  39. package/src/owners.js +132 -132
  40. package/src/parser.js +58 -131
  41. package/src/queries-cli.js +866 -0
  42. package/src/queries.js +1356 -2261
  43. package/src/resolve.js +11 -2
  44. package/src/result-formatter.js +21 -0
  45. package/src/sequence.js +364 -0
  46. package/src/structure.js +200 -199
  47. package/src/test-filter.js +7 -0
  48. package/src/triage.js +120 -162
  49. package/src/viewer.js +1 -1
package/src/cli.js CHANGED
@@ -25,12 +25,11 @@ import {
25
25
  exportNeo4jCSV,
26
26
  } from './export.js';
27
27
  import { setVerbose } from './logger.js';
28
- import { printNdjson } from './paginate.js';
28
+ import { EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
29
29
  import {
30
30
  children,
31
31
  context,
32
32
  diffImpact,
33
- EVERY_SYMBOL_KIND,
34
33
  explain,
35
34
  fileDeps,
36
35
  fileExports,
@@ -41,9 +40,8 @@ import {
41
40
  roles,
42
41
  stats,
43
42
  symbolPath,
44
- VALID_ROLES,
45
43
  where,
46
- } from './queries.js';
44
+ } from './queries-cli.js';
47
45
  import {
48
46
  listRepos,
49
47
  pruneRegistry,
@@ -51,6 +49,7 @@ import {
51
49
  registerRepo,
52
50
  unregisterRepo,
53
51
  } from './registry.js';
52
+ import { outputResult } from './result-formatter.js';
54
53
  import { snapshotDelete, snapshotList, snapshotRestore, snapshotSave } from './snapshot.js';
55
54
  import { checkForUpdates, printUpdateNotification } from './update-check.js';
56
55
  import { watchProject } from './watcher.js';
@@ -95,6 +94,17 @@ function resolveNoTests(opts) {
95
94
  return config.query?.excludeTests || false;
96
95
  }
97
96
 
97
+ /** Attach the common query options shared by most analysis commands. */
98
+ const QUERY_OPTS = (cmd) =>
99
+ cmd
100
+ .option('-d, --db <path>', 'Path to graph.db')
101
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
102
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
103
+ .option('-j, --json', 'Output as JSON')
104
+ .option('--limit <number>', 'Max results to return')
105
+ .option('--offset <number>', 'Skip N results (default: 0)')
106
+ .option('--ndjson', 'Newline-delimited JSON output');
107
+
98
108
  function formatSize(bytes) {
99
109
  if (bytes < 1024) return `${bytes} B`;
100
110
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -122,10 +132,11 @@ program
122
132
  });
123
133
  });
124
134
 
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')
135
+ QUERY_OPTS(
136
+ program
137
+ .command('query <name>')
138
+ .description('Function-level dependency chain or shortest path between symbols'),
139
+ )
129
140
  .option('--depth <n>', 'Transitive caller depth', '3')
130
141
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
131
142
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
@@ -134,12 +145,6 @@ program
134
145
  .option('--reverse', 'Path mode: follow edges backward')
135
146
  .option('--from-file <path>', 'Path mode: disambiguate source symbol by file')
136
147
  .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
148
  .action((name, opts) => {
144
149
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
145
150
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -201,25 +206,17 @@ program
201
206
  });
202
207
  });
203
208
 
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
- });
209
+ QUERY_OPTS(
210
+ program.command('impact <file>').description('Show what depends on this file (transitive)'),
211
+ ).action((file, opts) => {
212
+ impactAnalysis(file, opts.db, {
213
+ noTests: resolveNoTests(opts),
214
+ json: opts.json,
215
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
216
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
217
+ ndjson: opts.ndjson,
222
218
  });
219
+ });
223
220
 
224
221
  program
225
222
  .command('map')
@@ -247,59 +244,43 @@ program
247
244
  await stats(opts.db, { noTests: resolveNoTests(opts), json: opts.json });
248
245
  });
249
246
 
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
- });
247
+ QUERY_OPTS(
248
+ program.command('deps <file>').description('Show what this file imports and what imports it'),
249
+ ).action((file, opts) => {
250
+ fileDeps(file, opts.db, {
251
+ noTests: resolveNoTests(opts),
252
+ json: opts.json,
253
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
254
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
255
+ ndjson: opts.ndjson,
268
256
  });
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')
257
+ });
258
+
259
+ QUERY_OPTS(
260
+ program
261
+ .command('exports <file>')
262
+ .description('Show exported symbols with per-symbol consumers (who calls each export)'),
263
+ )
264
+ .option('--unused', 'Show only exports with zero consumers (dead exports)')
280
265
  .action((file, opts) => {
281
266
  fileExports(file, opts.db, {
282
267
  noTests: resolveNoTests(opts),
283
268
  json: opts.json,
269
+ unused: opts.unused || false,
284
270
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
285
271
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
286
272
  ndjson: opts.ndjson,
287
273
  });
288
274
  });
289
275
 
290
- program
291
- .command('fn-impact <name>')
292
- .description('Function-level impact: what functions break if this one changes')
293
- .option('-d, --db <path>', 'Path to graph.db')
276
+ QUERY_OPTS(
277
+ program
278
+ .command('fn-impact <name>')
279
+ .description('Function-level impact: what functions break if this one changes'),
280
+ )
294
281
  .option('--depth <n>', 'Max transitive depth', '5')
295
282
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
296
283
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
297
- .option('-T, --no-tests', 'Exclude test/spec files from results')
298
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
299
- .option('-j, --json', 'Output as JSON')
300
- .option('--limit <number>', 'Max results to return')
301
- .option('--offset <number>', 'Skip N results (default: 0)')
302
- .option('--ndjson', 'Newline-delimited JSON output')
303
284
  .action((name, opts) => {
304
285
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
305
286
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -317,21 +298,16 @@ program
317
298
  });
318
299
  });
319
300
 
320
- program
321
- .command('context <name>')
322
- .description('Full context for a function: source, deps, callers, tests, signature')
323
- .option('-d, --db <path>', 'Path to graph.db')
301
+ QUERY_OPTS(
302
+ program
303
+ .command('context <name>')
304
+ .description('Full context for a function: source, deps, callers, tests, signature'),
305
+ )
324
306
  .option('--depth <n>', 'Include callee source up to N levels deep', '0')
325
307
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
326
308
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
327
309
  .option('--no-source', 'Metadata only (skip source extraction)')
328
310
  .option('--with-test-source', 'Include test source code')
329
- .option('-T, --no-tests', 'Exclude test/spec files from results')
330
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
331
- .option('-j, --json', 'Output as JSON')
332
- .option('--limit <number>', 'Max results to return')
333
- .option('--offset <number>', 'Skip N results (default: 0)')
334
- .option('--ndjson', 'Newline-delimited JSON output')
335
311
  .action((name, opts) => {
336
312
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
337
313
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -415,17 +391,12 @@ program
415
391
  });
416
392
  });
417
393
 
418
- program
419
- .command('where [name]')
420
- .description('Find where a symbol is defined and used (minimal, fast lookup)')
421
- .option('-d, --db <path>', 'Path to graph.db')
394
+ QUERY_OPTS(
395
+ program
396
+ .command('where [name]')
397
+ .description('Find where a symbol is defined and used (minimal, fast lookup)'),
398
+ )
422
399
  .option('-f, --file <path>', 'File overview: list symbols, imports, exports')
423
- .option('-T, --no-tests', 'Exclude test/spec files from results')
424
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
425
- .option('-j, --json', 'Output as JSON')
426
- .option('--limit <number>', 'Max results to return')
427
- .option('--offset <number>', 'Skip N results (default: 0)')
428
- .option('--ndjson', 'Newline-delimited JSON output')
429
400
  .action((name, opts) => {
430
401
  if (!name && !opts.file) {
431
402
  console.error('Provide a symbol name or use --file <path>');
@@ -446,15 +417,14 @@ program
446
417
  .command('diff-impact [ref]')
447
418
  .description('Show impact of git changes (unstaged, staged, or vs a ref)')
448
419
  .option('-d, --db <path>', 'Path to graph.db')
449
- .option('--staged', 'Analyze staged changes instead of unstaged')
450
- .option('--depth <n>', 'Max transitive caller depth', '3')
451
420
  .option('-T, --no-tests', 'Exclude test/spec files from results')
452
421
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
453
- .option('-j, --json', 'Output as JSON')
454
- .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
455
422
  .option('--limit <number>', 'Max results to return')
456
423
  .option('--offset <number>', 'Skip N results (default: 0)')
457
424
  .option('--ndjson', 'Newline-delimited JSON output')
425
+ .option('--staged', 'Analyze staged changes instead of unstaged')
426
+ .option('--depth <n>', 'Max transitive caller depth', '3')
427
+ .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
458
428
  .action((ref, opts) => {
459
429
  diffImpact(opts.db, {
460
430
  ref,
@@ -992,11 +962,7 @@ program
992
962
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
993
963
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
994
964
  });
995
- if (opts.ndjson) {
996
- printNdjson(data, 'directories');
997
- } else if (opts.json) {
998
- console.log(JSON.stringify(data, null, 2));
999
- } else {
965
+ if (!outputResult(data, 'directories', opts)) {
1000
966
  console.log(formatStructure(data));
1001
967
  }
1002
968
  });
@@ -1079,41 +1045,28 @@ program
1079
1045
 
1080
1046
  if (file) {
1081
1047
  const data = coChangeData(file, opts.db, queryOpts);
1082
- if (opts.ndjson) {
1083
- printNdjson(data, 'partners');
1084
- } else if (opts.json) {
1085
- console.log(JSON.stringify(data, null, 2));
1086
- } else {
1048
+ if (!outputResult(data, 'partners', opts)) {
1087
1049
  console.log(formatCoChange(data));
1088
1050
  }
1089
1051
  } else {
1090
1052
  const data = coChangeTopData(opts.db, queryOpts);
1091
- if (opts.ndjson) {
1092
- printNdjson(data, 'pairs');
1093
- } else if (opts.json) {
1094
- console.log(JSON.stringify(data, null, 2));
1095
- } else {
1053
+ if (!outputResult(data, 'pairs', opts)) {
1096
1054
  console.log(formatCoChangeTop(data));
1097
1055
  }
1098
1056
  }
1099
1057
  });
1100
1058
 
1101
- program
1102
- .command('flow [name]')
1103
- .description(
1104
- 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
1105
- )
1059
+ QUERY_OPTS(
1060
+ program
1061
+ .command('flow [name]')
1062
+ .description(
1063
+ 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
1064
+ ),
1065
+ )
1106
1066
  .option('--list', 'List all entry points grouped by type')
1107
1067
  .option('--depth <n>', 'Max forward traversal depth', '10')
1108
- .option('-d, --db <path>', 'Path to graph.db')
1109
1068
  .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1110
1069
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1111
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1112
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1113
- .option('-j, --json', 'Output as JSON')
1114
- .option('--limit <number>', 'Max results to return')
1115
- .option('--offset <number>', 'Skip N results (default: 0)')
1116
- .option('--ndjson', 'Newline-delimited JSON output')
1117
1070
  .action(async (name, opts) => {
1118
1071
  if (!name && !opts.list) {
1119
1072
  console.error('Provide a function/entry point name or use --list to see all entry points.');
@@ -1137,18 +1090,43 @@ program
1137
1090
  });
1138
1091
  });
1139
1092
 
1140
- program
1141
- .command('dataflow <name>')
1142
- .description('Show data flow for a function: parameters, return consumers, mutations')
1143
- .option('-d, --db <path>', 'Path to graph.db')
1093
+ QUERY_OPTS(
1094
+ program
1095
+ .command('sequence <name>')
1096
+ .description(
1097
+ 'Generate a Mermaid sequence diagram from call graph edges (participants = files)',
1098
+ ),
1099
+ )
1100
+ .option('--depth <n>', 'Max forward traversal depth', '10')
1101
+ .option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
1102
+ .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1103
+ .option('-k, --kind <kind>', 'Filter by symbol kind')
1104
+ .action(async (name, opts) => {
1105
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1106
+ console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1107
+ process.exit(1);
1108
+ }
1109
+ const { sequence } = await import('./sequence.js');
1110
+ sequence(name, opts.db, {
1111
+ depth: parseInt(opts.depth, 10),
1112
+ file: opts.file,
1113
+ kind: opts.kind,
1114
+ noTests: resolveNoTests(opts),
1115
+ json: opts.json,
1116
+ dataflow: opts.dataflow,
1117
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1118
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1119
+ ndjson: opts.ndjson,
1120
+ });
1121
+ });
1122
+
1123
+ QUERY_OPTS(
1124
+ program
1125
+ .command('dataflow <name>')
1126
+ .description('Show data flow for a function: parameters, return consumers, mutations'),
1127
+ )
1144
1128
  .option('-f, --file <path>', 'Scope to file (partial match)')
1145
1129
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1146
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1147
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1148
- .option('-j, --json', 'Output as JSON')
1149
- .option('--ndjson', 'Newline-delimited JSON output')
1150
- .option('--limit <number>', 'Max results to return')
1151
- .option('--offset <number>', 'Skip N results (default: 0)')
1152
1130
  .option('--impact', 'Show data-dependent blast radius')
1153
1131
  .option('--depth <n>', 'Max traversal depth', '5')
1154
1132
  .action(async (name, opts) => {
@@ -1170,19 +1148,10 @@ program
1170
1148
  });
1171
1149
  });
1172
1150
 
1173
- program
1174
- .command('cfg <name>')
1175
- .description('Show control flow graph for a function')
1176
- .option('-d, --db <path>', 'Path to graph.db')
1151
+ QUERY_OPTS(program.command('cfg <name>').description('Show control flow graph for a function'))
1177
1152
  .option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
1178
1153
  .option('-f, --file <path>', 'Scope to file (partial match)')
1179
1154
  .option('-k, --kind <kind>', 'Filter by symbol kind')
1180
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1181
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1182
- .option('-j, --json', 'Output as JSON')
1183
- .option('--ndjson', 'Newline-delimited JSON output')
1184
- .option('--limit <number>', 'Max results to return')
1185
- .option('--offset <number>', 'Skip N results (default: 0)')
1186
1155
  .action(async (name, opts) => {
1187
1156
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1188
1157
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -1241,18 +1210,13 @@ program
1241
1210
  });
1242
1211
  });
1243
1212
 
1244
- program
1245
- .command('ast [pattern]')
1246
- .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern')
1247
- .option('-d, --db <path>', 'Path to graph.db')
1213
+ QUERY_OPTS(
1214
+ program
1215
+ .command('ast [pattern]')
1216
+ .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern'),
1217
+ )
1248
1218
  .option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
1249
1219
  .option('-f, --file <path>', 'Scope to file (partial match)')
1250
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1251
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1252
- .option('-j, --json', 'Output as JSON')
1253
- .option('--ndjson', 'Newline-delimited JSON output')
1254
- .option('--limit <number>', 'Max results to return')
1255
- .option('--offset <number>', 'Skip N results (default: 0)')
1256
1220
  .action(async (pattern, opts) => {
1257
1221
  const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
1258
1222
  if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
@@ -1270,19 +1234,14 @@ program
1270
1234
  });
1271
1235
  });
1272
1236
 
1273
- program
1274
- .command('communities')
1275
- .description('Detect natural module boundaries using Louvain community detection')
1237
+ QUERY_OPTS(
1238
+ program
1239
+ .command('communities')
1240
+ .description('Detect natural module boundaries using Louvain community detection'),
1241
+ )
1276
1242
  .option('--functions', 'Function-level instead of file-level')
1277
1243
  .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
1278
1244
  .option('--drift', 'Show only drift analysis')
1279
- .option('-d, --db <path>', 'Path to graph.db')
1280
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1281
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1282
- .option('-j, --json', 'Output as JSON')
1283
- .option('--limit <number>', 'Max results to return')
1284
- .option('--offset <number>', 'Skip N results (default: 0)')
1285
- .option('--ndjson', 'Newline-delimited JSON output')
1286
1245
  .action(async (opts) => {
1287
1246
  const { communities } = await import('./communities.js');
1288
1247
  communities(opts.db, {
@@ -1336,11 +1295,7 @@ program
1336
1295
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1337
1296
  noTests: resolveNoTests(opts),
1338
1297
  });
1339
- if (opts.ndjson) {
1340
- printNdjson(data, 'hotspots');
1341
- } else if (opts.json) {
1342
- console.log(JSON.stringify(data, null, 2));
1343
- } else {
1298
+ if (!outputResult(data, 'hotspots', opts)) {
1344
1299
  console.log(formatHotspots(data));
1345
1300
  }
1346
1301
  return;
package/src/cochange.js CHANGED
@@ -12,7 +12,7 @@ import { normalizePath } from './constants.js';
12
12
  import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
13
  import { warn } from './logger.js';
14
14
  import { paginateResult } from './paginate.js';
15
- import { isTestFile } from './queries.js';
15
+ import { isTestFile } from './test-filter.js';
16
16
 
17
17
  /**
18
18
  * Scan git history and return parsed commit data.
@@ -2,8 +2,9 @@ import path from 'node:path';
2
2
  import Graph from 'graphology';
3
3
  import louvain from 'graphology-communities-louvain';
4
4
  import { openReadonlyOrFail } from './db.js';
5
- import { paginateResult, printNdjson } from './paginate.js';
6
- import { isTestFile } from './queries.js';
5
+ import { paginateResult } from './paginate.js';
6
+ import { outputResult } from './result-formatter.js';
7
+ import { isTestFile } from './test-filter.js';
7
8
 
8
9
  // ─── Graph Construction ───────────────────────────────────────────────
9
10
 
@@ -96,12 +97,15 @@ function getDirectory(filePath) {
96
97
  export function communitiesData(customDbPath, opts = {}) {
97
98
  const db = openReadonlyOrFail(customDbPath);
98
99
  const resolution = opts.resolution ?? 1.0;
99
-
100
- const graph = buildGraphologyGraph(db, {
101
- functions: opts.functions,
102
- noTests: opts.noTests,
103
- });
104
- db.close();
100
+ let graph;
101
+ try {
102
+ graph = buildGraphologyGraph(db, {
103
+ functions: opts.functions,
104
+ noTests: opts.noTests,
105
+ });
106
+ } finally {
107
+ db.close();
108
+ }
105
109
 
106
110
  // Handle empty or trivial graphs
107
111
  if (graph.order === 0 || graph.size === 0) {
@@ -240,14 +244,7 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
240
244
  export function communities(customDbPath, opts = {}) {
241
245
  const data = communitiesData(customDbPath, opts);
242
246
 
243
- if (opts.ndjson) {
244
- printNdjson(data, 'communities');
245
- return;
246
- }
247
- if (opts.json) {
248
- console.log(JSON.stringify(data, null, 2));
249
- return;
250
- }
247
+ if (outputResult(data, 'communities', opts)) return;
251
248
 
252
249
  if (data.summary.communityCount === 0) {
253
250
  console.log(