@optave/codegraph 3.0.4 → 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.
- package/README.md +59 -52
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +9 -10
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast.js +15 -28
- package/src/audit.js +4 -5
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +84 -79
- package/src/builder.js +274 -159
- package/src/cfg.js +111 -341
- package/src/check.js +3 -3
- package/src/cli.js +122 -167
- package/src/cochange.js +1 -1
- package/src/communities.js +13 -16
- package/src/complexity.js +196 -1239
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -697
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository.js +134 -0
- package/src/db.js +19 -392
- package/src/embedder.js +145 -141
- package/src/export.js +1 -1
- package/src/flow.js +160 -228
- package/src/index.js +36 -2
- package/src/kinds.js +49 -0
- package/src/manifesto.js +3 -8
- package/src/mcp.js +97 -20
- package/src/owners.js +132 -132
- package/src/parser.js +58 -131
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1356 -2261
- package/src/resolve.js +11 -2
- package/src/result-formatter.js +21 -0
- package/src/sequence.js +364 -0
- package/src/structure.js +200 -199
- package/src/test-filter.js +7 -0
- package/src/triage.js +120 -162
- 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 './
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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,
|
|
456
|
-
FROM
|
|
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
|
|
431
|
+
WHERE n.kind = 'directory'
|
|
460
432
|
`)
|
|
461
|
-
.all(
|
|
462
|
-
if (noTests) files = files.filter((f) => !isTestFile(f.name));
|
|
433
|
+
.all();
|
|
463
434
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
noTests && kind === 'file'
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
602
|
+
try {
|
|
603
|
+
const threshold = opts.threshold || 0.3;
|
|
605
604
|
|
|
606
|
-
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
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(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
+
}
|