@optave/codegraph 2.2.1 → 2.2.3-dev.44e8146
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 +117 -126
- package/package.json +5 -5
- package/src/builder.js +96 -18
- package/src/cli.js +85 -25
- package/src/config.js +1 -0
- package/src/embedder.js +196 -15
- package/src/export.js +16 -7
- package/src/extractors/javascript.js +6 -8
- package/src/index.js +3 -0
- package/src/mcp.js +21 -7
- package/src/queries.js +222 -18
- package/src/structure.js +2 -1
- package/src/watcher.js +2 -1
package/src/export.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isTestFile } from './queries.js';
|
|
3
3
|
|
|
4
|
+
const DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Export the dependency graph in DOT (Graphviz) format.
|
|
6
8
|
*/
|
|
7
9
|
export function exportDOT(db, opts = {}) {
|
|
8
10
|
const fileLevel = opts.fileLevel !== false;
|
|
9
11
|
const noTests = opts.noTests || false;
|
|
12
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
10
13
|
const lines = [
|
|
11
14
|
'digraph codegraph {',
|
|
12
15
|
' rankdir=LR;',
|
|
@@ -23,8 +26,9 @@ export function exportDOT(db, opts = {}) {
|
|
|
23
26
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
24
27
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
25
28
|
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
29
|
+
AND e.confidence >= ?
|
|
26
30
|
`)
|
|
27
|
-
.all();
|
|
31
|
+
.all(minConf);
|
|
28
32
|
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
29
33
|
|
|
30
34
|
// Try to use directory nodes from DB (built by structure analysis)
|
|
@@ -102,8 +106,9 @@ export function exportDOT(db, opts = {}) {
|
|
|
102
106
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
103
107
|
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
104
108
|
AND e.kind = 'calls'
|
|
109
|
+
AND e.confidence >= ?
|
|
105
110
|
`)
|
|
106
|
-
.all();
|
|
111
|
+
.all(minConf);
|
|
107
112
|
if (noTests)
|
|
108
113
|
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
109
114
|
|
|
@@ -126,6 +131,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
126
131
|
export function exportMermaid(db, opts = {}) {
|
|
127
132
|
const fileLevel = opts.fileLevel !== false;
|
|
128
133
|
const noTests = opts.noTests || false;
|
|
134
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
129
135
|
const lines = ['graph LR'];
|
|
130
136
|
|
|
131
137
|
if (fileLevel) {
|
|
@@ -136,8 +142,9 @@ export function exportMermaid(db, opts = {}) {
|
|
|
136
142
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
137
143
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
138
144
|
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
145
|
+
AND e.confidence >= ?
|
|
139
146
|
`)
|
|
140
|
-
.all();
|
|
147
|
+
.all(minConf);
|
|
141
148
|
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
142
149
|
|
|
143
150
|
for (const { source, target } of edges) {
|
|
@@ -155,8 +162,9 @@ export function exportMermaid(db, opts = {}) {
|
|
|
155
162
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
156
163
|
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
157
164
|
AND e.kind = 'calls'
|
|
165
|
+
AND e.confidence >= ?
|
|
158
166
|
`)
|
|
159
|
-
.all();
|
|
167
|
+
.all(minConf);
|
|
160
168
|
if (noTests)
|
|
161
169
|
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
162
170
|
|
|
@@ -175,6 +183,7 @@ export function exportMermaid(db, opts = {}) {
|
|
|
175
183
|
*/
|
|
176
184
|
export function exportJSON(db, opts = {}) {
|
|
177
185
|
const noTests = opts.noTests || false;
|
|
186
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
178
187
|
|
|
179
188
|
let nodes = db
|
|
180
189
|
.prepare(`
|
|
@@ -185,13 +194,13 @@ export function exportJSON(db, opts = {}) {
|
|
|
185
194
|
|
|
186
195
|
let edges = db
|
|
187
196
|
.prepare(`
|
|
188
|
-
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind
|
|
197
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind, e.confidence
|
|
189
198
|
FROM edges e
|
|
190
199
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
191
200
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
192
|
-
WHERE n1.file != n2.file
|
|
201
|
+
WHERE n1.file != n2.file AND e.confidence >= ?
|
|
193
202
|
`)
|
|
194
|
-
.all();
|
|
203
|
+
.all(minConf);
|
|
195
204
|
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
196
205
|
|
|
197
206
|
return { nodes, edges };
|
|
@@ -139,9 +139,11 @@ export function extractSymbols(tree, _filePath) {
|
|
|
139
139
|
if (callInfo) {
|
|
140
140
|
calls.push(callInfo);
|
|
141
141
|
}
|
|
142
|
+
if (fn.type === 'member_expression') {
|
|
143
|
+
const cbDef = extractCallbackDefinition(node, fn);
|
|
144
|
+
if (cbDef) definitions.push(cbDef);
|
|
145
|
+
}
|
|
142
146
|
}
|
|
143
|
-
const cbDef = extractCallbackDefinition(node);
|
|
144
|
-
if (cbDef) definitions.push(cbDef);
|
|
145
147
|
break;
|
|
146
148
|
}
|
|
147
149
|
|
|
@@ -320,10 +322,6 @@ function extractReceiverName(objNode) {
|
|
|
320
322
|
if (objNode.type === 'identifier') return objNode.text;
|
|
321
323
|
if (objNode.type === 'this') return 'this';
|
|
322
324
|
if (objNode.type === 'super') return 'super';
|
|
323
|
-
if (objNode.type === 'member_expression') {
|
|
324
|
-
const prop = objNode.childForFieldName('property');
|
|
325
|
-
if (prop) return objNode.text;
|
|
326
|
-
}
|
|
327
325
|
return objNode.text;
|
|
328
326
|
}
|
|
329
327
|
|
|
@@ -432,8 +430,8 @@ const EXPRESS_METHODS = new Set([
|
|
|
432
430
|
]);
|
|
433
431
|
const EVENT_METHODS = new Set(['on', 'once', 'addEventListener', 'addListener']);
|
|
434
432
|
|
|
435
|
-
function extractCallbackDefinition(callNode) {
|
|
436
|
-
|
|
433
|
+
function extractCallbackDefinition(callNode, fn) {
|
|
434
|
+
if (!fn) fn = callNode.childForFieldName('function');
|
|
437
435
|
if (!fn || fn.type !== 'member_expression') return null;
|
|
438
436
|
|
|
439
437
|
const prop = fn.childForFieldName('property');
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,9 @@ export {
|
|
|
21
21
|
buildEmbeddings,
|
|
22
22
|
cosineSim,
|
|
23
23
|
DEFAULT_MODEL,
|
|
24
|
+
EMBEDDING_STRATEGIES,
|
|
24
25
|
embed,
|
|
26
|
+
estimateTokens,
|
|
25
27
|
MODELS,
|
|
26
28
|
multiSearchData,
|
|
27
29
|
search,
|
|
@@ -41,6 +43,7 @@ export {
|
|
|
41
43
|
ALL_SYMBOL_KINDS,
|
|
42
44
|
contextData,
|
|
43
45
|
diffImpactData,
|
|
46
|
+
diffImpactMermaid,
|
|
44
47
|
explainData,
|
|
45
48
|
FALSE_POSITIVE_CALLER_THRESHOLD,
|
|
46
49
|
FALSE_POSITIVE_NAMES,
|
package/src/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { findCycles } from './cycles.js';
|
|
10
10
|
import { findDbPath } from './db.js';
|
|
11
|
-
import { ALL_SYMBOL_KINDS } from './queries.js';
|
|
11
|
+
import { ALL_SYMBOL_KINDS, diffImpactMermaid } from './queries.js';
|
|
12
12
|
|
|
13
13
|
const REPO_PROP = {
|
|
14
14
|
repo: {
|
|
@@ -201,6 +201,11 @@ const BASE_TOOLS = [
|
|
|
201
201
|
ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
|
|
202
202
|
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
203
203
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
204
|
+
format: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
enum: ['json', 'mermaid'],
|
|
207
|
+
description: 'Output format (default: json)',
|
|
208
|
+
},
|
|
204
209
|
},
|
|
205
210
|
},
|
|
206
211
|
},
|
|
@@ -467,12 +472,21 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
467
472
|
});
|
|
468
473
|
break;
|
|
469
474
|
case 'diff_impact':
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
475
|
+
if (args.format === 'mermaid') {
|
|
476
|
+
result = diffImpactMermaid(dbPath, {
|
|
477
|
+
staged: args.staged,
|
|
478
|
+
ref: args.ref,
|
|
479
|
+
depth: args.depth,
|
|
480
|
+
noTests: args.no_tests,
|
|
481
|
+
});
|
|
482
|
+
} else {
|
|
483
|
+
result = diffImpactData(dbPath, {
|
|
484
|
+
staged: args.staged,
|
|
485
|
+
ref: args.ref,
|
|
486
|
+
depth: args.depth,
|
|
487
|
+
noTests: args.no_tests,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
476
490
|
break;
|
|
477
491
|
case 'semantic_search': {
|
|
478
492
|
const { searchData } = await import('./embedder.js');
|
package/src/queries.js
CHANGED
|
@@ -334,6 +334,7 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
|
334
334
|
dir: path.dirname(n.file) || '.',
|
|
335
335
|
inEdges: n.in_edges,
|
|
336
336
|
outEdges: n.out_edges,
|
|
337
|
+
coupling: n.in_edges + n.out_edges,
|
|
337
338
|
}));
|
|
338
339
|
|
|
339
340
|
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
@@ -608,16 +609,34 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
608
609
|
|
|
609
610
|
if (!diffOutput.trim()) {
|
|
610
611
|
db.close();
|
|
611
|
-
return {
|
|
612
|
+
return {
|
|
613
|
+
changedFiles: 0,
|
|
614
|
+
newFiles: [],
|
|
615
|
+
affectedFunctions: [],
|
|
616
|
+
affectedFiles: [],
|
|
617
|
+
summary: null,
|
|
618
|
+
};
|
|
612
619
|
}
|
|
613
620
|
|
|
614
621
|
const changedRanges = new Map();
|
|
622
|
+
const newFiles = new Set();
|
|
615
623
|
let currentFile = null;
|
|
624
|
+
let prevIsDevNull = false;
|
|
616
625
|
for (const line of diffOutput.split('\n')) {
|
|
626
|
+
if (line.startsWith('--- /dev/null')) {
|
|
627
|
+
prevIsDevNull = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (line.startsWith('--- ')) {
|
|
631
|
+
prevIsDevNull = false;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
617
634
|
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
618
635
|
if (fileMatch) {
|
|
619
636
|
currentFile = fileMatch[1];
|
|
620
637
|
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
638
|
+
if (prevIsDevNull) newFiles.add(currentFile);
|
|
639
|
+
prevIsDevNull = false;
|
|
621
640
|
continue;
|
|
622
641
|
}
|
|
623
642
|
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
@@ -630,7 +649,13 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
630
649
|
|
|
631
650
|
if (changedRanges.size === 0) {
|
|
632
651
|
db.close();
|
|
633
|
-
return {
|
|
652
|
+
return {
|
|
653
|
+
changedFiles: 0,
|
|
654
|
+
newFiles: [],
|
|
655
|
+
affectedFunctions: [],
|
|
656
|
+
affectedFiles: [],
|
|
657
|
+
summary: null,
|
|
658
|
+
};
|
|
634
659
|
}
|
|
635
660
|
|
|
636
661
|
const affectedFunctions = [];
|
|
@@ -658,6 +683,10 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
658
683
|
const visited = new Set([fn.id]);
|
|
659
684
|
let frontier = [fn.id];
|
|
660
685
|
let totalCallers = 0;
|
|
686
|
+
const levels = {};
|
|
687
|
+
const edges = [];
|
|
688
|
+
const idToKey = new Map();
|
|
689
|
+
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
661
690
|
for (let d = 1; d <= maxDepth; d++) {
|
|
662
691
|
const nextFrontier = [];
|
|
663
692
|
for (const fid of frontier) {
|
|
@@ -673,6 +702,11 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
673
702
|
visited.add(c.id);
|
|
674
703
|
nextFrontier.push(c.id);
|
|
675
704
|
allAffected.add(`${c.file}:${c.name}`);
|
|
705
|
+
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
706
|
+
idToKey.set(c.id, callerKey);
|
|
707
|
+
if (!levels[d]) levels[d] = [];
|
|
708
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
709
|
+
edges.push({ from: idToKey.get(fid), to: callerKey });
|
|
676
710
|
totalCallers++;
|
|
677
711
|
}
|
|
678
712
|
}
|
|
@@ -686,6 +720,8 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
686
720
|
file: fn.file,
|
|
687
721
|
line: fn.line,
|
|
688
722
|
transitiveCallers: totalCallers,
|
|
723
|
+
levels,
|
|
724
|
+
edges,
|
|
689
725
|
};
|
|
690
726
|
});
|
|
691
727
|
|
|
@@ -695,6 +731,7 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
695
731
|
db.close();
|
|
696
732
|
return {
|
|
697
733
|
changedFiles: changedRanges.size,
|
|
734
|
+
newFiles: [...newFiles],
|
|
698
735
|
affectedFunctions: functionResults,
|
|
699
736
|
affectedFiles: [...affectedFiles],
|
|
700
737
|
summary: {
|
|
@@ -705,6 +742,120 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
705
742
|
};
|
|
706
743
|
}
|
|
707
744
|
|
|
745
|
+
export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
746
|
+
const data = diffImpactData(customDbPath, opts);
|
|
747
|
+
if (data.error) return data.error;
|
|
748
|
+
if (data.changedFiles === 0 || data.affectedFunctions.length === 0) {
|
|
749
|
+
return 'flowchart TB\n none["No impacted functions detected"]';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const newFileSet = new Set(data.newFiles || []);
|
|
753
|
+
const lines = ['flowchart TB'];
|
|
754
|
+
|
|
755
|
+
// Assign stable Mermaid node IDs
|
|
756
|
+
let nodeCounter = 0;
|
|
757
|
+
const nodeIdMap = new Map();
|
|
758
|
+
const nodeLabels = new Map();
|
|
759
|
+
function nodeId(key, label) {
|
|
760
|
+
if (!nodeIdMap.has(key)) {
|
|
761
|
+
nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
762
|
+
if (label) nodeLabels.set(key, label);
|
|
763
|
+
}
|
|
764
|
+
return nodeIdMap.get(key);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Register all nodes (changed functions + their callers)
|
|
768
|
+
for (const fn of data.affectedFunctions) {
|
|
769
|
+
nodeId(`${fn.file}::${fn.name}:${fn.line}`, fn.name);
|
|
770
|
+
for (const callers of Object.values(fn.levels || {})) {
|
|
771
|
+
for (const c of callers) {
|
|
772
|
+
nodeId(`${c.file}::${c.name}:${c.line}`, c.name);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Collect all edges and determine blast radius
|
|
778
|
+
const allEdges = new Set();
|
|
779
|
+
const edgeFromNodes = new Set();
|
|
780
|
+
const edgeToNodes = new Set();
|
|
781
|
+
const changedKeys = new Set();
|
|
782
|
+
|
|
783
|
+
for (const fn of data.affectedFunctions) {
|
|
784
|
+
changedKeys.add(`${fn.file}::${fn.name}:${fn.line}`);
|
|
785
|
+
for (const edge of fn.edges || []) {
|
|
786
|
+
const edgeKey = `${edge.from}|${edge.to}`;
|
|
787
|
+
if (!allEdges.has(edgeKey)) {
|
|
788
|
+
allEdges.add(edgeKey);
|
|
789
|
+
edgeFromNodes.add(edge.from);
|
|
790
|
+
edgeToNodes.add(edge.to);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Blast radius: caller nodes that are never a source (leaf nodes of the impact tree)
|
|
796
|
+
const blastRadiusKeys = new Set();
|
|
797
|
+
for (const key of edgeToNodes) {
|
|
798
|
+
if (!edgeFromNodes.has(key) && !changedKeys.has(key)) {
|
|
799
|
+
blastRadiusKeys.add(key);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Intermediate callers: not changed, not blast radius
|
|
804
|
+
const intermediateKeys = new Set();
|
|
805
|
+
for (const key of edgeToNodes) {
|
|
806
|
+
if (!changedKeys.has(key) && !blastRadiusKeys.has(key)) {
|
|
807
|
+
intermediateKeys.add(key);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Group changed functions by file
|
|
812
|
+
const fileGroups = new Map();
|
|
813
|
+
for (const fn of data.affectedFunctions) {
|
|
814
|
+
if (!fileGroups.has(fn.file)) fileGroups.set(fn.file, []);
|
|
815
|
+
fileGroups.get(fn.file).push(fn);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Emit changed-file subgraphs
|
|
819
|
+
let sgCounter = 0;
|
|
820
|
+
for (const [file, fns] of fileGroups) {
|
|
821
|
+
const isNew = newFileSet.has(file);
|
|
822
|
+
const tag = isNew ? 'new' : 'modified';
|
|
823
|
+
const sgId = `sg${sgCounter++}`;
|
|
824
|
+
lines.push(` subgraph ${sgId}["${file} **(${tag})**"]`);
|
|
825
|
+
for (const fn of fns) {
|
|
826
|
+
const key = `${fn.file}::${fn.name}:${fn.line}`;
|
|
827
|
+
lines.push(` ${nodeIdMap.get(key)}["${fn.name}"]`);
|
|
828
|
+
}
|
|
829
|
+
lines.push(' end');
|
|
830
|
+
const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800';
|
|
831
|
+
lines.push(` style ${sgId} ${style}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Emit intermediate caller nodes (outside subgraphs)
|
|
835
|
+
for (const key of intermediateKeys) {
|
|
836
|
+
lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Emit blast radius subgraph
|
|
840
|
+
if (blastRadiusKeys.size > 0) {
|
|
841
|
+
const sgId = `sg${sgCounter++}`;
|
|
842
|
+
lines.push(` subgraph ${sgId}["Callers **(blast radius)**"]`);
|
|
843
|
+
for (const key of blastRadiusKeys) {
|
|
844
|
+
lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
|
|
845
|
+
}
|
|
846
|
+
lines.push(' end');
|
|
847
|
+
lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Emit edges (impact flows from changed fn toward callers)
|
|
851
|
+
for (const edgeKey of allEdges) {
|
|
852
|
+
const [from, to] = edgeKey.split('|');
|
|
853
|
+
lines.push(` ${nodeIdMap.get(from)} --> ${nodeIdMap.get(to)}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return lines.join('\n');
|
|
857
|
+
}
|
|
858
|
+
|
|
708
859
|
export function listFunctionsData(customDbPath, opts = {}) {
|
|
709
860
|
const db = openReadonlyOrFail(customDbPath);
|
|
710
861
|
const noTests = opts.noTests || false;
|
|
@@ -1113,10 +1264,10 @@ export function moduleMap(customDbPath, limit = 20, opts = {}) {
|
|
|
1113
1264
|
for (const [dir, files] of [...dirs].sort()) {
|
|
1114
1265
|
console.log(` [${dir}/]`);
|
|
1115
1266
|
for (const f of files) {
|
|
1116
|
-
const
|
|
1117
|
-
const bar = '#'.repeat(Math.min(
|
|
1267
|
+
const coupling = f.inEdges + f.outEdges;
|
|
1268
|
+
const bar = '#'.repeat(Math.min(coupling, 40));
|
|
1118
1269
|
console.log(
|
|
1119
|
-
` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} ${bar}`,
|
|
1270
|
+
` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} =${String(coupling).padStart(3)} ${bar}`,
|
|
1120
1271
|
);
|
|
1121
1272
|
}
|
|
1122
1273
|
}
|
|
@@ -1770,6 +1921,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
1770
1921
|
export function explainData(target, customDbPath, opts = {}) {
|
|
1771
1922
|
const db = openReadonlyOrFail(customDbPath);
|
|
1772
1923
|
const noTests = opts.noTests || false;
|
|
1924
|
+
const depth = opts.depth || 0;
|
|
1773
1925
|
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
1774
1926
|
|
|
1775
1927
|
const dbPath = findDbPath(customDbPath);
|
|
@@ -1799,6 +1951,37 @@ export function explainData(target, customDbPath, opts = {}) {
|
|
|
1799
1951
|
? explainFileImpl(db, target, getFileLines)
|
|
1800
1952
|
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
1801
1953
|
|
|
1954
|
+
// Recursive dependency explanation for function targets
|
|
1955
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
1956
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
1957
|
+
|
|
1958
|
+
function explainCallees(parentResults, currentDepth) {
|
|
1959
|
+
if (currentDepth <= 0) return;
|
|
1960
|
+
for (const r of parentResults) {
|
|
1961
|
+
const newCallees = [];
|
|
1962
|
+
for (const callee of r.callees) {
|
|
1963
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
1964
|
+
if (visited.has(key)) continue;
|
|
1965
|
+
visited.add(key);
|
|
1966
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
1967
|
+
const exact = calleeResults.find(
|
|
1968
|
+
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
1969
|
+
);
|
|
1970
|
+
if (exact) {
|
|
1971
|
+
exact._depth = (r._depth || 0) + 1;
|
|
1972
|
+
newCallees.push(exact);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (newCallees.length > 0) {
|
|
1976
|
+
r.depDetails = newCallees;
|
|
1977
|
+
explainCallees(newCallees, currentDepth - 1);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
explainCallees(results, depth);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1802
1985
|
db.close();
|
|
1803
1986
|
return { target, kind, results };
|
|
1804
1987
|
}
|
|
@@ -1858,46 +2041,63 @@ export function explain(target, customDbPath, opts = {}) {
|
|
|
1858
2041
|
console.log();
|
|
1859
2042
|
}
|
|
1860
2043
|
} else {
|
|
1861
|
-
|
|
2044
|
+
function printFunctionExplain(r, indent = '') {
|
|
1862
2045
|
const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
|
|
1863
2046
|
const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
|
|
1864
2047
|
const summaryPart = r.summary ? ` | ${r.summary}` : '';
|
|
1865
|
-
|
|
2048
|
+
const depthLevel = r._depth || 0;
|
|
2049
|
+
const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
|
|
2050
|
+
console.log(`\n${indent}${heading} ${r.name} (${r.kind}) ${r.file}:${lineRange}`);
|
|
1866
2051
|
if (lineInfo || r.summary) {
|
|
1867
|
-
console.log(
|
|
2052
|
+
console.log(`${indent} ${lineInfo}${summaryPart}`);
|
|
1868
2053
|
}
|
|
1869
2054
|
if (r.signature) {
|
|
1870
|
-
if (r.signature.params != null)
|
|
1871
|
-
|
|
2055
|
+
if (r.signature.params != null)
|
|
2056
|
+
console.log(`${indent} Parameters: (${r.signature.params})`);
|
|
2057
|
+
if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
|
|
1872
2058
|
}
|
|
1873
2059
|
|
|
1874
2060
|
if (r.callees.length > 0) {
|
|
1875
|
-
console.log(`\n
|
|
2061
|
+
console.log(`\n${indent} Calls (${r.callees.length}):`);
|
|
1876
2062
|
for (const c of r.callees) {
|
|
1877
|
-
console.log(
|
|
2063
|
+
console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
1878
2064
|
}
|
|
1879
2065
|
}
|
|
1880
2066
|
|
|
1881
2067
|
if (r.callers.length > 0) {
|
|
1882
|
-
console.log(`\n
|
|
2068
|
+
console.log(`\n${indent} Called by (${r.callers.length}):`);
|
|
1883
2069
|
for (const c of r.callers) {
|
|
1884
|
-
console.log(
|
|
2070
|
+
console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
1885
2071
|
}
|
|
1886
2072
|
}
|
|
1887
2073
|
|
|
1888
2074
|
if (r.relatedTests.length > 0) {
|
|
1889
2075
|
const label = r.relatedTests.length === 1 ? 'file' : 'files';
|
|
1890
|
-
console.log(`\n
|
|
2076
|
+
console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
|
|
1891
2077
|
for (const t of r.relatedTests) {
|
|
1892
|
-
console.log(
|
|
2078
|
+
console.log(`${indent} ${t.file}`);
|
|
1893
2079
|
}
|
|
1894
2080
|
}
|
|
1895
2081
|
|
|
1896
2082
|
if (r.callees.length === 0 && r.callers.length === 0) {
|
|
1897
|
-
console.log(
|
|
2083
|
+
console.log(
|
|
2084
|
+
`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`,
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Render recursive dependency details
|
|
2089
|
+
if (r.depDetails && r.depDetails.length > 0) {
|
|
2090
|
+
console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
|
|
2091
|
+
for (const dep of r.depDetails) {
|
|
2092
|
+
printFunctionExplain(dep, `${indent} `);
|
|
2093
|
+
}
|
|
1898
2094
|
}
|
|
1899
2095
|
console.log();
|
|
1900
2096
|
}
|
|
2097
|
+
|
|
2098
|
+
for (const r of data.results) {
|
|
2099
|
+
printFunctionExplain(r);
|
|
2100
|
+
}
|
|
1901
2101
|
}
|
|
1902
2102
|
}
|
|
1903
2103
|
|
|
@@ -2079,8 +2279,12 @@ export function fnImpact(name, customDbPath, opts = {}) {
|
|
|
2079
2279
|
}
|
|
2080
2280
|
|
|
2081
2281
|
export function diffImpact(customDbPath, opts = {}) {
|
|
2282
|
+
if (opts.format === 'mermaid') {
|
|
2283
|
+
console.log(diffImpactMermaid(customDbPath, opts));
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2082
2286
|
const data = diffImpactData(customDbPath, opts);
|
|
2083
|
-
if (opts.json) {
|
|
2287
|
+
if (opts.json || opts.format === 'json') {
|
|
2084
2288
|
console.log(JSON.stringify(data, null, 2));
|
|
2085
2289
|
return;
|
|
2086
2290
|
}
|
package/src/structure.js
CHANGED
|
@@ -231,7 +231,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
231
231
|
*/
|
|
232
232
|
export function structureData(customDbPath, opts = {}) {
|
|
233
233
|
const db = openReadonlyOrFail(customDbPath);
|
|
234
|
-
const
|
|
234
|
+
const rawDir = opts.directory || null;
|
|
235
|
+
const filterDir = rawDir && normalizePath(rawDir) !== '.' ? rawDir : null;
|
|
235
236
|
const maxDepth = opts.depth || null;
|
|
236
237
|
const sortBy = opts.sort || 'files';
|
|
237
238
|
const noTests = opts.noTests || false;
|
package/src/watcher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { readFileSafe } from './builder.js';
|
|
3
4
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
4
5
|
import { initSchema, openDb } from './db.js';
|
|
5
6
|
import { appendJournalEntries } from './journal.js';
|
|
@@ -35,7 +36,7 @@ async function updateFile(_db, rootDir, filePath, stmts, engineOpts, cache) {
|
|
|
35
36
|
|
|
36
37
|
let code;
|
|
37
38
|
try {
|
|
38
|
-
code =
|
|
39
|
+
code = readFileSafe(filePath);
|
|
39
40
|
} catch (err) {
|
|
40
41
|
warn(`Cannot read ${relPath}: ${err.message}`);
|
|
41
42
|
return null;
|