@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/builder.js CHANGED
@@ -4,7 +4,17 @@ import path from 'node:path';
4
4
  import { performance } from 'node:perf_hooks';
5
5
  import { loadConfig } from './config.js';
6
6
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
7
- import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb, setBuildMeta } from './db.js';
7
+ import {
8
+ bulkNodeIdsByFile,
9
+ closeDb,
10
+ getBuildMeta,
11
+ getNodeId,
12
+ initSchema,
13
+ MIGRATIONS,
14
+ openDb,
15
+ purgeFilesData,
16
+ setBuildMeta,
17
+ } from './db.js';
8
18
  import { readJournal, writeJournalHeader } from './journal.js';
9
19
  import { debug, info, warn } from './logger.js';
10
20
  import { loadNative } from './native.js';
@@ -350,88 +360,7 @@ function getChangedFiles(db, allFiles, rootDir) {
350
360
  * @param {boolean} [options.purgeHashes=true] - Also delete file_hashes entries
351
361
  */
352
362
  export function purgeFilesFromGraph(db, files, options = {}) {
353
- const { purgeHashes = true } = options;
354
- if (!files || files.length === 0) return;
355
-
356
- // Check if embeddings table exists
357
- let hasEmbeddings = false;
358
- try {
359
- db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
360
- hasEmbeddings = true;
361
- } catch {
362
- /* table doesn't exist */
363
- }
364
-
365
- const deleteEmbeddingsForFile = hasEmbeddings
366
- ? db.prepare('DELETE FROM embeddings WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)')
367
- : null;
368
- const deleteNodesForFile = db.prepare('DELETE FROM nodes WHERE file = ?');
369
- const deleteEdgesForFile = db.prepare(`
370
- DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f)
371
- OR target_id IN (SELECT id FROM nodes WHERE file = @f)
372
- `);
373
- const deleteMetricsForFile = db.prepare(
374
- 'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
375
- );
376
- let deleteComplexityForFile;
377
- try {
378
- deleteComplexityForFile = db.prepare(
379
- 'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
380
- );
381
- } catch {
382
- deleteComplexityForFile = null;
383
- }
384
- let deleteDataflowForFile;
385
- try {
386
- deleteDataflowForFile = db.prepare(
387
- 'DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?) OR target_id IN (SELECT id FROM nodes WHERE file = ?)',
388
- );
389
- } catch {
390
- deleteDataflowForFile = null;
391
- }
392
- let deleteHashForFile;
393
- if (purgeHashes) {
394
- try {
395
- deleteHashForFile = db.prepare('DELETE FROM file_hashes WHERE file = ?');
396
- } catch {
397
- deleteHashForFile = null;
398
- }
399
- }
400
- let deleteAstNodesForFile;
401
- try {
402
- deleteAstNodesForFile = db.prepare('DELETE FROM ast_nodes WHERE file = ?');
403
- } catch {
404
- deleteAstNodesForFile = null;
405
- }
406
- let deleteCfgForFile;
407
- try {
408
- deleteCfgForFile = db.prepare(
409
- 'DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
410
- );
411
- } catch {
412
- deleteCfgForFile = null;
413
- }
414
- let deleteCfgBlocksForFile;
415
- try {
416
- deleteCfgBlocksForFile = db.prepare(
417
- 'DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
418
- );
419
- } catch {
420
- deleteCfgBlocksForFile = null;
421
- }
422
-
423
- for (const relPath of files) {
424
- deleteEmbeddingsForFile?.run(relPath);
425
- deleteEdgesForFile.run({ f: relPath });
426
- deleteMetricsForFile.run(relPath);
427
- deleteComplexityForFile?.run(relPath);
428
- deleteDataflowForFile?.run(relPath, relPath);
429
- deleteAstNodesForFile?.run(relPath);
430
- deleteCfgForFile?.run(relPath);
431
- deleteCfgBlocksForFile?.run(relPath);
432
- deleteNodesForFile.run(relPath);
433
- if (purgeHashes) deleteHashForFile?.run(relPath);
434
- }
363
+ purgeFilesData(db, files, options);
435
364
  }
436
365
 
437
366
  export async function buildGraph(rootDir, opts = {}) {
@@ -677,9 +606,12 @@ export async function buildGraph(rootDir, opts = {}) {
677
606
  }
678
607
  }
679
608
 
680
- const getNodeId = db.prepare(
681
- 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
682
- );
609
+ const getNodeIdStmt = {
610
+ get: (name, kind, file, line) => {
611
+ const id = getNodeId(db, name, kind, file, line);
612
+ return id != null ? { id } : undefined;
613
+ },
614
+ };
683
615
 
684
616
  // Batch INSERT helpers — multi-value INSERTs reduce SQLite round-trips
685
617
  const BATCH_CHUNK = 200;
@@ -752,7 +684,7 @@ export async function buildGraph(rootDir, opts = {}) {
752
684
  }
753
685
 
754
686
  // Bulk-fetch all node IDs for a file in one query (replaces per-node getNodeId calls)
755
- const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
687
+ const bulkGetNodeIds = { all: (file) => bulkNodeIdsByFile(db, file) };
756
688
 
757
689
  const insertAll = db.transaction(() => {
758
690
  // Phase 1: Batch insert all file nodes + definitions + exports
@@ -1032,16 +964,22 @@ export async function buildGraph(rootDir, opts = {}) {
1032
964
  for (const [relPath, symbols] of fileSymbols) {
1033
965
  // Skip barrel-only files — loaded for resolution, edges already in DB
1034
966
  if (barrelOnlyFiles.has(relPath)) continue;
1035
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
967
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1036
968
  if (!fileNodeRow) continue;
1037
969
  const fileNodeId = fileNodeRow.id;
1038
970
 
1039
971
  // Import edges
1040
972
  for (const imp of symbols.imports) {
1041
973
  const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
1042
- const targetRow = getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
974
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
1043
975
  if (targetRow) {
1044
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
976
+ const edgeKind = imp.reexport
977
+ ? 'reexports'
978
+ : imp.typeOnly
979
+ ? 'imports-type'
980
+ : imp.dynamicImport
981
+ ? 'dynamic-imports'
982
+ : 'imports';
1045
983
  allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
1046
984
 
1047
985
  if (!imp.reexport && isBarrelFile(resolvedPath)) {
@@ -1055,12 +993,16 @@ export async function buildGraph(rootDir, opts = {}) {
1055
993
  !resolvedSources.has(actualSource)
1056
994
  ) {
1057
995
  resolvedSources.add(actualSource);
1058
- const actualRow = getNodeId.get(actualSource, 'file', actualSource, 0);
996
+ const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
1059
997
  if (actualRow) {
1060
998
  allEdgeRows.push([
1061
999
  fileNodeId,
1062
1000
  actualRow.id,
1063
- edgeKind === 'imports-type' ? 'imports-type' : 'imports',
1001
+ edgeKind === 'imports-type'
1002
+ ? 'imports-type'
1003
+ : edgeKind === 'dynamic-imports'
1004
+ ? 'dynamic-imports'
1005
+ : 'imports',
1064
1006
  0.9,
1065
1007
  0,
1066
1008
  ]);
@@ -1078,7 +1020,7 @@ export async function buildGraph(rootDir, opts = {}) {
1078
1020
  const nativeFiles = [];
1079
1021
  for (const [relPath, symbols] of fileSymbols) {
1080
1022
  if (barrelOnlyFiles.has(relPath)) continue;
1081
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
1023
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1082
1024
  if (!fileNodeRow) continue;
1083
1025
 
1084
1026
  // Pre-resolve imported names (including barrel resolution)
@@ -1120,7 +1062,7 @@ export async function buildGraph(rootDir, opts = {}) {
1120
1062
  // JS fallback — call/receiver/extends/implements edges
1121
1063
  for (const [relPath, symbols] of fileSymbols) {
1122
1064
  if (barrelOnlyFiles.has(relPath)) continue;
1123
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
1065
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1124
1066
  if (!fileNodeRow) continue;
1125
1067
 
1126
1068
  // Build import name -> target file mapping
@@ -1145,14 +1087,14 @@ export async function buildGraph(rootDir, opts = {}) {
1145
1087
  if (call.line <= end) {
1146
1088
  const span = end - def.line;
1147
1089
  if (span < callerSpan) {
1148
- const row = getNodeId.get(def.name, def.kind, relPath, def.line);
1090
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
1149
1091
  if (row) {
1150
1092
  caller = row;
1151
1093
  callerSpan = span;
1152
1094
  }
1153
1095
  }
1154
1096
  } else if (!caller) {
1155
- const row = getNodeId.get(def.name, def.kind, relPath, def.line);
1097
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
1156
1098
  if (row) caller = row;
1157
1099
  }
1158
1100
  }
@@ -1393,96 +1335,19 @@ export async function buildGraph(rootDir, opts = {}) {
1393
1335
  }
1394
1336
  }
1395
1337
 
1396
- // AST node extraction (calls, new, string, regex, throw, await)
1397
- _t.ast0 = performance.now();
1398
- if (opts.ast !== false) {
1399
- try {
1400
- const { buildAstNodes } = await import('./ast.js');
1401
- await buildAstNodes(db, astComplexitySymbols, rootDir, engineOpts);
1402
- } catch (err) {
1403
- debug(`AST node extraction failed: ${err.message}`);
1404
- }
1405
- }
1406
- _t.astMs = performance.now() - _t.ast0;
1407
-
1408
- // Compute per-function complexity metrics (cognitive, cyclomatic, nesting)
1409
- _t.complexity0 = performance.now();
1410
- if (opts.complexity !== false) {
1411
- try {
1412
- const { buildComplexityMetrics } = await import('./complexity.js');
1413
- await buildComplexityMetrics(db, astComplexitySymbols, rootDir, engineOpts);
1414
- } catch (err) {
1415
- debug(`Complexity analysis failed: ${err.message}`);
1416
- }
1417
- }
1418
- _t.complexityMs = performance.now() - _t.complexity0;
1419
-
1420
- // Pre-parse files missing WASM trees (native builds) so CFG + dataflow
1421
- // share a single parse pass instead of each creating parsers independently.
1422
- // Skip entirely when native engine already provides CFG + dataflow data.
1423
- if (opts.cfg !== false || opts.dataflow !== false) {
1424
- const needsCfg = opts.cfg !== false;
1425
- const needsDataflow = opts.dataflow !== false;
1426
-
1427
- let needsWasmTrees = false;
1428
- for (const [, symbols] of astComplexitySymbols) {
1429
- if (symbols._tree) continue; // already has a tree
1430
- // CFG: need tree if any function/method def lacks native CFG
1431
- if (needsCfg) {
1432
- const fnDefs = (symbols.definitions || []).filter(
1433
- (d) => (d.kind === 'function' || d.kind === 'method') && d.line,
1434
- );
1435
- if (
1436
- fnDefs.length > 0 &&
1437
- !fnDefs.every((d) => d.cfg === null || Array.isArray(d.cfg?.blocks))
1438
- ) {
1439
- needsWasmTrees = true;
1440
- break;
1441
- }
1442
- }
1443
- // Dataflow: need tree if file lacks native dataflow
1444
- if (needsDataflow && !symbols.dataflow) {
1445
- needsWasmTrees = true;
1446
- break;
1447
- }
1448
- }
1449
-
1450
- if (needsWasmTrees) {
1451
- _t.wasmPre0 = performance.now();
1452
- try {
1453
- const { ensureWasmTrees } = await import('./parser.js');
1454
- await ensureWasmTrees(astComplexitySymbols, rootDir);
1455
- } catch (err) {
1456
- debug(`WASM pre-parse failed: ${err.message}`);
1457
- }
1458
- _t.wasmPreMs = performance.now() - _t.wasmPre0;
1459
- } else {
1460
- _t.wasmPreMs = 0;
1461
- }
1462
- }
1463
-
1464
- // CFG analysis (skip with --no-cfg)
1465
- if (opts.cfg !== false) {
1466
- _t.cfg0 = performance.now();
1467
- try {
1468
- const { buildCFGData } = await import('./cfg.js');
1469
- await buildCFGData(db, astComplexitySymbols, rootDir, engineOpts);
1470
- } catch (err) {
1471
- debug(`CFG analysis failed: ${err.message}`);
1472
- }
1473
- _t.cfgMs = performance.now() - _t.cfg0;
1474
- }
1475
-
1476
- // Dataflow analysis (skip with --no-dataflow)
1477
- if (opts.dataflow !== false) {
1478
- _t.dataflow0 = performance.now();
1338
+ // ── Unified AST analysis engine ──────────────────────────────────────
1339
+ // Replaces 4 sequential buildXxx calls with one coordinated pass.
1340
+ {
1341
+ const { runAnalyses } = await import('./ast-analysis/engine.js');
1479
1342
  try {
1480
- const { buildDataflowEdges } = await import('./dataflow.js');
1481
- await buildDataflowEdges(db, astComplexitySymbols, rootDir, engineOpts);
1343
+ const analysisTiming = await runAnalyses(db, astComplexitySymbols, rootDir, opts, engineOpts);
1344
+ _t.astMs = analysisTiming.astMs;
1345
+ _t.complexityMs = analysisTiming.complexityMs;
1346
+ _t.cfgMs = analysisTiming.cfgMs;
1347
+ _t.dataflowMs = analysisTiming.dataflowMs;
1482
1348
  } catch (err) {
1483
- debug(`Dataflow analysis failed: ${err.message}`);
1349
+ debug(`Unified analysis engine failed: ${err.message}`);
1484
1350
  }
1485
- _t.dataflowMs = performance.now() - _t.dataflow0;
1486
1351
  }
1487
1352
 
1488
1353
  // Release any remaining cached WASM trees for GC
@@ -1601,7 +1466,6 @@ export async function buildGraph(rootDir, opts = {}) {
1601
1466
  rolesMs: +_t.rolesMs.toFixed(1),
1602
1467
  astMs: +_t.astMs.toFixed(1),
1603
1468
  complexityMs: +_t.complexityMs.toFixed(1),
1604
- ...(_t.wasmPreMs != null && { wasmPreMs: +_t.wasmPreMs.toFixed(1) }),
1605
1469
  ...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
1606
1470
  ...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
1607
1471
  },