@kodus/kodus-graph 0.2.9 → 0.2.11
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.d.ts +11 -0
- package/dist/analysis/prompt-formatter.js +147 -112
- 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 +39 -4
- 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
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('::')) {
|
|
@@ -123,7 +145,10 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
123
145
|
}
|
|
124
146
|
}
|
|
125
147
|
}
|
|
126
|
-
|
|
148
|
+
if (!resolved) {
|
|
149
|
+
continue; // Skip top-level calls with no enclosing function
|
|
150
|
+
}
|
|
151
|
+
sourceQualified = resolved;
|
|
127
152
|
}
|
|
128
153
|
edges.push({
|
|
129
154
|
kind: 'CALLS',
|
|
@@ -134,8 +159,11 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
134
159
|
confidence: ce.confidence,
|
|
135
160
|
});
|
|
136
161
|
}
|
|
137
|
-
// IMPORTS edges
|
|
162
|
+
// IMPORTS edges — only emit resolved imports (skip external/unresolved packages)
|
|
138
163
|
for (const ie of importEdges) {
|
|
164
|
+
if (!ie.resolved) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
139
167
|
edges.push({
|
|
140
168
|
kind: 'IMPORTS',
|
|
141
169
|
source_qualified: ie.source,
|
|
@@ -145,7 +173,14 @@ export function buildGraphData(raw, callEdges, importEdges, _repoDir, fileHashes
|
|
|
145
173
|
});
|
|
146
174
|
}
|
|
147
175
|
// Derived edges
|
|
148
|
-
const derived = deriveEdges(raw, importEdges);
|
|
176
|
+
const derived = deriveEdges(raw, importEdges, symbolTable, importMap);
|
|
177
|
+
// Release raw graph arrays — no longer needed after deriveEdges
|
|
178
|
+
raw.functions = [];
|
|
179
|
+
raw.classes = [];
|
|
180
|
+
raw.interfaces = [];
|
|
181
|
+
raw.enums = [];
|
|
182
|
+
raw.tests = [];
|
|
183
|
+
raw.rawCalls = [];
|
|
149
184
|
for (const e of derived.inherits) {
|
|
150
185
|
edges.push({
|
|
151
186
|
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;
|