@optave/codegraph 3.1.5 → 3.2.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.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -13,8 +13,9 @@ import {
13
13
  import { walkWithVisitors } from '../ast-analysis/visitor.js';
14
14
  import { createComplexityVisitor } from '../ast-analysis/visitors/complexity-visitor.js';
15
15
  import { getFunctionNodeId, openReadonlyOrFail } from '../db/index.js';
16
+ import { buildFileConditionSQL } from '../db/query-builder.js';
16
17
  import { loadConfig } from '../infrastructure/config.js';
17
- import { info } from '../infrastructure/logger.js';
18
+ import { debug, info } from '../infrastructure/logger.js';
18
19
  import { isTestFile } from '../infrastructure/test-filter.js';
19
20
  import { paginateResult } from '../shared/paginate.js';
20
21
 
@@ -330,41 +331,138 @@ export function computeAllMetrics(functionNode, langId) {
330
331
  */
331
332
  export { _findFunctionNode as findFunctionNode };
332
333
 
333
- /**
334
- * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
335
- * compute complexity, and upsert into function_complexity table.
336
- *
337
- * @param {object} db - open better-sqlite3 database (read-write)
338
- * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
339
- * @param {string} rootDir - absolute project root path
340
- * @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
341
- */
342
- export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
343
- // Only initialize WASM parsers if some files lack both a cached tree AND pre-computed complexity
344
- let parsers = null;
345
- let extToLang = null;
346
- let needsFallback = false;
334
+ async function initWasmParsersIfNeeded(fileSymbols) {
347
335
  for (const [relPath, symbols] of fileSymbols) {
348
336
  if (!symbols._tree) {
349
- // Only consider files whose language actually has complexity rules
350
337
  const ext = path.extname(relPath).toLowerCase();
351
338
  if (!COMPLEXITY_EXTENSIONS.has(ext)) continue;
352
- // Check if all function/method defs have pre-computed complexity (native engine)
353
339
  const hasPrecomputed = symbols.definitions.every(
354
340
  (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
355
341
  );
356
342
  if (!hasPrecomputed) {
357
- needsFallback = true;
358
- break;
343
+ const { createParsers } = await import('../domain/parser.js');
344
+ const parsers = await createParsers();
345
+ const extToLang = buildExtToLangMap();
346
+ return { parsers, extToLang };
359
347
  }
360
348
  }
361
349
  }
362
- if (needsFallback) {
363
- const { createParsers } = await import('../domain/parser.js');
364
- parsers = await createParsers();
365
- extToLang = buildExtToLangMap();
350
+ return { parsers: null, extToLang: null };
351
+ }
352
+
353
+ function getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser) {
354
+ let tree = symbols._tree;
355
+ let langId = symbols._langId;
356
+
357
+ const allPrecomputed = symbols.definitions.every(
358
+ (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
359
+ );
360
+
361
+ if (!allPrecomputed && !tree) {
362
+ const ext = path.extname(relPath).toLowerCase();
363
+ if (!COMPLEXITY_EXTENSIONS.has(ext)) return null;
364
+ if (!extToLang) return null;
365
+ langId = extToLang.get(ext);
366
+ if (!langId) return null;
367
+
368
+ const absPath = path.join(rootDir, relPath);
369
+ let code;
370
+ try {
371
+ code = fs.readFileSync(absPath, 'utf-8');
372
+ } catch (e) {
373
+ debug(`complexity: cannot read ${relPath}: ${e.message}`);
374
+ return null;
375
+ }
376
+
377
+ const parser = getParser(parsers, absPath);
378
+ if (!parser) return null;
379
+
380
+ try {
381
+ tree = parser.parse(code);
382
+ } catch (e) {
383
+ debug(`complexity: parse failed for ${relPath}: ${e.message}`);
384
+ return null;
385
+ }
366
386
  }
367
387
 
388
+ return { tree, langId };
389
+ }
390
+
391
+ function upsertPrecomputedComplexity(db, upsert, def, relPath) {
392
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
393
+ if (!nodeId) return 0;
394
+ const ch = def.complexity.halstead;
395
+ const cl = def.complexity.loc;
396
+ upsert.run(
397
+ nodeId,
398
+ def.complexity.cognitive,
399
+ def.complexity.cyclomatic,
400
+ def.complexity.maxNesting ?? 0,
401
+ cl ? cl.loc : 0,
402
+ cl ? cl.sloc : 0,
403
+ cl ? cl.commentLines : 0,
404
+ ch ? ch.n1 : 0,
405
+ ch ? ch.n2 : 0,
406
+ ch ? ch.bigN1 : 0,
407
+ ch ? ch.bigN2 : 0,
408
+ ch ? ch.vocabulary : 0,
409
+ ch ? ch.length : 0,
410
+ ch ? ch.volume : 0,
411
+ ch ? ch.difficulty : 0,
412
+ ch ? ch.effort : 0,
413
+ ch ? ch.bugs : 0,
414
+ def.complexity.maintainabilityIndex ?? 0,
415
+ );
416
+ return 1;
417
+ }
418
+
419
+ function upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules) {
420
+ if (!tree || !rules) return 0;
421
+
422
+ const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
423
+ if (!funcNode) return 0;
424
+
425
+ const metrics = computeAllMetrics(funcNode, langId);
426
+ if (!metrics) return 0;
427
+
428
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
429
+ if (!nodeId) return 0;
430
+
431
+ const h = metrics.halstead;
432
+ upsert.run(
433
+ nodeId,
434
+ metrics.cognitive,
435
+ metrics.cyclomatic,
436
+ metrics.maxNesting,
437
+ metrics.loc.loc,
438
+ metrics.loc.sloc,
439
+ metrics.loc.commentLines,
440
+ h ? h.n1 : 0,
441
+ h ? h.n2 : 0,
442
+ h ? h.bigN1 : 0,
443
+ h ? h.bigN2 : 0,
444
+ h ? h.vocabulary : 0,
445
+ h ? h.length : 0,
446
+ h ? h.volume : 0,
447
+ h ? h.difficulty : 0,
448
+ h ? h.effort : 0,
449
+ h ? h.bugs : 0,
450
+ metrics.mi,
451
+ );
452
+ return 1;
453
+ }
454
+
455
+ /**
456
+ * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
457
+ * compute complexity, and upsert into function_complexity table.
458
+ *
459
+ * @param {object} db - open better-sqlite3 database (read-write)
460
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
461
+ * @param {string} rootDir - absolute project root path
462
+ * @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
463
+ */
464
+ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
465
+ const { parsers, extToLang } = await initWasmParsersIfNeeded(fileSymbols);
368
466
  const { getParser } = await import('../domain/parser.js');
369
467
 
370
468
  const upsert = db.prepare(
@@ -381,39 +479,9 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
381
479
 
382
480
  const tx = db.transaction(() => {
383
481
  for (const [relPath, symbols] of fileSymbols) {
384
- // Check if all function/method defs have pre-computed complexity
385
- const allPrecomputed = symbols.definitions.every(
386
- (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
387
- );
388
-
389
- let tree = symbols._tree;
390
- let langId = symbols._langId;
391
-
392
- // Only attempt WASM fallback if we actually need AST-based computation
393
- if (!allPrecomputed && !tree) {
394
- const ext = path.extname(relPath).toLowerCase();
395
- if (!COMPLEXITY_EXTENSIONS.has(ext)) continue; // Language has no complexity rules
396
- if (!extToLang) continue; // No WASM parsers available
397
- langId = extToLang.get(ext);
398
- if (!langId) continue;
399
-
400
- const absPath = path.join(rootDir, relPath);
401
- let code;
402
- try {
403
- code = fs.readFileSync(absPath, 'utf-8');
404
- } catch {
405
- continue;
406
- }
407
-
408
- const parser = getParser(parsers, absPath);
409
- if (!parser) continue;
410
-
411
- try {
412
- tree = parser.parse(code);
413
- } catch {
414
- continue;
415
- }
416
- }
482
+ const result = getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser);
483
+ const tree = result ? result.tree : null;
484
+ const langId = result ? result.langId : null;
417
485
 
418
486
  const rules = langId ? COMPLEXITY_RULES.get(langId) : null;
419
487
 
@@ -421,71 +489,11 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
421
489
  if (def.kind !== 'function' && def.kind !== 'method') continue;
422
490
  if (!def.line) continue;
423
491
 
424
- // Use pre-computed complexity from native engine if available
425
492
  if (def.complexity) {
426
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
427
- if (!nodeId) continue;
428
- const ch = def.complexity.halstead;
429
- const cl = def.complexity.loc;
430
- upsert.run(
431
- nodeId,
432
- def.complexity.cognitive,
433
- def.complexity.cyclomatic,
434
- def.complexity.maxNesting ?? 0,
435
- cl ? cl.loc : 0,
436
- cl ? cl.sloc : 0,
437
- cl ? cl.commentLines : 0,
438
- ch ? ch.n1 : 0,
439
- ch ? ch.n2 : 0,
440
- ch ? ch.bigN1 : 0,
441
- ch ? ch.bigN2 : 0,
442
- ch ? ch.vocabulary : 0,
443
- ch ? ch.length : 0,
444
- ch ? ch.volume : 0,
445
- ch ? ch.difficulty : 0,
446
- ch ? ch.effort : 0,
447
- ch ? ch.bugs : 0,
448
- def.complexity.maintainabilityIndex ?? 0,
449
- );
450
- analyzed++;
451
- continue;
493
+ analyzed += upsertPrecomputedComplexity(db, upsert, def, relPath);
494
+ } else {
495
+ analyzed += upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules);
452
496
  }
453
-
454
- // Fallback: compute from AST tree
455
- if (!tree || !rules) continue;
456
-
457
- const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
458
- if (!funcNode) continue;
459
-
460
- // Single-pass: complexity + Halstead + LOC + MI in one DFS walk
461
- const metrics = computeAllMetrics(funcNode, langId);
462
- if (!metrics) continue;
463
-
464
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
465
- if (!nodeId) continue;
466
-
467
- const h = metrics.halstead;
468
- upsert.run(
469
- nodeId,
470
- metrics.cognitive,
471
- metrics.cyclomatic,
472
- metrics.maxNesting,
473
- metrics.loc.loc,
474
- metrics.loc.sloc,
475
- metrics.loc.commentLines,
476
- h ? h.n1 : 0,
477
- h ? h.n2 : 0,
478
- h ? h.bigN1 : 0,
479
- h ? h.bigN2 : 0,
480
- h ? h.vocabulary : 0,
481
- h ? h.length : 0,
482
- h ? h.volume : 0,
483
- h ? h.difficulty : 0,
484
- h ? h.effort : 0,
485
- h ? h.bugs : 0,
486
- metrics.mi,
487
- );
488
- analyzed++;
489
497
  }
490
498
  }
491
499
  });
@@ -547,9 +555,10 @@ export function complexityData(customDbPath, opts = {}) {
547
555
  where += ' AND n.name LIKE ?';
548
556
  params.push(`%${target}%`);
549
557
  }
550
- if (fileFilter) {
551
- where += ' AND n.file LIKE ?';
552
- params.push(`%${fileFilter}%`);
558
+ {
559
+ const fc = buildFileConditionSQL(fileFilter, 'n.file');
560
+ where += fc.sql;
561
+ params.push(...fc.params);
553
562
  }
554
563
  if (kindFilter) {
555
564
  where += ' AND n.kind = ?';
@@ -606,13 +615,14 @@ export function complexityData(customDbPath, opts = {}) {
606
615
  ORDER BY ${orderBy}`,
607
616
  )
608
617
  .all(...params);
609
- } catch {
618
+ } catch (e) {
619
+ debug(`complexity query failed (table may not exist): ${e.message}`);
610
620
  // Check if graph has nodes even though complexity table is missing/empty
611
621
  let hasGraph = false;
612
622
  try {
613
623
  hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
614
- } catch {
615
- /* ignore */
624
+ } catch (e2) {
625
+ debug(`nodes table check failed: ${e2.message}`);
616
626
  }
617
627
  return { functions: [], summary: null, thresholds, hasGraph };
618
628
  }
@@ -701,8 +711,8 @@ export function complexityData(customDbPath, opts = {}) {
701
711
  ).length,
702
712
  };
703
713
  }
704
- } catch {
705
- /* ignore */
714
+ } catch (e) {
715
+ debug(`complexity summary query failed: ${e.message}`);
706
716
  }
707
717
 
708
718
  // When summary is null (no complexity rows), check if graph has nodes
@@ -710,8 +720,8 @@ export function complexityData(customDbPath, opts = {}) {
710
720
  if (summary === null) {
711
721
  try {
712
722
  hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
713
- } catch {
714
- /* ignore */
723
+ } catch (e) {
724
+ debug(`nodes table check failed: ${e.message}`);
715
725
  }
716
726
  }
717
727
 
@@ -753,9 +763,10 @@ export function* iterComplexity(customDbPath, opts = {}) {
753
763
  where += ' AND n.name LIKE ?';
754
764
  params.push(`%${opts.target}%`);
755
765
  }
756
- if (opts.file) {
757
- where += ' AND n.file LIKE ?';
758
- params.push(`%${opts.file}%`);
766
+ {
767
+ const fc = buildFileConditionSQL(opts.file, 'n.file');
768
+ where += fc.sql;
769
+ params.push(...fc.params);
759
770
  }
760
771
  if (opts.kind) {
761
772
  where += ' AND n.kind = ?';