@optave/codegraph 2.4.0 → 2.5.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 +66 -10
- package/package.json +15 -5
- package/src/branch-compare.js +568 -0
- package/src/builder.js +183 -22
- package/src/cli.js +253 -8
- package/src/cochange.js +8 -8
- package/src/communities.js +303 -0
- package/src/complexity.js +2056 -0
- package/src/config.js +20 -1
- package/src/db.js +111 -1
- package/src/embedder.js +49 -12
- package/src/export.js +25 -1
- package/src/flow.js +361 -0
- package/src/index.js +32 -2
- package/src/manifesto.js +442 -0
- package/src/mcp.js +244 -5
- package/src/paginate.js +70 -0
- package/src/parser.js +21 -5
- package/src/queries.js +396 -7
- package/src/registry.js +6 -3
- package/src/structure.js +88 -24
- package/src/update-check.js +1 -0
- package/src/watcher.js +2 -2
package/src/queries.js
CHANGED
|
@@ -5,6 +5,7 @@ import { coChangeForFiles } from './cochange.js';
|
|
|
5
5
|
import { findCycles } from './cycles.js';
|
|
6
6
|
import { findDbPath, openReadonlyOrFail } from './db.js';
|
|
7
7
|
import { debug } from './logger.js';
|
|
8
|
+
import { paginateResult } from './paginate.js';
|
|
8
9
|
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -172,7 +173,7 @@ function findMatchingNodes(db, name, opts = {}) {
|
|
|
172
173
|
return nodes;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
function kindIcon(kind) {
|
|
176
|
+
export function kindIcon(kind) {
|
|
176
177
|
switch (kind) {
|
|
177
178
|
case 'function':
|
|
178
179
|
return 'f';
|
|
@@ -248,7 +249,8 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
248
249
|
});
|
|
249
250
|
|
|
250
251
|
db.close();
|
|
251
|
-
|
|
252
|
+
const base = { query: name, results };
|
|
253
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
@@ -565,6 +567,255 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
565
567
|
return { name, results };
|
|
566
568
|
}
|
|
567
569
|
|
|
570
|
+
export function pathData(from, to, customDbPath, opts = {}) {
|
|
571
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
572
|
+
const noTests = opts.noTests || false;
|
|
573
|
+
const maxDepth = opts.maxDepth || 10;
|
|
574
|
+
const edgeKinds = opts.edgeKinds || ['calls'];
|
|
575
|
+
const reverse = opts.reverse || false;
|
|
576
|
+
|
|
577
|
+
const fromNodes = findMatchingNodes(db, from, {
|
|
578
|
+
noTests,
|
|
579
|
+
file: opts.fromFile,
|
|
580
|
+
kind: opts.kind,
|
|
581
|
+
});
|
|
582
|
+
if (fromNodes.length === 0) {
|
|
583
|
+
db.close();
|
|
584
|
+
return {
|
|
585
|
+
from,
|
|
586
|
+
to,
|
|
587
|
+
found: false,
|
|
588
|
+
error: `No symbol matching "${from}"`,
|
|
589
|
+
fromCandidates: [],
|
|
590
|
+
toCandidates: [],
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const toNodes = findMatchingNodes(db, to, {
|
|
595
|
+
noTests,
|
|
596
|
+
file: opts.toFile,
|
|
597
|
+
kind: opts.kind,
|
|
598
|
+
});
|
|
599
|
+
if (toNodes.length === 0) {
|
|
600
|
+
db.close();
|
|
601
|
+
return {
|
|
602
|
+
from,
|
|
603
|
+
to,
|
|
604
|
+
found: false,
|
|
605
|
+
error: `No symbol matching "${to}"`,
|
|
606
|
+
fromCandidates: fromNodes
|
|
607
|
+
.slice(0, 5)
|
|
608
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
609
|
+
toCandidates: [],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const sourceNode = fromNodes[0];
|
|
614
|
+
const targetNode = toNodes[0];
|
|
615
|
+
|
|
616
|
+
const fromCandidates = fromNodes
|
|
617
|
+
.slice(0, 5)
|
|
618
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
619
|
+
const toCandidates = toNodes
|
|
620
|
+
.slice(0, 5)
|
|
621
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
622
|
+
|
|
623
|
+
// Self-path
|
|
624
|
+
if (sourceNode.id === targetNode.id) {
|
|
625
|
+
db.close();
|
|
626
|
+
return {
|
|
627
|
+
from,
|
|
628
|
+
to,
|
|
629
|
+
fromCandidates,
|
|
630
|
+
toCandidates,
|
|
631
|
+
found: true,
|
|
632
|
+
hops: 0,
|
|
633
|
+
path: [
|
|
634
|
+
{
|
|
635
|
+
name: sourceNode.name,
|
|
636
|
+
kind: sourceNode.kind,
|
|
637
|
+
file: sourceNode.file,
|
|
638
|
+
line: sourceNode.line,
|
|
639
|
+
edgeKind: null,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
alternateCount: 0,
|
|
643
|
+
edgeKinds,
|
|
644
|
+
reverse,
|
|
645
|
+
maxDepth,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Build edge kind filter
|
|
650
|
+
const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
|
|
651
|
+
|
|
652
|
+
// BFS — direction depends on `reverse` flag
|
|
653
|
+
// Forward: source_id → target_id (A calls... calls B)
|
|
654
|
+
// Reverse: target_id → source_id (B is called by... called by A)
|
|
655
|
+
const neighborQuery = reverse
|
|
656
|
+
? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
657
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
658
|
+
WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
|
|
659
|
+
: `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
660
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
661
|
+
WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
|
|
662
|
+
const neighborStmt = db.prepare(neighborQuery);
|
|
663
|
+
|
|
664
|
+
const visited = new Set([sourceNode.id]);
|
|
665
|
+
// parent map: nodeId → { parentId, edgeKind }
|
|
666
|
+
const parent = new Map();
|
|
667
|
+
let queue = [sourceNode.id];
|
|
668
|
+
let found = false;
|
|
669
|
+
let alternateCount = 0;
|
|
670
|
+
let foundDepth = -1;
|
|
671
|
+
|
|
672
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
673
|
+
const nextQueue = [];
|
|
674
|
+
for (const currentId of queue) {
|
|
675
|
+
const neighbors = neighborStmt.all(currentId, ...edgeKinds);
|
|
676
|
+
for (const n of neighbors) {
|
|
677
|
+
if (noTests && isTestFile(n.file)) continue;
|
|
678
|
+
if (n.id === targetNode.id) {
|
|
679
|
+
if (!found) {
|
|
680
|
+
found = true;
|
|
681
|
+
foundDepth = depth;
|
|
682
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
683
|
+
}
|
|
684
|
+
alternateCount++;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (!visited.has(n.id)) {
|
|
688
|
+
visited.add(n.id);
|
|
689
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
690
|
+
nextQueue.push(n.id);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (found) break;
|
|
695
|
+
queue = nextQueue;
|
|
696
|
+
if (queue.length === 0) break;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!found) {
|
|
700
|
+
db.close();
|
|
701
|
+
return {
|
|
702
|
+
from,
|
|
703
|
+
to,
|
|
704
|
+
fromCandidates,
|
|
705
|
+
toCandidates,
|
|
706
|
+
found: false,
|
|
707
|
+
hops: null,
|
|
708
|
+
path: [],
|
|
709
|
+
alternateCount: 0,
|
|
710
|
+
edgeKinds,
|
|
711
|
+
reverse,
|
|
712
|
+
maxDepth,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// alternateCount includes the one we kept; subtract 1 for "alternates"
|
|
717
|
+
alternateCount = Math.max(0, alternateCount - 1);
|
|
718
|
+
|
|
719
|
+
// Reconstruct path from target back to source
|
|
720
|
+
const pathIds = [targetNode.id];
|
|
721
|
+
let cur = targetNode.id;
|
|
722
|
+
while (cur !== sourceNode.id) {
|
|
723
|
+
const p = parent.get(cur);
|
|
724
|
+
pathIds.push(p.parentId);
|
|
725
|
+
cur = p.parentId;
|
|
726
|
+
}
|
|
727
|
+
pathIds.reverse();
|
|
728
|
+
|
|
729
|
+
// Build path with node info
|
|
730
|
+
const nodeCache = new Map();
|
|
731
|
+
const getNode = (id) => {
|
|
732
|
+
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
733
|
+
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
734
|
+
nodeCache.set(id, row);
|
|
735
|
+
return row;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const resultPath = pathIds.map((id, idx) => {
|
|
739
|
+
const node = getNode(id);
|
|
740
|
+
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
741
|
+
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
db.close();
|
|
745
|
+
return {
|
|
746
|
+
from,
|
|
747
|
+
to,
|
|
748
|
+
fromCandidates,
|
|
749
|
+
toCandidates,
|
|
750
|
+
found: true,
|
|
751
|
+
hops: foundDepth,
|
|
752
|
+
path: resultPath,
|
|
753
|
+
alternateCount,
|
|
754
|
+
edgeKinds,
|
|
755
|
+
reverse,
|
|
756
|
+
maxDepth,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function symbolPath(from, to, customDbPath, opts = {}) {
|
|
761
|
+
const data = pathData(from, to, customDbPath, opts);
|
|
762
|
+
if (opts.json) {
|
|
763
|
+
console.log(JSON.stringify(data, null, 2));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (data.error) {
|
|
768
|
+
console.log(data.error);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!data.found) {
|
|
773
|
+
const dir = data.reverse ? 'reverse ' : '';
|
|
774
|
+
console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
|
|
775
|
+
if (data.fromCandidates.length > 1) {
|
|
776
|
+
console.log(
|
|
777
|
+
`\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if (data.toCandidates.length > 1) {
|
|
781
|
+
console.log(
|
|
782
|
+
` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (data.hops === 0) {
|
|
789
|
+
console.log(`\n"${from}" and "${to}" resolve to the same symbol (0 hops):`);
|
|
790
|
+
const n = data.path[0];
|
|
791
|
+
console.log(` ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}\n`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const dir = data.reverse ? ' (reverse)' : '';
|
|
796
|
+
console.log(
|
|
797
|
+
`\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
|
|
798
|
+
);
|
|
799
|
+
for (let i = 0; i < data.path.length; i++) {
|
|
800
|
+
const n = data.path[i];
|
|
801
|
+
const indent = ' '.repeat(i + 1);
|
|
802
|
+
if (i === 0) {
|
|
803
|
+
console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
|
|
804
|
+
} else {
|
|
805
|
+
console.log(
|
|
806
|
+
`${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (data.alternateCount > 0) {
|
|
812
|
+
console.log(
|
|
813
|
+
`\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
console.log();
|
|
817
|
+
}
|
|
818
|
+
|
|
568
819
|
/**
|
|
569
820
|
* Fix #2: Shell injection vulnerability.
|
|
570
821
|
* Uses execFileSync instead of execSync to prevent shell interpretation of user input.
|
|
@@ -904,7 +1155,8 @@ export function listFunctionsData(customDbPath, opts = {}) {
|
|
|
904
1155
|
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
905
1156
|
|
|
906
1157
|
db.close();
|
|
907
|
-
|
|
1158
|
+
const base = { count: rows.length, functions: rows };
|
|
1159
|
+
return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
|
|
908
1160
|
}
|
|
909
1161
|
|
|
910
1162
|
export function statsData(customDbPath, opts = {}) {
|
|
@@ -1114,6 +1366,32 @@ export function statsData(customDbPath, opts = {}) {
|
|
|
1114
1366
|
const roles = {};
|
|
1115
1367
|
for (const r of roleRows) roles[r.role] = r.c;
|
|
1116
1368
|
|
|
1369
|
+
// Complexity summary
|
|
1370
|
+
let complexity = null;
|
|
1371
|
+
try {
|
|
1372
|
+
const cRows = db
|
|
1373
|
+
.prepare(
|
|
1374
|
+
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
1375
|
+
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
1376
|
+
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
1377
|
+
)
|
|
1378
|
+
.all();
|
|
1379
|
+
if (cRows.length > 0) {
|
|
1380
|
+
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
1381
|
+
complexity = {
|
|
1382
|
+
analyzed: cRows.length,
|
|
1383
|
+
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
1384
|
+
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
1385
|
+
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
1386
|
+
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
1387
|
+
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
1388
|
+
minMI: +Math.min(...miValues).toFixed(1),
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
/* table may not exist in older DBs */
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1117
1395
|
db.close();
|
|
1118
1396
|
return {
|
|
1119
1397
|
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
@@ -1124,11 +1402,21 @@ export function statsData(customDbPath, opts = {}) {
|
|
|
1124
1402
|
embeddings,
|
|
1125
1403
|
quality,
|
|
1126
1404
|
roles,
|
|
1405
|
+
complexity,
|
|
1127
1406
|
};
|
|
1128
1407
|
}
|
|
1129
1408
|
|
|
1130
|
-
export function stats(customDbPath, opts = {}) {
|
|
1409
|
+
export async function stats(customDbPath, opts = {}) {
|
|
1131
1410
|
const data = statsData(customDbPath, { noTests: opts.noTests });
|
|
1411
|
+
|
|
1412
|
+
// Community detection summary (async import for lazy-loading)
|
|
1413
|
+
try {
|
|
1414
|
+
const { communitySummaryForStats } = await import('./communities.js');
|
|
1415
|
+
data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
|
|
1416
|
+
} catch {
|
|
1417
|
+
/* graphology may not be available */
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1132
1420
|
if (opts.json) {
|
|
1133
1421
|
console.log(JSON.stringify(data, null, 2));
|
|
1134
1422
|
return;
|
|
@@ -1236,13 +1524,39 @@ export function stats(customDbPath, opts = {}) {
|
|
|
1236
1524
|
}
|
|
1237
1525
|
}
|
|
1238
1526
|
|
|
1527
|
+
// Complexity
|
|
1528
|
+
if (data.complexity) {
|
|
1529
|
+
const cx = data.complexity;
|
|
1530
|
+
const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
|
|
1531
|
+
console.log(
|
|
1532
|
+
`\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Communities
|
|
1537
|
+
if (data.communities) {
|
|
1538
|
+
const cm = data.communities;
|
|
1539
|
+
console.log(
|
|
1540
|
+
`\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1239
1544
|
console.log();
|
|
1240
1545
|
}
|
|
1241
1546
|
|
|
1242
1547
|
// ─── Human-readable output (original formatting) ───────────────────────
|
|
1243
1548
|
|
|
1244
1549
|
export function queryName(name, customDbPath, opts = {}) {
|
|
1245
|
-
const data = queryNameData(name, customDbPath, {
|
|
1550
|
+
const data = queryNameData(name, customDbPath, {
|
|
1551
|
+
noTests: opts.noTests,
|
|
1552
|
+
limit: opts.limit,
|
|
1553
|
+
offset: opts.offset,
|
|
1554
|
+
});
|
|
1555
|
+
if (opts.ndjson) {
|
|
1556
|
+
if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
|
|
1557
|
+
for (const r of data.results) console.log(JSON.stringify(r));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1246
1560
|
if (opts.json) {
|
|
1247
1561
|
console.log(JSON.stringify(data, null, 2));
|
|
1248
1562
|
return;
|
|
@@ -1698,6 +2012,27 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1698
2012
|
});
|
|
1699
2013
|
}
|
|
1700
2014
|
|
|
2015
|
+
// Complexity metrics
|
|
2016
|
+
let complexityMetrics = null;
|
|
2017
|
+
try {
|
|
2018
|
+
const cRow = db
|
|
2019
|
+
.prepare(
|
|
2020
|
+
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
2021
|
+
)
|
|
2022
|
+
.get(node.id);
|
|
2023
|
+
if (cRow) {
|
|
2024
|
+
complexityMetrics = {
|
|
2025
|
+
cognitive: cRow.cognitive,
|
|
2026
|
+
cyclomatic: cRow.cyclomatic,
|
|
2027
|
+
maxNesting: cRow.max_nesting,
|
|
2028
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
2029
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
} catch {
|
|
2033
|
+
/* table may not exist */
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1701
2036
|
return {
|
|
1702
2037
|
name: node.name,
|
|
1703
2038
|
kind: node.kind,
|
|
@@ -1707,6 +2042,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1707
2042
|
endLine: node.end_line || null,
|
|
1708
2043
|
source,
|
|
1709
2044
|
signature,
|
|
2045
|
+
complexity: complexityMetrics,
|
|
1710
2046
|
callees,
|
|
1711
2047
|
callers,
|
|
1712
2048
|
relatedTests,
|
|
@@ -1741,6 +2077,17 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
1741
2077
|
console.log();
|
|
1742
2078
|
}
|
|
1743
2079
|
|
|
2080
|
+
// Complexity
|
|
2081
|
+
if (r.complexity) {
|
|
2082
|
+
const cx = r.complexity;
|
|
2083
|
+
const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
|
|
2084
|
+
console.log('## Complexity');
|
|
2085
|
+
console.log(
|
|
2086
|
+
` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
|
|
2087
|
+
);
|
|
2088
|
+
console.log();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1744
2091
|
// Source
|
|
1745
2092
|
if (r.source) {
|
|
1746
2093
|
console.log('## Source');
|
|
@@ -1959,6 +2306,27 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
1959
2306
|
.filter((r) => isTestFile(r.file))
|
|
1960
2307
|
.map((r) => ({ file: r.file }));
|
|
1961
2308
|
|
|
2309
|
+
// Complexity metrics
|
|
2310
|
+
let complexityMetrics = null;
|
|
2311
|
+
try {
|
|
2312
|
+
const cRow = db
|
|
2313
|
+
.prepare(
|
|
2314
|
+
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
2315
|
+
)
|
|
2316
|
+
.get(node.id);
|
|
2317
|
+
if (cRow) {
|
|
2318
|
+
complexityMetrics = {
|
|
2319
|
+
cognitive: cRow.cognitive,
|
|
2320
|
+
cyclomatic: cRow.cyclomatic,
|
|
2321
|
+
maxNesting: cRow.max_nesting,
|
|
2322
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
2323
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
} catch {
|
|
2327
|
+
/* table may not exist */
|
|
2328
|
+
}
|
|
2329
|
+
|
|
1962
2330
|
return {
|
|
1963
2331
|
name: node.name,
|
|
1964
2332
|
kind: node.kind,
|
|
@@ -1969,6 +2337,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
1969
2337
|
lineCount,
|
|
1970
2338
|
summary,
|
|
1971
2339
|
signature,
|
|
2340
|
+
complexity: complexityMetrics,
|
|
1972
2341
|
callees,
|
|
1973
2342
|
callers,
|
|
1974
2343
|
relatedTests,
|
|
@@ -2118,6 +2487,14 @@ export function explain(target, customDbPath, opts = {}) {
|
|
|
2118
2487
|
if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
|
|
2119
2488
|
}
|
|
2120
2489
|
|
|
2490
|
+
if (r.complexity) {
|
|
2491
|
+
const cx = r.complexity;
|
|
2492
|
+
const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
|
|
2493
|
+
console.log(
|
|
2494
|
+
`${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2121
2498
|
if (r.callees.length > 0) {
|
|
2122
2499
|
console.log(`\n${indent} Calls (${r.callees.length}):`);
|
|
2123
2500
|
for (const c of r.callees) {
|
|
@@ -2261,11 +2638,17 @@ export function whereData(target, customDbPath, opts = {}) {
|
|
|
2261
2638
|
const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
|
|
2262
2639
|
|
|
2263
2640
|
db.close();
|
|
2264
|
-
|
|
2641
|
+
const base = { target, mode: fileMode ? 'file' : 'symbol', results };
|
|
2642
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2265
2643
|
}
|
|
2266
2644
|
|
|
2267
2645
|
export function where(target, customDbPath, opts = {}) {
|
|
2268
2646
|
const data = whereData(target, customDbPath, opts);
|
|
2647
|
+
if (opts.ndjson) {
|
|
2648
|
+
if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
|
|
2649
|
+
for (const r of data.results) console.log(JSON.stringify(r));
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2269
2652
|
if (opts.json) {
|
|
2270
2653
|
console.log(JSON.stringify(data, null, 2));
|
|
2271
2654
|
return;
|
|
@@ -2347,11 +2730,17 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2347
2730
|
}
|
|
2348
2731
|
|
|
2349
2732
|
db.close();
|
|
2350
|
-
|
|
2733
|
+
const base = { count: rows.length, summary, symbols: rows };
|
|
2734
|
+
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
2351
2735
|
}
|
|
2352
2736
|
|
|
2353
2737
|
export function roles(customDbPath, opts = {}) {
|
|
2354
2738
|
const data = rolesData(customDbPath, opts);
|
|
2739
|
+
if (opts.ndjson) {
|
|
2740
|
+
if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
|
|
2741
|
+
for (const s of data.symbols) console.log(JSON.stringify(s));
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2355
2744
|
if (opts.json) {
|
|
2356
2745
|
console.log(JSON.stringify(data, null, 2));
|
|
2357
2746
|
return;
|
package/src/registry.js
CHANGED
|
@@ -135,11 +135,14 @@ export function resolveRepoDbPath(name, registryPath = REGISTRY_PATH) {
|
|
|
135
135
|
* Remove registry entries whose repo directory no longer exists on disk,
|
|
136
136
|
* or that haven't been accessed within `ttlDays` days.
|
|
137
137
|
* Returns an array of `{ name, path, reason }` for each pruned entry.
|
|
138
|
+
*
|
|
139
|
+
* When `dryRun` is true, entries are identified but not removed from disk.
|
|
138
140
|
*/
|
|
139
141
|
export function pruneRegistry(
|
|
140
142
|
registryPath = REGISTRY_PATH,
|
|
141
143
|
ttlDays = DEFAULT_TTL_DAYS,
|
|
142
144
|
excludeNames = [],
|
|
145
|
+
dryRun = false,
|
|
143
146
|
) {
|
|
144
147
|
const registry = loadRegistry(registryPath);
|
|
145
148
|
const pruned = [];
|
|
@@ -152,17 +155,17 @@ export function pruneRegistry(
|
|
|
152
155
|
if (excludeSet.has(name)) continue;
|
|
153
156
|
if (!fs.existsSync(entry.path)) {
|
|
154
157
|
pruned.push({ name, path: entry.path, reason: 'missing' });
|
|
155
|
-
delete registry.repos[name];
|
|
158
|
+
if (!dryRun) delete registry.repos[name];
|
|
156
159
|
continue;
|
|
157
160
|
}
|
|
158
161
|
const lastAccess = Date.parse(entry.lastAccessedAt || entry.addedAt);
|
|
159
162
|
if (lastAccess < cutoff) {
|
|
160
163
|
pruned.push({ name, path: entry.path, reason: 'expired' });
|
|
161
|
-
delete registry.repos[name];
|
|
164
|
+
if (!dryRun) delete registry.repos[name];
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
if (pruned.length > 0) {
|
|
168
|
+
if (!dryRun && pruned.length > 0) {
|
|
166
169
|
saveRegistry(registry, registryPath);
|
|
167
170
|
}
|
|
168
171
|
|