@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/structure.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { normalizePath } from './constants.js';
3
- import { openReadonlyOrFail } from './db.js';
3
+ import { openReadonlyOrFail, testFilterSQL } from './db.js';
4
4
  import { debug } from './logger.js';
5
5
  import { paginateResult } from './paginate.js';
6
- import { isTestFile } from './queries.js';
6
+ import { isTestFile } from './test-filter.js';
7
7
 
8
8
  // ─── Build-time: insert directory nodes, contains edges, and metrics ────
9
9
 
@@ -413,115 +413,117 @@ export function classifyNodeRoles(db) {
413
413
  */
414
414
  export function structureData(customDbPath, opts = {}) {
415
415
  const db = openReadonlyOrFail(customDbPath);
416
- const rawDir = opts.directory || null;
417
- const filterDir = rawDir && normalizePath(rawDir) !== '.' ? rawDir : null;
418
- const maxDepth = opts.depth || null;
419
- const sortBy = opts.sort || 'files';
420
- const noTests = opts.noTests || false;
421
- const full = opts.full || false;
422
- const fileLimit = opts.fileLimit || 25;
423
-
424
- // Get all directory nodes with their metrics
425
- let dirs = db
426
- .prepare(`
427
- SELECT n.id, n.name, n.file, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
428
- FROM nodes n
429
- LEFT JOIN node_metrics nm ON n.id = nm.node_id
430
- WHERE n.kind = 'directory'
431
- `)
432
- .all();
433
-
434
- if (filterDir) {
435
- const norm = normalizePath(filterDir);
436
- dirs = dirs.filter((d) => d.name === norm || d.name.startsWith(`${norm}/`));
437
- }
438
-
439
- if (maxDepth) {
440
- const baseDepth = filterDir ? normalizePath(filterDir).split('/').length : 0;
441
- dirs = dirs.filter((d) => {
442
- const depth = d.name.split('/').length - baseDepth;
443
- return depth <= maxDepth;
444
- });
445
- }
446
-
447
- // Sort
448
- const sortFn = getSortFn(sortBy);
449
- dirs.sort(sortFn);
450
-
451
- // Get file metrics for each directory
452
- const result = dirs.map((d) => {
453
- let files = db
416
+ try {
417
+ const rawDir = opts.directory || null;
418
+ const filterDir = rawDir && normalizePath(rawDir) !== '.' ? rawDir : null;
419
+ const maxDepth = opts.depth || null;
420
+ const sortBy = opts.sort || 'files';
421
+ const noTests = opts.noTests || false;
422
+ const full = opts.full || false;
423
+ const fileLimit = opts.fileLimit || 25;
424
+
425
+ // Get all directory nodes with their metrics
426
+ let dirs = db
454
427
  .prepare(`
455
- SELECT n.name, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count, nm.fan_in, nm.fan_out
456
- FROM edges e
457
- JOIN nodes n ON e.target_id = n.id
428
+ SELECT n.id, n.name, n.file, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
429
+ FROM nodes n
458
430
  LEFT JOIN node_metrics nm ON n.id = nm.node_id
459
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
431
+ WHERE n.kind = 'directory'
460
432
  `)
461
- .all(d.id);
462
- if (noTests) files = files.filter((f) => !isTestFile(f.name));
433
+ .all();
463
434
 
464
- const subdirs = db
465
- .prepare(`
466
- SELECT n.name
467
- FROM edges e
468
- JOIN nodes n ON e.target_id = n.id
469
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'directory'
470
- `)
471
- .all(d.id);
472
-
473
- const fileCount = noTests ? files.length : d.file_count || 0;
474
- return {
475
- directory: d.name,
476
- fileCount,
477
- symbolCount: d.symbol_count || 0,
478
- fanIn: d.fan_in || 0,
479
- fanOut: d.fan_out || 0,
480
- cohesion: d.cohesion,
481
- density: fileCount > 0 ? (d.symbol_count || 0) / fileCount : 0,
482
- files: files.map((f) => ({
483
- file: f.name,
484
- lineCount: f.line_count || 0,
485
- symbolCount: f.symbol_count || 0,
486
- importCount: f.import_count || 0,
487
- exportCount: f.export_count || 0,
488
- fanIn: f.fan_in || 0,
489
- fanOut: f.fan_out || 0,
490
- })),
491
- subdirectories: subdirs.map((s) => s.name),
492
- };
493
- });
435
+ if (filterDir) {
436
+ const norm = normalizePath(filterDir);
437
+ dirs = dirs.filter((d) => d.name === norm || d.name.startsWith(`${norm}/`));
438
+ }
494
439
 
495
- db.close();
496
-
497
- // Apply global file limit unless full mode
498
- if (!full) {
499
- const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
500
- if (totalFiles > fileLimit) {
501
- let shown = 0;
502
- for (const d of result) {
503
- const remaining = fileLimit - shown;
504
- if (remaining <= 0) {
505
- d.files = [];
506
- } else if (d.files.length > remaining) {
507
- d.files = d.files.slice(0, remaining);
508
- shown = fileLimit;
509
- } else {
510
- shown += d.files.length;
511
- }
512
- }
513
- const suppressed = totalFiles - fileLimit;
440
+ if (maxDepth) {
441
+ const baseDepth = filterDir ? normalizePath(filterDir).split('/').length : 0;
442
+ dirs = dirs.filter((d) => {
443
+ const depth = d.name.split('/').length - baseDepth;
444
+ return depth <= maxDepth;
445
+ });
446
+ }
447
+
448
+ // Sort
449
+ const sortFn = getSortFn(sortBy);
450
+ dirs.sort(sortFn);
451
+
452
+ // Get file metrics for each directory
453
+ const result = dirs.map((d) => {
454
+ let files = db
455
+ .prepare(`
456
+ SELECT n.name, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count, nm.fan_in, nm.fan_out
457
+ FROM edges e
458
+ JOIN nodes n ON e.target_id = n.id
459
+ LEFT JOIN node_metrics nm ON n.id = nm.node_id
460
+ WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
461
+ `)
462
+ .all(d.id);
463
+ if (noTests) files = files.filter((f) => !isTestFile(f.name));
464
+
465
+ const subdirs = db
466
+ .prepare(`
467
+ SELECT n.name
468
+ FROM edges e
469
+ JOIN nodes n ON e.target_id = n.id
470
+ WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'directory'
471
+ `)
472
+ .all(d.id);
473
+
474
+ const fileCount = noTests ? files.length : d.file_count || 0;
514
475
  return {
515
- directories: result,
516
- count: result.length,
517
- suppressed,
518
- warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
476
+ directory: d.name,
477
+ fileCount,
478
+ symbolCount: d.symbol_count || 0,
479
+ fanIn: d.fan_in || 0,
480
+ fanOut: d.fan_out || 0,
481
+ cohesion: d.cohesion,
482
+ density: fileCount > 0 ? (d.symbol_count || 0) / fileCount : 0,
483
+ files: files.map((f) => ({
484
+ file: f.name,
485
+ lineCount: f.line_count || 0,
486
+ symbolCount: f.symbol_count || 0,
487
+ importCount: f.import_count || 0,
488
+ exportCount: f.export_count || 0,
489
+ fanIn: f.fan_in || 0,
490
+ fanOut: f.fan_out || 0,
491
+ })),
492
+ subdirectories: subdirs.map((s) => s.name),
519
493
  };
494
+ });
495
+
496
+ // Apply global file limit unless full mode
497
+ if (!full) {
498
+ const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
499
+ if (totalFiles > fileLimit) {
500
+ let shown = 0;
501
+ for (const d of result) {
502
+ const remaining = fileLimit - shown;
503
+ if (remaining <= 0) {
504
+ d.files = [];
505
+ } else if (d.files.length > remaining) {
506
+ d.files = d.files.slice(0, remaining);
507
+ shown = fileLimit;
508
+ } else {
509
+ shown += d.files.length;
510
+ }
511
+ }
512
+ const suppressed = totalFiles - fileLimit;
513
+ return {
514
+ directories: result,
515
+ count: result.length,
516
+ suppressed,
517
+ warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
518
+ };
519
+ }
520
520
  }
521
- }
522
521
 
523
- const base = { directories: result, count: result.length };
524
- return paginateResult(base, 'directories', { limit: opts.limit, offset: opts.offset });
522
+ const base = { directories: result, count: result.length };
523
+ return paginateResult(base, 'directories', { limit: opts.limit, offset: opts.offset });
524
+ } finally {
525
+ db.close();
526
+ }
525
527
  }
526
528
 
527
529
  /**
@@ -529,71 +531,67 @@ export function structureData(customDbPath, opts = {}) {
529
531
  */
530
532
  export function hotspotsData(customDbPath, opts = {}) {
531
533
  const db = openReadonlyOrFail(customDbPath);
532
- const metric = opts.metric || 'fan-in';
533
- const level = opts.level || 'file';
534
- const limit = opts.limit || 10;
535
- const noTests = opts.noTests || false;
536
-
537
- const kind = level === 'directory' ? 'directory' : 'file';
538
-
539
- const testFilter =
540
- noTests && kind === 'file'
541
- ? `AND n.name NOT LIKE '%.test.%'
542
- AND n.name NOT LIKE '%.spec.%'
543
- AND n.name NOT LIKE '%__test__%'
544
- AND n.name NOT LIKE '%__tests__%'
545
- AND n.name NOT LIKE '%.stories.%'`
546
- : '';
547
-
548
- const HOTSPOT_QUERIES = {
549
- 'fan-in': db.prepare(`
550
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
551
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
552
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
553
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
554
- 'fan-out': db.prepare(`
555
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
556
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
557
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
558
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
559
- density: db.prepare(`
560
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
561
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
562
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
563
- WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
564
- coupling: db.prepare(`
565
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
566
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
567
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
568
- WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
569
- };
570
-
571
- const stmt = HOTSPOT_QUERIES[metric] || HOTSPOT_QUERIES['fan-in'];
572
- const rows = stmt.all(kind, limit);
573
-
574
- const hotspots = rows.map((r) => ({
575
- name: r.name,
576
- kind: r.kind,
577
- lineCount: r.line_count,
578
- symbolCount: r.symbol_count,
579
- importCount: r.import_count,
580
- exportCount: r.export_count,
581
- fanIn: r.fan_in,
582
- fanOut: r.fan_out,
583
- cohesion: r.cohesion,
584
- fileCount: r.file_count,
585
- density:
586
- r.file_count > 0
587
- ? (r.symbol_count || 0) / r.file_count
588
- : r.line_count > 0
589
- ? (r.symbol_count || 0) / r.line_count
590
- : 0,
591
- coupling: (r.fan_in || 0) + (r.fan_out || 0),
592
- }));
593
-
594
- db.close();
595
- const base = { metric, level, limit, hotspots };
596
- return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
534
+ try {
535
+ const metric = opts.metric || 'fan-in';
536
+ const level = opts.level || 'file';
537
+ const limit = opts.limit || 10;
538
+ const noTests = opts.noTests || false;
539
+
540
+ const kind = level === 'directory' ? 'directory' : 'file';
541
+
542
+ const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
543
+
544
+ const HOTSPOT_QUERIES = {
545
+ 'fan-in': db.prepare(`
546
+ SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
547
+ nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
548
+ FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
549
+ WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
550
+ 'fan-out': db.prepare(`
551
+ SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
552
+ nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
553
+ FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
554
+ WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
555
+ density: db.prepare(`
556
+ SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
557
+ nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
558
+ FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
559
+ WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
560
+ coupling: db.prepare(`
561
+ SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
562
+ nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
563
+ FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
564
+ WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
565
+ };
566
+
567
+ const stmt = HOTSPOT_QUERIES[metric] || HOTSPOT_QUERIES['fan-in'];
568
+ const rows = stmt.all(kind, limit);
569
+
570
+ const hotspots = rows.map((r) => ({
571
+ name: r.name,
572
+ kind: r.kind,
573
+ lineCount: r.line_count,
574
+ symbolCount: r.symbol_count,
575
+ importCount: r.import_count,
576
+ exportCount: r.export_count,
577
+ fanIn: r.fan_in,
578
+ fanOut: r.fan_out,
579
+ cohesion: r.cohesion,
580
+ fileCount: r.file_count,
581
+ density:
582
+ r.file_count > 0
583
+ ? (r.symbol_count || 0) / r.file_count
584
+ : r.line_count > 0
585
+ ? (r.symbol_count || 0) / r.line_count
586
+ : 0,
587
+ coupling: (r.fan_in || 0) + (r.fan_out || 0),
588
+ }));
589
+
590
+ const base = { metric, level, limit, hotspots };
591
+ return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
592
+ } finally {
593
+ db.close();
594
+ }
597
595
  }
598
596
 
599
597
  /**
@@ -601,42 +599,45 @@ export function hotspotsData(customDbPath, opts = {}) {
601
599
  */
602
600
  export function moduleBoundariesData(customDbPath, opts = {}) {
603
601
  const db = openReadonlyOrFail(customDbPath);
604
- const threshold = opts.threshold || 0.3;
602
+ try {
603
+ const threshold = opts.threshold || 0.3;
605
604
 
606
- const dirs = db
607
- .prepare(`
608
- SELECT n.id, n.name, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
609
- FROM nodes n
610
- JOIN node_metrics nm ON n.id = nm.node_id
611
- WHERE n.kind = 'directory' AND nm.cohesion IS NOT NULL AND nm.cohesion >= ?
612
- ORDER BY nm.cohesion DESC
613
- `)
614
- .all(threshold);
615
-
616
- const modules = dirs.map((d) => {
617
- // Get files inside this directory
618
- const files = db
605
+ const dirs = db
619
606
  .prepare(`
620
- SELECT n.name FROM edges e
621
- JOIN nodes n ON e.target_id = n.id
622
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
607
+ SELECT n.id, n.name, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
608
+ FROM nodes n
609
+ JOIN node_metrics nm ON n.id = nm.node_id
610
+ WHERE n.kind = 'directory' AND nm.cohesion IS NOT NULL AND nm.cohesion >= ?
611
+ ORDER BY nm.cohesion DESC
623
612
  `)
624
- .all(d.id)
625
- .map((f) => f.name);
626
-
627
- return {
628
- directory: d.name,
629
- cohesion: d.cohesion,
630
- fileCount: d.file_count || 0,
631
- symbolCount: d.symbol_count || 0,
632
- fanIn: d.fan_in || 0,
633
- fanOut: d.fan_out || 0,
634
- files,
635
- };
636
- });
613
+ .all(threshold);
614
+
615
+ const modules = dirs.map((d) => {
616
+ // Get files inside this directory
617
+ const files = db
618
+ .prepare(`
619
+ SELECT n.name FROM edges e
620
+ JOIN nodes n ON e.target_id = n.id
621
+ WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
622
+ `)
623
+ .all(d.id)
624
+ .map((f) => f.name);
637
625
 
638
- db.close();
639
- return { threshold, modules, count: modules.length };
626
+ return {
627
+ directory: d.name,
628
+ cohesion: d.cohesion,
629
+ fileCount: d.file_count || 0,
630
+ symbolCount: d.symbol_count || 0,
631
+ fanIn: d.fan_in || 0,
632
+ fanOut: d.fan_out || 0,
633
+ files,
634
+ };
635
+ });
636
+
637
+ return { threshold, modules, count: modules.length };
638
+ } finally {
639
+ db.close();
640
+ }
640
641
  }
641
642
 
642
643
  // ─── Formatters ───────────────────────────────────────────────────────
@@ -0,0 +1,7 @@
1
+ /** Pattern matching test/spec/stories files. */
2
+ export const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
3
+
4
+ /** Check whether a file path looks like a test file. */
5
+ export function isTestFile(filePath) {
6
+ return TEST_PATTERN.test(filePath);
7
+ }