@kodus/kodus-graph 0.2.9 → 0.2.10
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/dist/analysis/blast-radius.d.ts +1 -1
- package/dist/analysis/blast-radius.js +19 -21
- package/dist/analysis/context-builder.js +13 -4
- package/dist/analysis/diff.d.ts +6 -0
- package/dist/analysis/diff.js +16 -1
- package/dist/analysis/enrich.d.ts +1 -1
- package/dist/analysis/enrich.js +37 -9
- package/dist/analysis/prompt-formatter.js +8 -1
- package/dist/cli.js +2 -0
- package/dist/commands/analyze.js +5 -3
- package/dist/commands/diff.js +2 -2
- package/dist/commands/parse.d.ts +1 -0
- package/dist/commands/parse.js +3 -3
- package/dist/commands/update.js +2 -2
- package/dist/graph/builder.d.ts +5 -1
- package/dist/graph/builder.js +35 -3
- package/dist/graph/edges.d.ts +5 -1
- package/dist/graph/edges.js +61 -7
- package/dist/graph/types.d.ts +3 -0
- package/dist/parser/batch.d.ts +1 -0
- package/dist/parser/batch.js +18 -3
- package/dist/parser/languages.js +1 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -0
- package/dist/resolver/import-resolver.js +130 -32
- package/dist/resolver/languages/csharp.d.ts +2 -0
- package/dist/resolver/languages/csharp.js +69 -6
- package/dist/resolver/languages/go.js +8 -7
- package/dist/resolver/languages/java.js +102 -17
- package/dist/resolver/languages/php.js +26 -5
- package/dist/resolver/languages/python.js +79 -3
- package/dist/resolver/languages/ruby.d.ts +16 -1
- package/dist/resolver/languages/ruby.js +58 -7
- package/dist/resolver/languages/rust.js +8 -7
- package/dist/resolver/languages/typescript.d.ts +8 -0
- package/dist/resolver/languages/typescript.js +193 -17
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { BlastRadiusResult, GraphData } from '../graph/types';
|
|
2
|
-
export declare function computeBlastRadius(graph: GraphData,
|
|
2
|
+
export declare function computeBlastRadius(graph: GraphData, changedQualifiedNames: string[], maxDepth?: number, minConfidence?: number): BlastRadiusResult;
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
export function computeBlastRadius(graph,
|
|
2
|
-
|
|
1
|
+
export function computeBlastRadius(graph, changedQualifiedNames, maxDepth = 2, minConfidence) {
|
|
2
|
+
const minConf = minConfidence ?? 0.5;
|
|
3
|
+
// Build adjacency list filtering by confidence
|
|
3
4
|
const adj = new Map();
|
|
4
|
-
|
|
5
|
-
if (
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
// Reverse direction: target -> source (who calls/imports this?)
|
|
9
|
-
if (!adj.has(edge.target_qualified)) {
|
|
10
|
-
adj.set(edge.target_qualified, new Set());
|
|
5
|
+
const addEdge = (from, to) => {
|
|
6
|
+
if (!adj.has(from)) {
|
|
7
|
+
adj.set(from, new Set());
|
|
11
8
|
}
|
|
12
|
-
adj.get(
|
|
13
|
-
|
|
9
|
+
adj.get(from).add(to);
|
|
10
|
+
};
|
|
11
|
+
for (const edge of graph.edges) {
|
|
14
12
|
if (edge.kind === 'IMPORTS') {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// IMPORTS: no confidence filter, bidirectional
|
|
14
|
+
addEdge(edge.target_qualified, edge.source_qualified);
|
|
15
|
+
addEdge(edge.source_qualified, edge.target_qualified);
|
|
16
|
+
}
|
|
17
|
+
else if (edge.kind === 'CALLS' && (edge.confidence ?? 1.0) >= minConf) {
|
|
18
|
+
// CALLS: only edges with sufficient confidence, reverse direction
|
|
19
|
+
addEdge(edge.target_qualified, edge.source_qualified);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
const seeds = graph.nodes.filter((n) => changedSet.has(n.file_path)).map((n) => n.qualified_name);
|
|
24
|
-
// BFS
|
|
25
|
-
const visited = new Set(seeds);
|
|
22
|
+
// Seeds: qualified names directly (no more file-level)
|
|
23
|
+
const visited = new Set(changedQualifiedNames);
|
|
26
24
|
const byDepth = {};
|
|
27
|
-
let frontier =
|
|
25
|
+
let frontier = [...changedQualifiedNames];
|
|
28
26
|
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
29
27
|
const next = [];
|
|
30
28
|
for (const node of frontier) {
|
|
@@ -18,13 +18,22 @@ export function buildContextV2(opts) {
|
|
|
18
18
|
const newNodesInChanged = mergedGraph.nodes.filter((n) => changedSet.has(n.file_path));
|
|
19
19
|
const newEdgesInChanged = mergedGraph.edges.filter((e) => changedSet.has(e.file_path));
|
|
20
20
|
const structuralDiff = computeStructuralDiff(oldIndexed, newNodesInChanged, newEdgesInChanged, changedFiles);
|
|
21
|
-
|
|
21
|
+
// Extract truly changed qualified names from structural diff (added + modified + removed)
|
|
22
|
+
const trulyChangedQN = new Set([
|
|
23
|
+
...structuralDiff.nodes.added.map((n) => n.qualified_name),
|
|
24
|
+
...structuralDiff.nodes.modified.map((n) => n.qualified_name),
|
|
25
|
+
...structuralDiff.nodes.removed.map((n) => n.qualified_name),
|
|
26
|
+
]);
|
|
27
|
+
const blastRadius = computeBlastRadius(mergedGraph, [...trulyChangedQN], maxDepth, minConfidence);
|
|
22
28
|
const allFlows = detectFlows(indexed, { maxDepth: 10, type: 'all' });
|
|
23
29
|
const testGaps = opts.skipTests ? [] : findTestGaps(mergedGraph, changedFiles);
|
|
24
30
|
const risk = computeRiskScore(mergedGraph, changedFiles, blastRadius, { skipTests: opts.skipTests });
|
|
25
31
|
const inheritance = extractInheritance(indexed, changedFiles);
|
|
26
|
-
// Phase 3: Filter affected flows
|
|
27
|
-
const changedFuncSet = new Set(
|
|
32
|
+
// Phase 3: Filter affected flows — only truly changed (added+modified+removed), non-test functions
|
|
33
|
+
const changedFuncSet = new Set([...trulyChangedQN].filter((qn) => {
|
|
34
|
+
const node = indexed.byQualified.get(qn);
|
|
35
|
+
return node && !node.is_test;
|
|
36
|
+
}));
|
|
28
37
|
const affectedFlows = [];
|
|
29
38
|
for (const flow of allFlows.flows) {
|
|
30
39
|
const touches = flow.path.filter((qn) => changedFuncSet.has(qn));
|
|
@@ -39,7 +48,7 @@ export function buildContextV2(opts) {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
// Phase 3: Enrichment
|
|
42
|
-
const enriched = enrichChangedFunctions(indexed, changedFiles, structuralDiff, allFlows.flows, minConfidence);
|
|
51
|
+
const enriched = enrichChangedFunctions(indexed, changedFiles, structuralDiff, allFlows.flows, minConfidence, true);
|
|
43
52
|
// Phase 4: Assembly
|
|
44
53
|
const totalCallers = enriched.reduce((s, f) => s + f.callers.length, 0);
|
|
45
54
|
const totalCallees = enriched.reduce((s, f) => s + f.callees.length, 0);
|
package/dist/analysis/diff.d.ts
CHANGED
|
@@ -7,9 +7,15 @@ export interface NodeChange {
|
|
|
7
7
|
line_start: number;
|
|
8
8
|
line_end: number;
|
|
9
9
|
}
|
|
10
|
+
export interface ContractDiff {
|
|
11
|
+
field: 'params' | 'return_type' | 'modifiers';
|
|
12
|
+
old_value: string;
|
|
13
|
+
new_value: string;
|
|
14
|
+
}
|
|
10
15
|
export interface ModifiedNode {
|
|
11
16
|
qualified_name: string;
|
|
12
17
|
changes: string[];
|
|
18
|
+
contract_diffs: ContractDiff[];
|
|
13
19
|
}
|
|
14
20
|
export interface DiffResult {
|
|
15
21
|
changed_files: string[];
|
package/dist/analysis/diff.js
CHANGED
|
@@ -84,14 +84,29 @@ export function computeStructuralDiff(oldGraph, newNodes, newEdges, changedFiles
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
const contractDiffs = [];
|
|
87
88
|
if ((n.params || '') !== (newN.params || '')) {
|
|
88
89
|
changes.push('params');
|
|
90
|
+
contractDiffs.push({ field: 'params', old_value: n.params || '()', new_value: newN.params || '()' });
|
|
89
91
|
}
|
|
90
92
|
if ((n.return_type || '') !== (newN.return_type || '')) {
|
|
91
93
|
changes.push('return_type');
|
|
94
|
+
contractDiffs.push({
|
|
95
|
+
field: 'return_type',
|
|
96
|
+
old_value: n.return_type || 'void',
|
|
97
|
+
new_value: newN.return_type || 'void',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if ((n.modifiers || '') !== (newN.modifiers || '')) {
|
|
101
|
+
changes.push('modifiers');
|
|
102
|
+
contractDiffs.push({
|
|
103
|
+
field: 'modifiers',
|
|
104
|
+
old_value: n.modifiers || '',
|
|
105
|
+
new_value: newN.modifiers || '',
|
|
106
|
+
});
|
|
92
107
|
}
|
|
93
108
|
if (changes.length > 0) {
|
|
94
|
-
modified.push({ qualified_name: qn, changes });
|
|
109
|
+
modified.push({ qualified_name: qn, changes, contract_diffs: contractDiffs });
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
}
|
|
@@ -2,4 +2,4 @@ import type { IndexedGraph } from '../graph/loader';
|
|
|
2
2
|
import type { EnrichedFunction } from '../graph/types';
|
|
3
3
|
import type { DiffResult } from './diff';
|
|
4
4
|
import type { Flow } from './flows';
|
|
5
|
-
export declare function enrichChangedFunctions(graph: IndexedGraph, changedFiles: string[], diff: DiffResult, allFlows: Flow[], minConfidence: number): EnrichedFunction[];
|
|
5
|
+
export declare function enrichChangedFunctions(graph: IndexedGraph, changedFiles: string[], diff: DiffResult, allFlows: Flow[], minConfidence: number, onlyChanged?: boolean): EnrichedFunction[];
|
package/dist/analysis/enrich.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minConfidence) {
|
|
1
|
+
export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minConfidence, onlyChanged = false) {
|
|
2
2
|
const changedSet = new Set(changedFiles);
|
|
3
3
|
// Pre-index diff results
|
|
4
4
|
const addedSet = new Set(diff.nodes.added.map((n) => n.qualified_name));
|
|
5
|
-
const modifiedMap = new Map(diff.nodes.modified.map((m) => [m.qualified_name, m
|
|
5
|
+
const modifiedMap = new Map(diff.nodes.modified.map((m) => [m.qualified_name, m]));
|
|
6
6
|
// Pre-index TESTED_BY
|
|
7
7
|
const testedFiles = new Set(graph.edges.filter((e) => e.kind === 'TESTED_BY').map((e) => e.source_qualified));
|
|
8
8
|
// Pre-index flows by function
|
|
@@ -21,12 +21,22 @@ export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minC
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
// Filter functions in changed files
|
|
24
|
-
const changedFunctions = graph.nodes.filter((n) =>
|
|
25
|
-
!n.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
n.
|
|
29
|
-
|
|
24
|
+
const changedFunctions = graph.nodes.filter((n) => {
|
|
25
|
+
if (!changedSet.has(n.file_path)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (n.is_test ||
|
|
29
|
+
n.kind === 'Constructor' ||
|
|
30
|
+
n.kind === 'Class' ||
|
|
31
|
+
n.kind === 'Interface' ||
|
|
32
|
+
n.kind === 'Enum') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (onlyChanged) {
|
|
36
|
+
return addedSet.has(n.qualified_name) || modifiedMap.has(n.qualified_name);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
30
40
|
return changedFunctions
|
|
31
41
|
.sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line_start - b.line_start)
|
|
32
42
|
.map((node) => {
|
|
@@ -78,7 +88,23 @@ export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minC
|
|
|
78
88
|
const signature = `${shortName}${params}${ret}`;
|
|
79
89
|
// Diff
|
|
80
90
|
const isNew = addedSet.has(node.qualified_name);
|
|
81
|
-
const
|
|
91
|
+
const modifiedNode = modifiedMap.get(node.qualified_name);
|
|
92
|
+
const diffChanges = isNew ? [] : modifiedNode?.changes || [];
|
|
93
|
+
const contractDiffs = isNew ? [] : (modifiedNode?.contract_diffs ?? []);
|
|
94
|
+
// Caller impact
|
|
95
|
+
let callerImpact;
|
|
96
|
+
if (contractDiffs.length > 0 && callers.length > 0) {
|
|
97
|
+
const impacts = [];
|
|
98
|
+
const paramsDiff = contractDiffs.find((d) => d.field === 'params');
|
|
99
|
+
const returnDiff = contractDiffs.find((d) => d.field === 'return_type');
|
|
100
|
+
if (paramsDiff) {
|
|
101
|
+
impacts.push(`${callers.length} callers may need param update`);
|
|
102
|
+
}
|
|
103
|
+
if (returnDiff) {
|
|
104
|
+
impacts.push(`${callers.length} callers may assume old return type`);
|
|
105
|
+
}
|
|
106
|
+
callerImpact = impacts.length > 0 ? impacts.join('; ') : undefined;
|
|
107
|
+
}
|
|
82
108
|
return {
|
|
83
109
|
qualified_name: node.qualified_name,
|
|
84
110
|
name: node.name,
|
|
@@ -91,6 +117,8 @@ export function enrichChangedFunctions(graph, changedFiles, diff, allFlows, minC
|
|
|
91
117
|
callees,
|
|
92
118
|
has_test_coverage: testedFiles.has(node.file_path),
|
|
93
119
|
diff_changes: diffChanges,
|
|
120
|
+
contract_diffs: contractDiffs,
|
|
121
|
+
caller_impact: callerImpact,
|
|
94
122
|
is_new: isNew,
|
|
95
123
|
in_flows: flowsByFunction.get(node.qualified_name) || [],
|
|
96
124
|
};
|
|
@@ -20,7 +20,14 @@ export function formatPrompt(output) {
|
|
|
20
20
|
lines.push('Status: new');
|
|
21
21
|
}
|
|
22
22
|
else if (fn.diff_changes.length > 0) {
|
|
23
|
-
lines.push(
|
|
23
|
+
lines.push('Status: modified');
|
|
24
|
+
lines.push(` Changes: ${fn.diff_changes.join(', ')}`);
|
|
25
|
+
for (const cd of fn.contract_diffs) {
|
|
26
|
+
lines.push(` - ${cd.field}: ${cd.old_value} -> ${cd.new_value}`);
|
|
27
|
+
}
|
|
28
|
+
if (fn.caller_impact) {
|
|
29
|
+
lines.push(` Impact: ${fn.caller_impact}`);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
else {
|
|
26
33
|
lines.push('Status: unchanged');
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ program
|
|
|
24
24
|
.option('--include <glob...>', 'Include only files matching glob (repeatable)')
|
|
25
25
|
.option('--exclude <glob...>', 'Exclude files matching glob (repeatable)')
|
|
26
26
|
.option('--skip-tests', 'Skip test detection (no Test nodes, TESTED_BY edges, or test gaps)')
|
|
27
|
+
.option('--max-memory <mb>', 'Maximum memory usage in MB (default: 768)', (v) => parseInt(v, 10))
|
|
27
28
|
.requiredOption('--out <path>', 'Output JSON file path')
|
|
28
29
|
.action(async (opts) => {
|
|
29
30
|
const repoDir = resolve(opts.repoDir);
|
|
@@ -39,6 +40,7 @@ program
|
|
|
39
40
|
include: opts.include,
|
|
40
41
|
exclude: opts.exclude,
|
|
41
42
|
skipTests: opts.skipTests ?? false,
|
|
43
|
+
maxMemoryMB: opts.maxMemory,
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
46
|
program
|
package/dist/commands/analyze.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function executeAnalyze(opts) {
|
|
|
60
60
|
// Pre-resolve re-exports so barrel imports follow through to actual definitions
|
|
61
61
|
const barrelMap = buildReExportMap(rawGraph.reExports, repoDir, tsconfigAliases);
|
|
62
62
|
for (const imp of rawGraph.imports) {
|
|
63
|
-
const langKey = imp.lang
|
|
63
|
+
const langKey = imp.lang;
|
|
64
64
|
const resolved = resolveImport(resolve(repoDir, imp.file), imp.module, langKey, repoDir, tsconfigAliases);
|
|
65
65
|
const resolvedRel = resolved ? relative(repoDir, resolved) : null;
|
|
66
66
|
importEdges.push({
|
|
@@ -98,11 +98,13 @@ export async function executeAnalyze(opts) {
|
|
|
98
98
|
log.warn('Failed to compute file hash', { file: f, error: String(err) });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
const localGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes);
|
|
101
|
+
const localGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes, symbolTable, importMap);
|
|
102
102
|
// Merge with main graph (or use local only)
|
|
103
103
|
const mergedGraph = mainGraph ? mergeGraphs(mainGraph, localGraphData, opts.files) : localGraphData;
|
|
104
104
|
// Analyze
|
|
105
|
-
|
|
105
|
+
// Temporary: convert file-level to function-level until Mudança 3 provides trulyChangedQN
|
|
106
|
+
const changedQN = mergedGraph.nodes.filter((n) => opts.files.includes(n.file_path)).map((n) => n.qualified_name);
|
|
107
|
+
const blastRadius = computeBlastRadius(mergedGraph, changedQN);
|
|
106
108
|
const riskScore = computeRiskScore(mergedGraph, opts.files, blastRadius, { skipTests: opts.skipTests });
|
|
107
109
|
const testGaps = opts.skipTests ? [] : findTestGaps(mergedGraph, opts.files);
|
|
108
110
|
const output = {
|
package/dist/commands/diff.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function executeDiff(opts) {
|
|
|
57
57
|
symbolTable.add(i.file, i.name, i.qualified);
|
|
58
58
|
}
|
|
59
59
|
for (const imp of rawGraph.imports) {
|
|
60
|
-
const langKey = imp.lang
|
|
60
|
+
const langKey = imp.lang;
|
|
61
61
|
const resolved = resolveImport(resolve(repoDir, imp.file), imp.module, langKey, repoDir, tsconfigAliases);
|
|
62
62
|
const resolvedRel = resolved ? relative(repoDir, resolved) : null;
|
|
63
63
|
importEdges.push({
|
|
@@ -79,7 +79,7 @@ export async function executeDiff(opts) {
|
|
|
79
79
|
}
|
|
80
80
|
catch { }
|
|
81
81
|
}
|
|
82
|
-
const newGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes);
|
|
82
|
+
const newGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes, symbolTable, importMap);
|
|
83
83
|
process.stderr.write(`[3/4] Re-parsed ${absFiles.length} files (${newGraphData.nodes.length} nodes)\n`);
|
|
84
84
|
// Compute diff
|
|
85
85
|
const relChangedFiles = changedFiles.map((f) => (f.startsWith('/') ? relative(repoDir, f) : f));
|
package/dist/commands/parse.d.ts
CHANGED
package/dist/commands/parse.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function executeParse(opts) {
|
|
|
18
18
|
const files = discoverFiles(repoDir, opts.all ? undefined : opts.files, opts.include, opts.exclude);
|
|
19
19
|
process.stderr.write(`[1/5] Discovered ${files.length} files\n`);
|
|
20
20
|
// Phase 2: Parse + extract
|
|
21
|
-
let rawGraph = await parseBatch(files, repoDir, { skipTests: opts.skipTests });
|
|
21
|
+
let rawGraph = await parseBatch(files, repoDir, { skipTests: opts.skipTests, maxMemoryMB: opts.maxMemoryMB });
|
|
22
22
|
process.stderr.write(`[2/5] Parsed ${rawGraph.functions.length} functions, ${rawGraph.classes.length} classes, ${rawGraph.rawCalls.length} call sites\n`);
|
|
23
23
|
// Phase 3: Resolve imports
|
|
24
24
|
const tsconfigAliases = loadTsconfigAliases(repoDir);
|
|
@@ -37,7 +37,7 @@ export async function executeParse(opts) {
|
|
|
37
37
|
// Pre-resolve re-exports so barrel imports follow through to actual definitions
|
|
38
38
|
const barrelMap = buildReExportMap(rawGraph.reExports, repoDir, tsconfigAliases);
|
|
39
39
|
for (const imp of rawGraph.imports) {
|
|
40
|
-
const langKey = imp.lang
|
|
40
|
+
const langKey = imp.lang;
|
|
41
41
|
const resolved = resolveImport(resolve(repoDir, imp.file), imp.module, langKey, repoDir, tsconfigAliases);
|
|
42
42
|
const resolvedRel = resolved ? relative(repoDir, resolved) : null;
|
|
43
43
|
importEdges.push({
|
|
@@ -80,7 +80,7 @@ export async function executeParse(opts) {
|
|
|
80
80
|
}
|
|
81
81
|
const parseErrors = rawGraph.parseErrors;
|
|
82
82
|
const extractErrors = rawGraph.extractErrors;
|
|
83
|
-
const graphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes);
|
|
83
|
+
const graphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes, symbolTable, importMap);
|
|
84
84
|
process.stderr.write(`[5/5] Built graph: ${graphData.nodes.length} nodes, ${graphData.edges.length} edges\n`);
|
|
85
85
|
// Release intermediaries — no longer needed after buildGraphData
|
|
86
86
|
rawGraph = null;
|
package/dist/commands/update.js
CHANGED
|
@@ -97,7 +97,7 @@ export async function executeUpdate(opts) {
|
|
|
97
97
|
symbolTable.add(i.file, i.name, i.qualified);
|
|
98
98
|
}
|
|
99
99
|
for (const imp of rawGraph.imports) {
|
|
100
|
-
const langKey = imp.lang
|
|
100
|
+
const langKey = imp.lang;
|
|
101
101
|
const resolved = resolveImport(resolve(repoDir, imp.file), imp.module, langKey, repoDir, tsconfigAliases);
|
|
102
102
|
const resolvedRel = resolved ? relative(repoDir, resolved) : null;
|
|
103
103
|
importEdges.push({
|
|
@@ -119,7 +119,7 @@ export async function executeUpdate(opts) {
|
|
|
119
119
|
}
|
|
120
120
|
catch { }
|
|
121
121
|
}
|
|
122
|
-
const newGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes);
|
|
122
|
+
const newGraphData = buildGraphData(rawGraph, callEdges, importEdges, repoDir, fileHashes, symbolTable, importMap);
|
|
123
123
|
process.stderr.write(`[4/5] Built new graph fragment (${newGraphData.nodes.length} nodes)\n`);
|
|
124
124
|
// Merge: keep old nodes/edges NOT in changed/deleted files, add new ones
|
|
125
125
|
const changedOrDeleted = new Set([...toReparse, ...deleted]);
|
package/dist/graph/builder.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { GraphData, ImportEdge, RawCallEdge, RawGraph } from './types';
|
|
2
|
-
export declare function buildGraphData(raw: RawGraph, callEdges: RawCallEdge[], importEdges: ImportEdge[], _repoDir: string, fileHashes: Map<string, string
|
|
2
|
+
export declare function buildGraphData(raw: RawGraph, callEdges: RawCallEdge[], importEdges: ImportEdge[], _repoDir: string, fileHashes: Map<string, string>, symbolTable?: {
|
|
3
|
+
lookupGlobal(name: string): string[];
|
|
4
|
+
}, importMap?: {
|
|
5
|
+
lookup(file: string, name: string): string | null;
|
|
6
|
+
}): GraphData;
|
package/dist/graph/builder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deriveEdges } from './edges';
|
|
2
|
-
export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes) {
|
|
2
|
+
export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes, symbolTable, importMap) {
|
|
3
3
|
const nodes = [];
|
|
4
4
|
const edges = [];
|
|
5
5
|
// Functions -> nodes
|
|
@@ -85,6 +85,23 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
85
85
|
content_hash: t.content_hash,
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
+
// Build a set of all parsed file paths for validation (filter external targets)
|
|
89
|
+
const parsedFiles = new Set();
|
|
90
|
+
for (const f of raw.functions) {
|
|
91
|
+
parsedFiles.add(f.file);
|
|
92
|
+
}
|
|
93
|
+
for (const c of raw.classes) {
|
|
94
|
+
parsedFiles.add(c.file);
|
|
95
|
+
}
|
|
96
|
+
for (const i of raw.interfaces) {
|
|
97
|
+
parsedFiles.add(i.file);
|
|
98
|
+
}
|
|
99
|
+
for (const e of raw.enums) {
|
|
100
|
+
parsedFiles.add(e.file);
|
|
101
|
+
}
|
|
102
|
+
for (const t of raw.tests) {
|
|
103
|
+
parsedFiles.add(t.file);
|
|
104
|
+
}
|
|
88
105
|
// Build file→functions index to resolve caller from line number
|
|
89
106
|
const functionsByFile = new Map();
|
|
90
107
|
for (const node of nodes) {
|
|
@@ -106,6 +123,11 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
106
123
|
}
|
|
107
124
|
// CALLS edges — resolve caller function from call line number
|
|
108
125
|
for (const ce of callEdges) {
|
|
126
|
+
// Skip calls to external packages (target file not in repo)
|
|
127
|
+
const targetFile = ce.target.split('::')[0];
|
|
128
|
+
if (targetFile && !parsedFiles.has(targetFile)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
109
131
|
const sourceFile = ce.source.includes('::') ? ce.source.split('::')[0] : ce.source;
|
|
110
132
|
let sourceQualified;
|
|
111
133
|
if (ce.source.includes('::')) {
|
|
@@ -134,8 +156,11 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
134
156
|
confidence: ce.confidence,
|
|
135
157
|
});
|
|
136
158
|
}
|
|
137
|
-
// IMPORTS edges
|
|
159
|
+
// IMPORTS edges — only emit resolved imports (skip external/unresolved packages)
|
|
138
160
|
for (const ie of importEdges) {
|
|
161
|
+
if (!ie.resolved) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
139
164
|
edges.push({
|
|
140
165
|
kind: 'IMPORTS',
|
|
141
166
|
source_qualified: ie.source,
|
|
@@ -145,7 +170,14 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
// Derived edges
|
|
148
|
-
const derived = deriveEdges(raw, importEdges);
|
|
173
|
+
const derived = deriveEdges(raw, importEdges, symbolTable, importMap);
|
|
174
|
+
// Release raw graph arrays — no longer needed after deriveEdges
|
|
175
|
+
raw.functions = [];
|
|
176
|
+
raw.classes = [];
|
|
177
|
+
raw.interfaces = [];
|
|
178
|
+
raw.enums = [];
|
|
179
|
+
raw.tests = [];
|
|
180
|
+
raw.rawCalls = [];
|
|
149
181
|
for (const e of derived.inherits) {
|
|
150
182
|
edges.push({
|
|
151
183
|
kind: 'INHERITS',
|
package/dist/graph/edges.d.ts
CHANGED
|
@@ -15,5 +15,9 @@ export interface DerivedEdges {
|
|
|
15
15
|
* prefixes/suffixes. Returns null if no test pattern was found.
|
|
16
16
|
*/
|
|
17
17
|
export declare function extractTestStem(testFile: string): string | null;
|
|
18
|
-
export declare function deriveEdges(graph: RawGraph, importEdges: ImportEdge[]
|
|
18
|
+
export declare function deriveEdges(graph: RawGraph, importEdges: ImportEdge[], symbolTable?: {
|
|
19
|
+
lookupGlobal(name: string): string[];
|
|
20
|
+
}, importMap?: {
|
|
21
|
+
lookup(file: string, name: string): string | null;
|
|
22
|
+
}): DerivedEdges;
|
|
19
23
|
export {};
|
package/dist/graph/edges.js
CHANGED
|
@@ -21,16 +21,70 @@ export function extractTestStem(testFile) {
|
|
|
21
21
|
}
|
|
22
22
|
return cleaned;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a bare type name (e.g. "User", "IAuthService") to its qualified name
|
|
26
|
+
* using import map, same-file lookup, and global symbol table.
|
|
27
|
+
* Returns null if the name cannot be resolved (external class/interface).
|
|
28
|
+
*/
|
|
29
|
+
function resolveTypeName(name, file, graph, symbolTable, importMap) {
|
|
30
|
+
// 1. Check import map — was it imported in this file?
|
|
31
|
+
const importedFrom = importMap?.lookup(file, name);
|
|
32
|
+
if (importedFrom) {
|
|
33
|
+
// Look up the qualified name in the imported file
|
|
34
|
+
const candidates = symbolTable?.lookupGlobal(name) ?? [];
|
|
35
|
+
const match = candidates.find((q) => q.startsWith(`${importedFrom}::`));
|
|
36
|
+
if (match) {
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
// If importedFrom is not a local file (not in graph), it's an external package — skip
|
|
40
|
+
const isLocal = graph.classes.some((c) => c.file === importedFrom) ||
|
|
41
|
+
graph.interfaces.some((i) => i.file === importedFrom) ||
|
|
42
|
+
graph.functions.some((f) => f.file === importedFrom);
|
|
43
|
+
if (!isLocal) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Fallback: construct qualified name from local import target
|
|
47
|
+
return `${importedFrom}::${name}`;
|
|
48
|
+
}
|
|
49
|
+
// 2. Check same file — class or interface defined in same file
|
|
50
|
+
const sameFileClass = graph.classes.find((other) => other.name === name && other.file === file);
|
|
51
|
+
if (sameFileClass) {
|
|
52
|
+
return sameFileClass.qualified;
|
|
53
|
+
}
|
|
54
|
+
const sameFileInterface = graph.interfaces.find((other) => other.name === name && other.file === file);
|
|
55
|
+
if (sameFileInterface) {
|
|
56
|
+
return sameFileInterface.qualified;
|
|
57
|
+
}
|
|
58
|
+
// 3. Check global symbol table — unique match only
|
|
59
|
+
const globalCandidates = symbolTable?.lookupGlobal(name) ?? [];
|
|
60
|
+
if (globalCandidates.length === 1) {
|
|
61
|
+
return globalCandidates[0];
|
|
62
|
+
}
|
|
63
|
+
// 4. External class/interface (React.Component, Error, etc.) — unresolvable
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function deriveEdges(graph, importEdges, symbolTable, importMap) {
|
|
67
|
+
// INHERITS: class extends another class — resolve to qualified names
|
|
68
|
+
const inherits = [];
|
|
69
|
+
for (const c of graph.classes) {
|
|
70
|
+
if (!c.extends) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const resolved = resolveTypeName(c.extends, c.file, graph, symbolTable, importMap);
|
|
74
|
+
if (resolved) {
|
|
75
|
+
inherits.push({ source: c.qualified, target: resolved, file: c.file });
|
|
76
|
+
}
|
|
77
|
+
// Skip unresolvable external classes (React.Component, Error, etc.)
|
|
78
|
+
}
|
|
79
|
+
// IMPLEMENTS: class implements interface(s) — resolve to qualified names
|
|
30
80
|
const implements_ = [];
|
|
31
81
|
for (const c of graph.classes) {
|
|
32
82
|
for (const iface of c.implements) {
|
|
33
|
-
|
|
83
|
+
const resolved = resolveTypeName(iface, c.file, graph, symbolTable, importMap);
|
|
84
|
+
if (resolved) {
|
|
85
|
+
implements_.push({ source: c.qualified, target: resolved, file: c.file });
|
|
86
|
+
}
|
|
87
|
+
// Skip unresolvable external interfaces
|
|
34
88
|
}
|
|
35
89
|
}
|
|
36
90
|
// TESTED_BY: two heuristics, deduplicated
|
package/dist/graph/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ContractDiff } from '../analysis/diff';
|
|
1
2
|
export type NodeKind = 'Function' | 'Method' | 'Constructor' | 'Class' | 'Interface' | 'Enum' | 'Test';
|
|
2
3
|
export type EdgeKind = 'CALLS' | 'IMPORTS' | 'INHERITS' | 'IMPLEMENTS' | 'TESTED_BY' | 'CONTAINS';
|
|
3
4
|
export interface GraphNode {
|
|
@@ -122,6 +123,8 @@ export interface EnrichedFunction {
|
|
|
122
123
|
callees: CalleeRef[];
|
|
123
124
|
has_test_coverage: boolean;
|
|
124
125
|
diff_changes: string[];
|
|
126
|
+
contract_diffs: ContractDiff[];
|
|
127
|
+
caller_impact?: string;
|
|
125
128
|
is_new: boolean;
|
|
126
129
|
in_flows: string[];
|
|
127
130
|
}
|
package/dist/parser/batch.d.ts
CHANGED
package/dist/parser/batch.js
CHANGED
|
@@ -5,7 +5,8 @@ import { NOISE } from '../shared/filters';
|
|
|
5
5
|
import { log } from '../shared/logger';
|
|
6
6
|
import { extractCallsFromFile, extractFromFile } from './extractor';
|
|
7
7
|
import { getLanguage } from './languages';
|
|
8
|
-
const
|
|
8
|
+
const INITIAL_BATCH = 50;
|
|
9
|
+
const MEMORY_THRESHOLD_RATIO = 0.7;
|
|
9
10
|
export async function parseBatch(files, repoRoot, options) {
|
|
10
11
|
const graph = {
|
|
11
12
|
functions: [],
|
|
@@ -21,8 +22,10 @@ export async function parseBatch(files, repoRoot, options) {
|
|
|
21
22
|
const seen = new Set();
|
|
22
23
|
let parseErrors = 0;
|
|
23
24
|
let extractErrors = 0;
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let batchSize = INITIAL_BATCH;
|
|
26
|
+
const maxMemBytes = (options?.maxMemoryMB ?? 768) * 1024 * 1024;
|
|
27
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
28
|
+
const batch = files.slice(i, i + batchSize);
|
|
26
29
|
const promises = batch.map(async (filePath) => {
|
|
27
30
|
const lang = getLanguage(extname(filePath));
|
|
28
31
|
if (!lang) {
|
|
@@ -70,6 +73,18 @@ export async function parseBatch(files, repoRoot, options) {
|
|
|
70
73
|
}
|
|
71
74
|
});
|
|
72
75
|
await Promise.all(promises);
|
|
76
|
+
// Dynamic batch sizing: reduce if memory pressure detected
|
|
77
|
+
const rss = process.memoryUsage().rss;
|
|
78
|
+
if (rss > maxMemBytes * MEMORY_THRESHOLD_RATIO) {
|
|
79
|
+
const oldBatch = batchSize;
|
|
80
|
+
batchSize = Math.max(5, Math.floor(batchSize / 2));
|
|
81
|
+
log.warn('Memory pressure detected, reducing batch size', {
|
|
82
|
+
rssMB: Math.round(rss / 1024 / 1024),
|
|
83
|
+
maxMB: Math.round(maxMemBytes / 1024 / 1024),
|
|
84
|
+
oldBatchSize: oldBatch,
|
|
85
|
+
newBatchSize: batchSize,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
if (options?.skipTests) {
|
|
75
90
|
graph.tests = [];
|
package/dist/parser/languages.js
CHANGED
|
@@ -242,6 +242,7 @@ function derivedKinds(config) {
|
|
|
242
242
|
result.method = firstOf(config.method);
|
|
243
243
|
}
|
|
244
244
|
if (firstOf(config.constructorKinds)) {
|
|
245
|
+
// biome-ignore lint/complexity/useLiteralKeys: bracket notation required — dot notation resolves to Function.prototype.constructor (TS2322)
|
|
245
246
|
result['constructor'] = firstOf(config.constructorKinds);
|
|
246
247
|
}
|
|
247
248
|
if (firstOf(config.interface)) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External package detector.
|
|
3
|
+
* Reads dependency manifests (package.json, requirements.txt, go.mod, etc.)
|
|
4
|
+
* to determine if an import target is an external (third-party) package.
|
|
5
|
+
*/
|
|
6
|
+
export declare function clearExternalCache(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Check if an import is an external (third-party) package.
|
|
9
|
+
* Returns the package name if external, null if not detected as external.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectExternal(modulePath: string, lang: string, repoRoot: string): string | null;
|