@optave/codegraph 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  queryName,
30
30
  roles,
31
31
  stats,
32
+ symbolPath,
32
33
  VALID_ROLES,
33
34
  where,
34
35
  } from './queries.js';
@@ -99,8 +100,17 @@ program
99
100
  .option('-T, --no-tests', 'Exclude test/spec files from results')
100
101
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
101
102
  .option('-j, --json', 'Output as JSON')
103
+ .option('--limit <number>', 'Max results to return')
104
+ .option('--offset <number>', 'Skip N results (default: 0)')
105
+ .option('--ndjson', 'Newline-delimited JSON output')
102
106
  .action((name, opts) => {
103
- queryName(name, opts.db, { noTests: resolveNoTests(opts), json: opts.json });
107
+ queryName(name, opts.db, {
108
+ noTests: resolveNoTests(opts),
109
+ json: opts.json,
110
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
111
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
112
+ ndjson: opts.ndjson,
113
+ });
104
114
  });
105
115
 
106
116
  program
@@ -136,8 +146,8 @@ program
136
146
  .option('-T, --no-tests', 'Exclude test/spec files from results')
137
147
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
138
148
  .option('-j, --json', 'Output as JSON')
139
- .action((opts) => {
140
- stats(opts.db, { noTests: resolveNoTests(opts), json: opts.json });
149
+ .action(async (opts) => {
150
+ await stats(opts.db, { noTests: resolveNoTests(opts), json: opts.json });
141
151
  });
142
152
 
143
153
  program
@@ -199,6 +209,36 @@ program
199
209
  });
200
210
  });
201
211
 
212
+ program
213
+ .command('path <from> <to>')
214
+ .description('Find shortest path between two symbols (A calls...calls B)')
215
+ .option('-d, --db <path>', 'Path to graph.db')
216
+ .option('--max-depth <n>', 'Maximum BFS depth', '10')
217
+ .option('--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)')
218
+ .option('--reverse', 'Follow edges backward (B is called by...called by A)')
219
+ .option('--from-file <path>', 'Disambiguate source symbol by file (partial match)')
220
+ .option('--to-file <path>', 'Disambiguate target symbol by file (partial match)')
221
+ .option('-k, --kind <kind>', 'Filter both symbols by kind')
222
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
223
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
224
+ .option('-j, --json', 'Output as JSON')
225
+ .action((from, to, opts) => {
226
+ if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
227
+ console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
228
+ process.exit(1);
229
+ }
230
+ symbolPath(from, to, opts.db, {
231
+ maxDepth: parseInt(opts.maxDepth, 10),
232
+ edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
233
+ reverse: opts.reverse,
234
+ fromFile: opts.fromFile,
235
+ toFile: opts.toFile,
236
+ kind: opts.kind,
237
+ noTests: resolveNoTests(opts),
238
+ json: opts.json,
239
+ });
240
+ });
241
+
202
242
  program
203
243
  .command('context <name>')
204
244
  .description('Full context for a function: source, deps, callers, tests, signature')
@@ -251,13 +291,23 @@ program
251
291
  .option('-T, --no-tests', 'Exclude test/spec files from results')
252
292
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
253
293
  .option('-j, --json', 'Output as JSON')
294
+ .option('--limit <number>', 'Max results to return')
295
+ .option('--offset <number>', 'Skip N results (default: 0)')
296
+ .option('--ndjson', 'Newline-delimited JSON output')
254
297
  .action((name, opts) => {
255
298
  if (!name && !opts.file) {
256
299
  console.error('Provide a symbol name or use --file <path>');
257
300
  process.exit(1);
258
301
  }
259
302
  const target = opts.file || name;
260
- where(target, opts.db, { file: !!opts.file, noTests: resolveNoTests(opts), json: opts.json });
303
+ where(target, opts.db, {
304
+ file: !!opts.file,
305
+ noTests: resolveNoTests(opts),
306
+ json: opts.json,
307
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
308
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
309
+ ndjson: opts.ndjson,
310
+ });
261
311
  });
262
312
 
263
313
  program
@@ -470,6 +520,7 @@ program
470
520
  `Embedding strategy: ${EMBEDDING_STRATEGIES.join(', ')}. "structured" uses graph context (callers/callees), "source" embeds raw code`,
471
521
  'structured',
472
522
  )
523
+ .option('-d, --db <path>', 'Path to graph.db')
473
524
  .action(async (dir, opts) => {
474
525
  if (!EMBEDDING_STRATEGIES.includes(opts.strategy)) {
475
526
  console.error(
@@ -479,7 +530,7 @@ program
479
530
  }
480
531
  const root = path.resolve(dir || '.');
481
532
  const model = opts.model || config.embeddings?.model || DEFAULT_MODEL;
482
- await buildEmbeddings(root, model, undefined, { strategy: opts.strategy });
533
+ await buildEmbeddings(root, model, opts.db, { strategy: opts.strategy });
483
534
  });
484
535
 
485
536
  program
@@ -516,6 +567,7 @@ program
516
567
  .option('-d, --db <path>', 'Path to graph.db')
517
568
  .option('--depth <n>', 'Max directory depth')
518
569
  .option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
570
+ .option('--full', 'Show all files without limit')
519
571
  .option('-T, --no-tests', 'Exclude test/spec files')
520
572
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
521
573
  .option('-j, --json', 'Output as JSON')
@@ -525,6 +577,7 @@ program
525
577
  directory: dir,
526
578
  depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
527
579
  sort: opts.sort,
580
+ full: opts.full,
528
581
  noTests: resolveNoTests(opts),
529
582
  });
530
583
  if (opts.json) {
@@ -570,6 +623,9 @@ program
570
623
  .option('-T, --no-tests', 'Exclude test/spec files')
571
624
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
572
625
  .option('-j, --json', 'Output as JSON')
626
+ .option('--limit <number>', 'Max results to return')
627
+ .option('--offset <number>', 'Skip N results (default: 0)')
628
+ .option('--ndjson', 'Newline-delimited JSON output')
573
629
  .action((opts) => {
574
630
  if (opts.role && !VALID_ROLES.includes(opts.role)) {
575
631
  console.error(`Invalid role "${opts.role}". Valid roles: ${VALID_ROLES.join(', ')}`);
@@ -580,6 +636,9 @@ program
580
636
  file: opts.file,
581
637
  noTests: resolveNoTests(opts),
582
638
  json: opts.json,
639
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
640
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
641
+ ndjson: opts.ndjson,
583
642
  });
584
643
  });
585
644
 
@@ -645,6 +704,144 @@ program
645
704
  }
646
705
  });
647
706
 
707
+ program
708
+ .command('flow [name]')
709
+ .description(
710
+ 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
711
+ )
712
+ .option('--list', 'List all entry points grouped by type')
713
+ .option('--depth <n>', 'Max forward traversal depth', '10')
714
+ .option('-d, --db <path>', 'Path to graph.db')
715
+ .option('-f, --file <path>', 'Scope to a specific file (partial match)')
716
+ .option('-k, --kind <kind>', 'Filter by symbol kind')
717
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
718
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
719
+ .option('-j, --json', 'Output as JSON')
720
+ .option('--limit <number>', 'Max results to return')
721
+ .option('--offset <number>', 'Skip N results (default: 0)')
722
+ .option('--ndjson', 'Newline-delimited JSON output')
723
+ .action(async (name, opts) => {
724
+ if (!name && !opts.list) {
725
+ console.error('Provide a function/entry point name or use --list to see all entry points.');
726
+ process.exit(1);
727
+ }
728
+ if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
729
+ console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
730
+ process.exit(1);
731
+ }
732
+ const { flow } = await import('./flow.js');
733
+ flow(name, opts.db, {
734
+ list: opts.list,
735
+ depth: parseInt(opts.depth, 10),
736
+ file: opts.file,
737
+ kind: opts.kind,
738
+ noTests: resolveNoTests(opts),
739
+ json: opts.json,
740
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
741
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
742
+ ndjson: opts.ndjson,
743
+ });
744
+ });
745
+
746
+ program
747
+ .command('complexity [target]')
748
+ .description('Show per-function complexity metrics (cognitive, cyclomatic, nesting depth, MI)')
749
+ .option('-d, --db <path>', 'Path to graph.db')
750
+ .option('-n, --limit <number>', 'Max results', '20')
751
+ .option(
752
+ '--sort <metric>',
753
+ 'Sort by: cognitive | cyclomatic | nesting | mi | volume | effort | bugs | loc',
754
+ 'cognitive',
755
+ )
756
+ .option('--above-threshold', 'Only functions exceeding warn thresholds')
757
+ .option('--health', 'Show health metrics (Halstead, MI) columns')
758
+ .option('-f, --file <path>', 'Scope to file (partial match)')
759
+ .option('-k, --kind <kind>', 'Filter by symbol kind')
760
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
761
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
762
+ .option('-j, --json', 'Output as JSON')
763
+ .action(async (target, opts) => {
764
+ if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
765
+ console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
766
+ process.exit(1);
767
+ }
768
+ const { complexity } = await import('./complexity.js');
769
+ complexity(opts.db, {
770
+ target,
771
+ limit: parseInt(opts.limit, 10),
772
+ sort: opts.sort,
773
+ aboveThreshold: opts.aboveThreshold,
774
+ health: opts.health,
775
+ file: opts.file,
776
+ kind: opts.kind,
777
+ noTests: resolveNoTests(opts),
778
+ json: opts.json,
779
+ });
780
+ });
781
+
782
+ program
783
+ .command('manifesto')
784
+ .description('Evaluate manifesto rules (pass/fail verdicts for code health)')
785
+ .option('-d, --db <path>', 'Path to graph.db')
786
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
787
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
788
+ .option('-f, --file <path>', 'Scope to file (partial match)')
789
+ .option('-k, --kind <kind>', 'Filter by symbol kind')
790
+ .option('-j, --json', 'Output as JSON')
791
+ .action(async (opts) => {
792
+ if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
793
+ console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
794
+ process.exit(1);
795
+ }
796
+ const { manifesto } = await import('./manifesto.js');
797
+ manifesto(opts.db, {
798
+ file: opts.file,
799
+ kind: opts.kind,
800
+ noTests: resolveNoTests(opts),
801
+ json: opts.json,
802
+ });
803
+ });
804
+
805
+ program
806
+ .command('communities')
807
+ .description('Detect natural module boundaries using Louvain community detection')
808
+ .option('--functions', 'Function-level instead of file-level')
809
+ .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
810
+ .option('--drift', 'Show only drift analysis')
811
+ .option('-d, --db <path>', 'Path to graph.db')
812
+ .option('-T, --no-tests', 'Exclude test/spec files from results')
813
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
814
+ .option('-j, --json', 'Output as JSON')
815
+ .action(async (opts) => {
816
+ const { communities } = await import('./communities.js');
817
+ communities(opts.db, {
818
+ functions: opts.functions,
819
+ resolution: parseFloat(opts.resolution),
820
+ drift: opts.drift,
821
+ noTests: resolveNoTests(opts),
822
+ json: opts.json,
823
+ });
824
+ });
825
+
826
+ program
827
+ .command('branch-compare <base> <target>')
828
+ .description('Compare code structure between two branches/refs')
829
+ .option('--depth <n>', 'Max transitive caller depth', '3')
830
+ .option('-T, --no-tests', 'Exclude test/spec files')
831
+ .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
832
+ .option('-j, --json', 'Output as JSON')
833
+ .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
834
+ .action(async (base, target, opts) => {
835
+ const { branchCompare } = await import('./branch-compare.js');
836
+ await branchCompare(base, target, {
837
+ engine: program.opts().engine,
838
+ depth: parseInt(opts.depth, 10),
839
+ noTests: resolveNoTests(opts),
840
+ json: opts.json,
841
+ format: opts.format,
842
+ });
843
+ });
844
+
648
845
  program
649
846
  .command('watch [dir]')
650
847
  .description('Watch project for file changes and incrementally update the graph')
@@ -680,6 +877,43 @@ program
680
877
  console.log(` Engine flag : --engine ${engine}`);
681
878
  console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
682
879
  console.log();
880
+
881
+ // Build metadata from DB
882
+ try {
883
+ const { findDbPath, getBuildMeta } = await import('./db.js');
884
+ const Database = (await import('better-sqlite3')).default;
885
+ const dbPath = findDbPath();
886
+ const fs = await import('node:fs');
887
+ if (fs.existsSync(dbPath)) {
888
+ const db = new Database(dbPath, { readonly: true });
889
+ const buildEngine = getBuildMeta(db, 'engine');
890
+ const buildVersion = getBuildMeta(db, 'codegraph_version');
891
+ const builtAt = getBuildMeta(db, 'built_at');
892
+ db.close();
893
+
894
+ if (buildEngine || buildVersion || builtAt) {
895
+ console.log('Build metadata');
896
+ console.log('──────────────');
897
+ if (buildEngine) console.log(` Engine : ${buildEngine}`);
898
+ if (buildVersion) console.log(` Version : ${buildVersion}`);
899
+ if (builtAt) console.log(` Built at : ${builtAt}`);
900
+
901
+ if (buildVersion && buildVersion !== program.version()) {
902
+ console.log(
903
+ ` ⚠ DB was built with v${buildVersion}, current is v${program.version()}. Consider: codegraph build --no-incremental`,
904
+ );
905
+ }
906
+ if (buildEngine && buildEngine !== activeName) {
907
+ console.log(
908
+ ` ⚠ DB was built with ${buildEngine} engine, active is ${activeName}. Consider: codegraph build --no-incremental`,
909
+ );
910
+ }
911
+ console.log();
912
+ }
913
+ }
914
+ } catch {
915
+ /* diagnostics must never crash */
916
+ }
683
917
  });
684
918
 
685
919
  program.parse();
package/src/cochange.js CHANGED
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process';
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { normalizePath } from './constants.js';
12
- import { findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
12
+ import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
13
  import { warn } from './logger.js';
14
14
  import { isTestFile } from './queries.js';
15
15
 
@@ -145,7 +145,7 @@ export function analyzeCoChanges(customDbPath, opts = {}) {
145
145
  const repoRoot = path.resolve(path.dirname(dbPath), '..');
146
146
 
147
147
  if (!fs.existsSync(path.join(repoRoot, '.git'))) {
148
- db.close();
148
+ closeDb(db);
149
149
  return { error: `Not a git repository: ${repoRoot}` };
150
150
  }
151
151
 
@@ -245,7 +245,7 @@ export function analyzeCoChanges(customDbPath, opts = {}) {
245
245
 
246
246
  const totalPairs = db.prepare('SELECT COUNT(*) as cnt FROM co_changes').get().cnt;
247
247
 
248
- db.close();
248
+ closeDb(db);
249
249
 
250
250
  return {
251
251
  pairsFound: totalPairs,
@@ -275,14 +275,14 @@ export function coChangeData(file, customDbPath, opts = {}) {
275
275
  try {
276
276
  db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
277
277
  } catch {
278
- db.close();
278
+ closeDb(db);
279
279
  return { error: 'No co-change data found. Run `codegraph co-change --analyze` first.' };
280
280
  }
281
281
 
282
282
  // Resolve file via partial match
283
283
  const resolvedFile = resolveCoChangeFile(db, file);
284
284
  if (!resolvedFile) {
285
- db.close();
285
+ closeDb(db);
286
286
  return { error: `No co-change data found for file matching "${file}"` };
287
287
  }
288
288
 
@@ -311,7 +311,7 @@ export function coChangeData(file, customDbPath, opts = {}) {
311
311
  }
312
312
 
313
313
  const meta = getCoChangeMeta(db);
314
- db.close();
314
+ closeDb(db);
315
315
 
316
316
  return { file: resolvedFile, partners, meta };
317
317
  }
@@ -334,7 +334,7 @@ export function coChangeTopData(customDbPath, opts = {}) {
334
334
  try {
335
335
  db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
336
336
  } catch {
337
- db.close();
337
+ closeDb(db);
338
338
  return { error: 'No co-change data found. Run `codegraph co-change --analyze` first.' };
339
339
  }
340
340
 
@@ -363,7 +363,7 @@ export function coChangeTopData(customDbPath, opts = {}) {
363
363
  }
364
364
 
365
365
  const meta = getCoChangeMeta(db);
366
- db.close();
366
+ closeDb(db);
367
367
 
368
368
  return { pairs, meta };
369
369
  }