@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.
- package/README.md +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- 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-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- 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/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
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 { getNodeId, openReadonlyOrFail, testFilterSQL } from './db.js';
|
|
4
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
4
5
|
import { debug } from './logger.js';
|
|
5
6
|
import { paginateResult } from './paginate.js';
|
|
6
|
-
import { isTestFile } from './queries.js';
|
|
7
7
|
|
|
8
8
|
// ─── Build-time: insert directory nodes, contains edges, and metrics ────
|
|
9
9
|
|
|
@@ -21,9 +21,12 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
21
21
|
const insertNode = db.prepare(
|
|
22
22
|
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
|
|
23
23
|
);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const getNodeIdStmt = {
|
|
25
|
+
get: (name, kind, file, line) => {
|
|
26
|
+
const id = getNodeId(db, name, kind, file, line);
|
|
27
|
+
return id != null ? { id } : undefined;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
27
30
|
const insertEdge = db.prepare(
|
|
28
31
|
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
|
|
29
32
|
);
|
|
@@ -56,12 +59,12 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
56
59
|
}
|
|
57
60
|
// Delete metrics for changed files
|
|
58
61
|
for (const f of changedFiles) {
|
|
59
|
-
const fileRow =
|
|
62
|
+
const fileRow = getNodeIdStmt.get(f, 'file', f, 0);
|
|
60
63
|
if (fileRow) deleteMetricForNode.run(fileRow.id);
|
|
61
64
|
}
|
|
62
65
|
// Delete metrics for affected directories
|
|
63
66
|
for (const dir of affectedDirs) {
|
|
64
|
-
const dirRow =
|
|
67
|
+
const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
|
|
65
68
|
if (dirRow) deleteMetricForNode.run(dirRow.id);
|
|
66
69
|
}
|
|
67
70
|
})();
|
|
@@ -126,8 +129,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
126
129
|
if (!dir || dir === '.') continue;
|
|
127
130
|
// On incremental, skip dirs whose contains edges are intact
|
|
128
131
|
if (affectedDirs && !affectedDirs.has(dir)) continue;
|
|
129
|
-
const dirRow =
|
|
130
|
-
const fileRow =
|
|
132
|
+
const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
|
|
133
|
+
const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
131
134
|
if (dirRow && fileRow) {
|
|
132
135
|
insertEdge.run(dirRow.id, fileRow.id, 'contains', 1.0, 0);
|
|
133
136
|
}
|
|
@@ -138,8 +141,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
138
141
|
if (!parent || parent === '.' || parent === dir) continue;
|
|
139
142
|
// On incremental, skip parent dirs whose contains edges are intact
|
|
140
143
|
if (affectedDirs && !affectedDirs.has(parent)) continue;
|
|
141
|
-
const parentRow =
|
|
142
|
-
const childRow =
|
|
144
|
+
const parentRow = getNodeIdStmt.get(parent, 'directory', parent, 0);
|
|
145
|
+
const childRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
|
|
143
146
|
if (parentRow && childRow) {
|
|
144
147
|
insertEdge.run(parentRow.id, childRow.id, 'contains', 1.0, 0);
|
|
145
148
|
}
|
|
@@ -169,7 +172,7 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
169
172
|
|
|
170
173
|
const computeFileMetrics = db.transaction(() => {
|
|
171
174
|
for (const [relPath, symbols] of fileSymbols) {
|
|
172
|
-
const fileRow =
|
|
175
|
+
const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
173
176
|
if (!fileRow) continue;
|
|
174
177
|
|
|
175
178
|
const lineCount = lineCountMap.get(relPath) || 0;
|
|
@@ -263,7 +266,7 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
263
266
|
|
|
264
267
|
const computeDirMetrics = db.transaction(() => {
|
|
265
268
|
for (const [dir, files] of dirFiles) {
|
|
266
|
-
const dirRow =
|
|
269
|
+
const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
|
|
267
270
|
if (!dirRow) continue;
|
|
268
271
|
|
|
269
272
|
const fileCount = files.length;
|
|
@@ -413,115 +416,117 @@ export function classifyNodeRoles(db) {
|
|
|
413
416
|
*/
|
|
414
417
|
export function structureData(customDbPath, opts = {}) {
|
|
415
418
|
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
|
|
419
|
+
try {
|
|
420
|
+
const rawDir = opts.directory || null;
|
|
421
|
+
const filterDir = rawDir && normalizePath(rawDir) !== '.' ? rawDir : null;
|
|
422
|
+
const maxDepth = opts.depth || null;
|
|
423
|
+
const sortBy = opts.sort || 'files';
|
|
424
|
+
const noTests = opts.noTests || false;
|
|
425
|
+
const full = opts.full || false;
|
|
426
|
+
const fileLimit = opts.fileLimit || 25;
|
|
427
|
+
|
|
428
|
+
// Get all directory nodes with their metrics
|
|
429
|
+
let dirs = db
|
|
454
430
|
.prepare(`
|
|
455
|
-
SELECT n.name,
|
|
456
|
-
FROM
|
|
457
|
-
JOIN nodes n ON e.target_id = n.id
|
|
431
|
+
SELECT n.id, n.name, n.file, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
432
|
+
FROM nodes n
|
|
458
433
|
LEFT JOIN node_metrics nm ON n.id = nm.node_id
|
|
459
|
-
WHERE
|
|
434
|
+
WHERE n.kind = 'directory'
|
|
460
435
|
`)
|
|
461
|
-
.all(
|
|
462
|
-
if (noTests) files = files.filter((f) => !isTestFile(f.name));
|
|
436
|
+
.all();
|
|
463
437
|
|
|
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
|
-
});
|
|
438
|
+
if (filterDir) {
|
|
439
|
+
const norm = normalizePath(filterDir);
|
|
440
|
+
dirs = dirs.filter((d) => d.name === norm || d.name.startsWith(`${norm}/`));
|
|
441
|
+
}
|
|
494
442
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
443
|
+
if (maxDepth) {
|
|
444
|
+
const baseDepth = filterDir ? normalizePath(filterDir).split('/').length : 0;
|
|
445
|
+
dirs = dirs.filter((d) => {
|
|
446
|
+
const depth = d.name.split('/').length - baseDepth;
|
|
447
|
+
return depth <= maxDepth;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Sort
|
|
452
|
+
const sortFn = getSortFn(sortBy);
|
|
453
|
+
dirs.sort(sortFn);
|
|
454
|
+
|
|
455
|
+
// Get file metrics for each directory
|
|
456
|
+
const result = dirs.map((d) => {
|
|
457
|
+
let files = db
|
|
458
|
+
.prepare(`
|
|
459
|
+
SELECT n.name, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count, nm.fan_in, nm.fan_out
|
|
460
|
+
FROM edges e
|
|
461
|
+
JOIN nodes n ON e.target_id = n.id
|
|
462
|
+
LEFT JOIN node_metrics nm ON n.id = nm.node_id
|
|
463
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
464
|
+
`)
|
|
465
|
+
.all(d.id);
|
|
466
|
+
if (noTests) files = files.filter((f) => !isTestFile(f.name));
|
|
467
|
+
|
|
468
|
+
const subdirs = db
|
|
469
|
+
.prepare(`
|
|
470
|
+
SELECT n.name
|
|
471
|
+
FROM edges e
|
|
472
|
+
JOIN nodes n ON e.target_id = n.id
|
|
473
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'directory'
|
|
474
|
+
`)
|
|
475
|
+
.all(d.id);
|
|
476
|
+
|
|
477
|
+
const fileCount = noTests ? files.length : d.file_count || 0;
|
|
514
478
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
479
|
+
directory: d.name,
|
|
480
|
+
fileCount,
|
|
481
|
+
symbolCount: d.symbol_count || 0,
|
|
482
|
+
fanIn: d.fan_in || 0,
|
|
483
|
+
fanOut: d.fan_out || 0,
|
|
484
|
+
cohesion: d.cohesion,
|
|
485
|
+
density: fileCount > 0 ? (d.symbol_count || 0) / fileCount : 0,
|
|
486
|
+
files: files.map((f) => ({
|
|
487
|
+
file: f.name,
|
|
488
|
+
lineCount: f.line_count || 0,
|
|
489
|
+
symbolCount: f.symbol_count || 0,
|
|
490
|
+
importCount: f.import_count || 0,
|
|
491
|
+
exportCount: f.export_count || 0,
|
|
492
|
+
fanIn: f.fan_in || 0,
|
|
493
|
+
fanOut: f.fan_out || 0,
|
|
494
|
+
})),
|
|
495
|
+
subdirectories: subdirs.map((s) => s.name),
|
|
519
496
|
};
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Apply global file limit unless full mode
|
|
500
|
+
if (!full) {
|
|
501
|
+
const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
|
|
502
|
+
if (totalFiles > fileLimit) {
|
|
503
|
+
let shown = 0;
|
|
504
|
+
for (const d of result) {
|
|
505
|
+
const remaining = fileLimit - shown;
|
|
506
|
+
if (remaining <= 0) {
|
|
507
|
+
d.files = [];
|
|
508
|
+
} else if (d.files.length > remaining) {
|
|
509
|
+
d.files = d.files.slice(0, remaining);
|
|
510
|
+
shown = fileLimit;
|
|
511
|
+
} else {
|
|
512
|
+
shown += d.files.length;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const suppressed = totalFiles - fileLimit;
|
|
516
|
+
return {
|
|
517
|
+
directories: result,
|
|
518
|
+
count: result.length,
|
|
519
|
+
suppressed,
|
|
520
|
+
warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
520
523
|
}
|
|
521
|
-
}
|
|
522
524
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
+
const base = { directories: result, count: result.length };
|
|
526
|
+
return paginateResult(base, 'directories', { limit: opts.limit, offset: opts.offset });
|
|
527
|
+
} finally {
|
|
528
|
+
db.close();
|
|
529
|
+
}
|
|
525
530
|
}
|
|
526
531
|
|
|
527
532
|
/**
|
|
@@ -529,71 +534,67 @@ export function structureData(customDbPath, opts = {}) {
|
|
|
529
534
|
*/
|
|
530
535
|
export function hotspotsData(customDbPath, opts = {}) {
|
|
531
536
|
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
|
-
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
|
-
};
|
|
537
|
+
try {
|
|
538
|
+
const metric = opts.metric || 'fan-in';
|
|
539
|
+
const level = opts.level || 'file';
|
|
540
|
+
const limit = opts.limit || 10;
|
|
541
|
+
const noTests = opts.noTests || false;
|
|
542
|
+
|
|
543
|
+
const kind = level === 'directory' ? 'directory' : 'file';
|
|
544
|
+
|
|
545
|
+
const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
|
|
546
|
+
|
|
547
|
+
const HOTSPOT_QUERIES = {
|
|
548
|
+
'fan-in': db.prepare(`
|
|
549
|
+
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
550
|
+
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
551
|
+
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
552
|
+
WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
|
|
553
|
+
'fan-out': db.prepare(`
|
|
554
|
+
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
555
|
+
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
556
|
+
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
557
|
+
WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
|
|
558
|
+
density: db.prepare(`
|
|
559
|
+
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
560
|
+
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
561
|
+
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
562
|
+
WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
|
|
563
|
+
coupling: db.prepare(`
|
|
564
|
+
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
565
|
+
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
566
|
+
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
567
|
+
WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
|
|
568
|
+
};
|
|
570
569
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
570
|
+
const stmt = HOTSPOT_QUERIES[metric] || HOTSPOT_QUERIES['fan-in'];
|
|
571
|
+
const rows = stmt.all(kind, limit);
|
|
572
|
+
|
|
573
|
+
const hotspots = rows.map((r) => ({
|
|
574
|
+
name: r.name,
|
|
575
|
+
kind: r.kind,
|
|
576
|
+
lineCount: r.line_count,
|
|
577
|
+
symbolCount: r.symbol_count,
|
|
578
|
+
importCount: r.import_count,
|
|
579
|
+
exportCount: r.export_count,
|
|
580
|
+
fanIn: r.fan_in,
|
|
581
|
+
fanOut: r.fan_out,
|
|
582
|
+
cohesion: r.cohesion,
|
|
583
|
+
fileCount: r.file_count,
|
|
584
|
+
density:
|
|
585
|
+
r.file_count > 0
|
|
586
|
+
? (r.symbol_count || 0) / r.file_count
|
|
587
|
+
: r.line_count > 0
|
|
588
|
+
? (r.symbol_count || 0) / r.line_count
|
|
589
|
+
: 0,
|
|
590
|
+
coupling: (r.fan_in || 0) + (r.fan_out || 0),
|
|
591
|
+
}));
|
|
592
|
+
|
|
593
|
+
const base = { metric, level, limit, hotspots };
|
|
594
|
+
return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
|
|
595
|
+
} finally {
|
|
596
|
+
db.close();
|
|
597
|
+
}
|
|
597
598
|
}
|
|
598
599
|
|
|
599
600
|
/**
|
|
@@ -601,104 +602,45 @@ export function hotspotsData(customDbPath, opts = {}) {
|
|
|
601
602
|
*/
|
|
602
603
|
export function moduleBoundariesData(customDbPath, opts = {}) {
|
|
603
604
|
const db = openReadonlyOrFail(customDbPath);
|
|
604
|
-
|
|
605
|
-
|
|
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);
|
|
605
|
+
try {
|
|
606
|
+
const threshold = opts.threshold || 0.3;
|
|
615
607
|
|
|
616
|
-
|
|
617
|
-
// Get files inside this directory
|
|
618
|
-
const files = db
|
|
608
|
+
const dirs = db
|
|
619
609
|
.prepare(`
|
|
620
|
-
SELECT n.name
|
|
621
|
-
|
|
622
|
-
|
|
610
|
+
SELECT n.id, n.name, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
611
|
+
FROM nodes n
|
|
612
|
+
JOIN node_metrics nm ON n.id = nm.node_id
|
|
613
|
+
WHERE n.kind = 'directory' AND nm.cohesion IS NOT NULL AND nm.cohesion >= ?
|
|
614
|
+
ORDER BY nm.cohesion DESC
|
|
623
615
|
`)
|
|
624
|
-
.all(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
db.close();
|
|
639
|
-
return { threshold, modules, count: modules.length };
|
|
640
|
-
}
|
|
616
|
+
.all(threshold);
|
|
617
|
+
|
|
618
|
+
const modules = dirs.map((d) => {
|
|
619
|
+
// Get files inside this directory
|
|
620
|
+
const files = db
|
|
621
|
+
.prepare(`
|
|
622
|
+
SELECT n.name FROM edges e
|
|
623
|
+
JOIN nodes n ON e.target_id = n.id
|
|
624
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
625
|
+
`)
|
|
626
|
+
.all(d.id)
|
|
627
|
+
.map((f) => f.name);
|
|
641
628
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
lines.push(
|
|
653
|
-
`${indent}${d.directory}/ (${d.fileCount} files, ${d.symbolCount} symbols, <-${d.fanIn} ->${d.fanOut}${cohStr})`,
|
|
654
|
-
);
|
|
655
|
-
for (const f of d.files) {
|
|
656
|
-
lines.push(
|
|
657
|
-
`${indent} ${path.basename(f.file)} ${f.lineCount}L ${f.symbolCount}sym <-${f.fanIn} ->${f.fanOut}`,
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
if (data.warning) {
|
|
662
|
-
lines.push('');
|
|
663
|
-
lines.push(`⚠ ${data.warning}`);
|
|
664
|
-
}
|
|
665
|
-
return lines.join('\n');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export function formatHotspots(data) {
|
|
669
|
-
if (data.hotspots.length === 0) return 'No hotspots found. Run "codegraph build" first.';
|
|
670
|
-
|
|
671
|
-
const lines = [`\nHotspots by ${data.metric} (${data.level}-level, top ${data.limit}):\n`];
|
|
672
|
-
let rank = 1;
|
|
673
|
-
for (const h of data.hotspots) {
|
|
674
|
-
const extra =
|
|
675
|
-
h.kind === 'directory'
|
|
676
|
-
? `${h.fileCount} files, cohesion=${h.cohesion !== null ? h.cohesion.toFixed(2) : 'n/a'}`
|
|
677
|
-
: `${h.lineCount || 0}L, ${h.symbolCount || 0} symbols`;
|
|
678
|
-
lines.push(
|
|
679
|
-
` ${String(rank++).padStart(2)}. ${h.name} <-${h.fanIn || 0} ->${h.fanOut || 0} (${extra})`,
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
return lines.join('\n');
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
export function formatModuleBoundaries(data) {
|
|
686
|
-
if (data.count === 0) return `No modules found with cohesion >= ${data.threshold}.`;
|
|
629
|
+
return {
|
|
630
|
+
directory: d.name,
|
|
631
|
+
cohesion: d.cohesion,
|
|
632
|
+
fileCount: d.file_count || 0,
|
|
633
|
+
symbolCount: d.symbol_count || 0,
|
|
634
|
+
fanIn: d.fan_in || 0,
|
|
635
|
+
fanOut: d.fan_out || 0,
|
|
636
|
+
files,
|
|
637
|
+
};
|
|
638
|
+
});
|
|
687
639
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
` ${m.directory}/ cohesion=${m.cohesion.toFixed(2)} (${m.fileCount} files, ${m.symbolCount} symbols)`,
|
|
692
|
-
);
|
|
693
|
-
lines.push(` Incoming: ${m.fanIn} edges Outgoing: ${m.fanOut} edges`);
|
|
694
|
-
if (m.files.length > 0) {
|
|
695
|
-
lines.push(
|
|
696
|
-
` Files: ${m.files.slice(0, 5).join(', ')}${m.files.length > 5 ? ` ... +${m.files.length - 5}` : ''}`,
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
lines.push('');
|
|
640
|
+
return { threshold, modules, count: modules.length };
|
|
641
|
+
} finally {
|
|
642
|
+
db.close();
|
|
700
643
|
}
|
|
701
|
-
return lines.join('\n');
|
|
702
644
|
}
|
|
703
645
|
|
|
704
646
|
// ─── Helpers ──────────────────────────────────────────────────────────
|