@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
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,61 +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')
280
- .option('--unused', 'Show only exports with zero consumers')
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)')
281
265
  .action((file, opts) => {
282
266
  fileExports(file, opts.db, {
283
267
  noTests: resolveNoTests(opts),
284
268
  json: opts.json,
269
+ unused: opts.unused || false,
285
270
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
286
271
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
287
272
  ndjson: opts.ndjson,
288
- unused: opts.unused,
289
273
  });
290
274
  });
291
275
 
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')
276
+ QUERY_OPTS(
277
+ program
278
+ .command('fn-impact <name>')
279
+ .description('Function-level impact: what functions break if this one changes'),
280
+ )
296
281
  .option('--depth <n>', 'Max transitive depth', '5')
297
282
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
298
283
  .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
284
  .action((name, opts) => {
306
285
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
307
286
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -319,21 +298,16 @@ program
319
298
  });
320
299
  });
321
300
 
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')
301
+ QUERY_OPTS(
302
+ program
303
+ .command('context <name>')
304
+ .description('Full context for a function: source, deps, callers, tests, signature'),
305
+ )
326
306
  .option('--depth <n>', 'Include callee source up to N levels deep', '0')
327
307
  .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
328
308
  .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
329
309
  .option('--no-source', 'Metadata only (skip source extraction)')
330
310
  .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
311
  .action((name, opts) => {
338
312
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
339
313
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -417,17 +391,12 @@ program
417
391
  });
418
392
  });
419
393
 
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')
394
+ QUERY_OPTS(
395
+ program
396
+ .command('where [name]')
397
+ .description('Find where a symbol is defined and used (minimal, fast lookup)'),
398
+ )
424
399
  .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
400
  .action((name, opts) => {
432
401
  if (!name && !opts.file) {
433
402
  console.error('Provide a symbol name or use --file <path>');
@@ -448,15 +417,14 @@ program
448
417
  .command('diff-impact [ref]')
449
418
  .description('Show impact of git changes (unstaged, staged, or vs a ref)')
450
419
  .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
420
  .option('-T, --no-tests', 'Exclude test/spec files from results')
454
421
  .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
422
  .option('--limit <number>', 'Max results to return')
458
423
  .option('--offset <number>', 'Skip N results (default: 0)')
459
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')
460
428
  .action((ref, opts) => {
461
429
  diffImpact(opts.db, {
462
430
  ref,
@@ -994,11 +962,7 @@ program
994
962
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
995
963
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
996
964
  });
997
- if (opts.ndjson) {
998
- printNdjson(data, 'directories');
999
- } else if (opts.json) {
1000
- console.log(JSON.stringify(data, null, 2));
1001
- } else {
965
+ if (!outputResult(data, 'directories', opts)) {
1002
966
  console.log(formatStructure(data));
1003
967
  }
1004
968
  });
@@ -1081,41 +1045,28 @@ program
1081
1045
 
1082
1046
  if (file) {
1083
1047
  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 {
1048
+ if (!outputResult(data, 'partners', opts)) {
1089
1049
  console.log(formatCoChange(data));
1090
1050
  }
1091
1051
  } else {
1092
1052
  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 {
1053
+ if (!outputResult(data, 'pairs', opts)) {
1098
1054
  console.log(formatCoChangeTop(data));
1099
1055
  }
1100
1056
  }
1101
1057
  });
1102
1058
 
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
- )
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
+ )
1108
1066
  .option('--list', 'List all entry points grouped by type')
1109
1067
  .option('--depth <n>', 'Max forward traversal depth', '10')
1110
- .option('-d, --db <path>', 'Path to graph.db')
1111
1068
  .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1112
1069
  .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
1070
  .action(async (name, opts) => {
1120
1071
  if (!name && !opts.list) {
1121
1072
  console.error('Provide a function/entry point name or use --list to see all entry points.');
@@ -1139,20 +1090,17 @@ program
1139
1090
  });
1140
1091
  });
1141
1092
 
1142
- program
1143
- .command('sequence <name>')
1144
- .description('Generate a Mermaid sequence diagram from call graph edges (participants = files)')
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
+ )
1145
1100
  .option('--depth <n>', 'Max forward traversal depth', '10')
1146
1101
  .option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
1147
- .option('-d, --db <path>', 'Path to graph.db')
1148
1102
  .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1149
1103
  .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
1104
  .action(async (name, opts) => {
1157
1105
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1158
1106
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -1172,18 +1120,13 @@ program
1172
1120
  });
1173
1121
  });
1174
1122
 
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')
1123
+ QUERY_OPTS(
1124
+ program
1125
+ .command('dataflow <name>')
1126
+ .description('Show data flow for a function: parameters, return consumers, mutations'),
1127
+ )
1179
1128
  .option('-f, --file <path>', 'Scope to file (partial match)')
1180
1129
  .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
1130
  .option('--impact', 'Show data-dependent blast radius')
1188
1131
  .option('--depth <n>', 'Max traversal depth', '5')
1189
1132
  .action(async (name, opts) => {
@@ -1205,19 +1148,10 @@ program
1205
1148
  });
1206
1149
  });
1207
1150
 
1208
- program
1209
- .command('cfg <name>')
1210
- .description('Show control flow graph for a function')
1211
- .option('-d, --db <path>', 'Path to graph.db')
1151
+ QUERY_OPTS(program.command('cfg <name>').description('Show control flow graph for a function'))
1212
1152
  .option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
1213
1153
  .option('-f, --file <path>', 'Scope to file (partial match)')
1214
1154
  .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
1155
  .action(async (name, opts) => {
1222
1156
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1223
1157
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
@@ -1276,18 +1210,13 @@ program
1276
1210
  });
1277
1211
  });
1278
1212
 
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')
1213
+ QUERY_OPTS(
1214
+ program
1215
+ .command('ast [pattern]')
1216
+ .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern'),
1217
+ )
1283
1218
  .option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
1284
1219
  .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
1220
  .action(async (pattern, opts) => {
1292
1221
  const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
1293
1222
  if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
@@ -1305,19 +1234,14 @@ program
1305
1234
  });
1306
1235
  });
1307
1236
 
1308
- program
1309
- .command('communities')
1310
- .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
+ )
1311
1242
  .option('--functions', 'Function-level instead of file-level')
1312
1243
  .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
1313
1244
  .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
1245
  .action(async (opts) => {
1322
1246
  const { communities } = await import('./communities.js');
1323
1247
  communities(opts.db, {
@@ -1371,11 +1295,7 @@ program
1371
1295
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1372
1296
  noTests: resolveNoTests(opts),
1373
1297
  });
1374
- if (opts.ndjson) {
1375
- printNdjson(data, 'hotspots');
1376
- } else if (opts.json) {
1377
- console.log(JSON.stringify(data, null, 2));
1378
- } else {
1298
+ if (!outputResult(data, 'hotspots', opts)) {
1379
1299
  console.log(formatHotspots(data));
1380
1300
  }
1381
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(