@optave/codegraph 3.1.1 → 3.1.3

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 (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
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,91 +360,11 @@ 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 = {}) {
367
+ const _t_buildStart = performance.now();
438
368
  rootDir = path.resolve(rootDir);
439
369
  const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
440
370
  const db = openDb(dbPath);
@@ -677,9 +607,12 @@ export async function buildGraph(rootDir, opts = {}) {
677
607
  }
678
608
  }
679
609
 
680
- const getNodeId = db.prepare(
681
- 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
682
- );
610
+ const getNodeIdStmt = {
611
+ get: (name, kind, file, line) => {
612
+ const id = getNodeId(db, name, kind, file, line);
613
+ return id != null ? { id } : undefined;
614
+ },
615
+ };
683
616
 
684
617
  // Batch INSERT helpers — multi-value INSERTs reduce SQLite round-trips
685
618
  const BATCH_CHUNK = 200;
@@ -736,6 +669,7 @@ export async function buildGraph(rootDir, opts = {}) {
736
669
 
737
670
  // ── Phase timing ────────────────────────────────────────────────────
738
671
  const _t = {};
672
+ _t.setupMs = performance.now() - _t_buildStart;
739
673
 
740
674
  // ── Unified parse via parseFilesAuto ───────────────────────────────
741
675
  const filePaths = filesToParse.map((item) => item.file);
@@ -752,7 +686,7 @@ export async function buildGraph(rootDir, opts = {}) {
752
686
  }
753
687
 
754
688
  // 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 = ?');
689
+ const bulkGetNodeIds = { all: (file) => bulkNodeIdsByFile(db, file) };
756
690
 
757
691
  const insertAll = db.transaction(() => {
758
692
  // Phase 1: Batch insert all file nodes + definitions + exports
@@ -1032,16 +966,22 @@ export async function buildGraph(rootDir, opts = {}) {
1032
966
  for (const [relPath, symbols] of fileSymbols) {
1033
967
  // Skip barrel-only files — loaded for resolution, edges already in DB
1034
968
  if (barrelOnlyFiles.has(relPath)) continue;
1035
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
969
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1036
970
  if (!fileNodeRow) continue;
1037
971
  const fileNodeId = fileNodeRow.id;
1038
972
 
1039
973
  // Import edges
1040
974
  for (const imp of symbols.imports) {
1041
975
  const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
1042
- const targetRow = getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
976
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
1043
977
  if (targetRow) {
1044
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
978
+ const edgeKind = imp.reexport
979
+ ? 'reexports'
980
+ : imp.typeOnly
981
+ ? 'imports-type'
982
+ : imp.dynamicImport
983
+ ? 'dynamic-imports'
984
+ : 'imports';
1045
985
  allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
1046
986
 
1047
987
  if (!imp.reexport && isBarrelFile(resolvedPath)) {
@@ -1055,12 +995,16 @@ export async function buildGraph(rootDir, opts = {}) {
1055
995
  !resolvedSources.has(actualSource)
1056
996
  ) {
1057
997
  resolvedSources.add(actualSource);
1058
- const actualRow = getNodeId.get(actualSource, 'file', actualSource, 0);
998
+ const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
1059
999
  if (actualRow) {
1060
1000
  allEdgeRows.push([
1061
1001
  fileNodeId,
1062
1002
  actualRow.id,
1063
- edgeKind === 'imports-type' ? 'imports-type' : 'imports',
1003
+ edgeKind === 'imports-type'
1004
+ ? 'imports-type'
1005
+ : edgeKind === 'dynamic-imports'
1006
+ ? 'dynamic-imports'
1007
+ : 'imports',
1064
1008
  0.9,
1065
1009
  0,
1066
1010
  ]);
@@ -1078,7 +1022,7 @@ export async function buildGraph(rootDir, opts = {}) {
1078
1022
  const nativeFiles = [];
1079
1023
  for (const [relPath, symbols] of fileSymbols) {
1080
1024
  if (barrelOnlyFiles.has(relPath)) continue;
1081
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
1025
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1082
1026
  if (!fileNodeRow) continue;
1083
1027
 
1084
1028
  // Pre-resolve imported names (including barrel resolution)
@@ -1120,7 +1064,7 @@ export async function buildGraph(rootDir, opts = {}) {
1120
1064
  // JS fallback — call/receiver/extends/implements edges
1121
1065
  for (const [relPath, symbols] of fileSymbols) {
1122
1066
  if (barrelOnlyFiles.has(relPath)) continue;
1123
- const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
1067
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
1124
1068
  if (!fileNodeRow) continue;
1125
1069
 
1126
1070
  // Build import name -> target file mapping
@@ -1145,14 +1089,14 @@ export async function buildGraph(rootDir, opts = {}) {
1145
1089
  if (call.line <= end) {
1146
1090
  const span = end - def.line;
1147
1091
  if (span < callerSpan) {
1148
- const row = getNodeId.get(def.name, def.kind, relPath, def.line);
1092
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
1149
1093
  if (row) {
1150
1094
  caller = row;
1151
1095
  callerSpan = span;
1152
1096
  }
1153
1097
  }
1154
1098
  } else if (!caller) {
1155
- const row = getNodeId.get(def.name, def.kind, relPath, def.line);
1099
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
1156
1100
  if (row) caller = row;
1157
1101
  }
1158
1102
  }
@@ -1393,96 +1337,30 @@ export async function buildGraph(rootDir, opts = {}) {
1393
1337
  }
1394
1338
  }
1395
1339
 
1396
- // AST node extraction (calls, new, string, regex, throw, await)
1397
- _t.ast0 = performance.now();
1398
- if (opts.ast !== false) {
1340
+ // ── Unified AST analysis engine ──────────────────────────────────────
1341
+ // Replaces 4 sequential buildXxx calls with one coordinated pass.
1342
+ {
1343
+ const { runAnalyses } = await import('./ast-analysis/engine.js');
1399
1344
  try {
1400
- const { buildAstNodes } = await import('./ast.js');
1401
- await buildAstNodes(db, astComplexitySymbols, rootDir, engineOpts);
1345
+ const analysisTiming = await runAnalyses(db, astComplexitySymbols, rootDir, opts, engineOpts);
1346
+ _t.astMs = analysisTiming.astMs;
1347
+ _t.complexityMs = analysisTiming.complexityMs;
1348
+ _t.cfgMs = analysisTiming.cfgMs;
1349
+ _t.dataflowMs = analysisTiming.dataflowMs;
1402
1350
  } catch (err) {
1403
- debug(`AST node extraction failed: ${err.message}`);
1351
+ debug(`Unified analysis engine failed: ${err.message}`);
1404
1352
  }
1405
1353
  }
1406
- _t.astMs = performance.now() - _t.ast0;
1407
1354
 
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
- }
1355
+ _t.finalize0 = performance.now();
1449
1356
 
1450
- if (needsWasmTrees) {
1357
+ // Release any remaining cached WASM trees — call .delete() to free WASM memory
1358
+ for (const [, symbols] of allSymbols) {
1359
+ if (symbols._tree && typeof symbols._tree.delete === 'function') {
1451
1360
  try {
1452
- const { ensureWasmTrees } = await import('./parser.js');
1453
- await ensureWasmTrees(astComplexitySymbols, rootDir);
1454
- } catch (err) {
1455
- debug(`WASM pre-parse failed: ${err.message}`);
1456
- }
1457
- }
1458
- }
1459
-
1460
- // CFG analysis (skip with --no-cfg)
1461
- if (opts.cfg !== false) {
1462
- _t.cfg0 = performance.now();
1463
- try {
1464
- const { buildCFGData } = await import('./cfg.js');
1465
- await buildCFGData(db, astComplexitySymbols, rootDir, engineOpts);
1466
- } catch (err) {
1467
- debug(`CFG analysis failed: ${err.message}`);
1468
- }
1469
- _t.cfgMs = performance.now() - _t.cfg0;
1470
- }
1471
-
1472
- // Dataflow analysis (skip with --no-dataflow)
1473
- if (opts.dataflow !== false) {
1474
- _t.dataflow0 = performance.now();
1475
- try {
1476
- const { buildDataflowEdges } = await import('./dataflow.js');
1477
- await buildDataflowEdges(db, astComplexitySymbols, rootDir, engineOpts);
1478
- } catch (err) {
1479
- debug(`Dataflow analysis failed: ${err.message}`);
1361
+ symbols._tree.delete();
1362
+ } catch {}
1480
1363
  }
1481
- _t.dataflowMs = performance.now() - _t.dataflow0;
1482
- }
1483
-
1484
- // Release any remaining cached WASM trees for GC
1485
- for (const [, symbols] of allSymbols) {
1486
1364
  symbols._tree = null;
1487
1365
  symbols._langId = null;
1488
1366
  }
@@ -1587,8 +1465,11 @@ export async function buildGraph(rootDir, opts = {}) {
1587
1465
  }
1588
1466
  }
1589
1467
 
1468
+ _t.finalizeMs = performance.now() - _t.finalize0;
1469
+
1590
1470
  return {
1591
1471
  phases: {
1472
+ setupMs: +_t.setupMs.toFixed(1),
1592
1473
  parseMs: +_t.parseMs.toFixed(1),
1593
1474
  insertMs: +_t.insertMs.toFixed(1),
1594
1475
  resolveMs: +_t.resolveMs.toFixed(1),
@@ -1599,6 +1480,7 @@ export async function buildGraph(rootDir, opts = {}) {
1599
1480
  complexityMs: +_t.complexityMs.toFixed(1),
1600
1481
  ...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
1601
1482
  ...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
1483
+ finalizeMs: +_t.finalizeMs.toFixed(1),
1602
1484
  },
1603
1485
  };
1604
1486
  }