@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.
Files changed (40) hide show
  1. package/dist/analysis/blast-radius.d.ts +1 -1
  2. package/dist/analysis/blast-radius.js +19 -21
  3. package/dist/analysis/context-builder.js +13 -4
  4. package/dist/analysis/diff.d.ts +6 -0
  5. package/dist/analysis/diff.js +16 -1
  6. package/dist/analysis/enrich.d.ts +1 -1
  7. package/dist/analysis/enrich.js +37 -9
  8. package/dist/analysis/prompt-formatter.d.ts +11 -0
  9. package/dist/analysis/prompt-formatter.js +147 -112
  10. package/dist/cli.js +2 -0
  11. package/dist/commands/analyze.js +5 -3
  12. package/dist/commands/diff.js +2 -2
  13. package/dist/commands/parse.d.ts +1 -0
  14. package/dist/commands/parse.js +3 -3
  15. package/dist/commands/update.js +2 -2
  16. package/dist/graph/builder.d.ts +5 -1
  17. package/dist/graph/builder.js +39 -4
  18. package/dist/graph/edges.d.ts +5 -1
  19. package/dist/graph/edges.js +61 -7
  20. package/dist/graph/types.d.ts +3 -0
  21. package/dist/parser/batch.d.ts +1 -0
  22. package/dist/parser/batch.js +18 -3
  23. package/dist/parser/languages.js +1 -0
  24. package/dist/resolver/external-detector.d.ts +11 -0
  25. package/dist/resolver/external-detector.js +820 -0
  26. package/dist/resolver/fs-cache.d.ts +8 -0
  27. package/dist/resolver/fs-cache.js +36 -0
  28. package/dist/resolver/import-resolver.js +130 -32
  29. package/dist/resolver/languages/csharp.d.ts +2 -0
  30. package/dist/resolver/languages/csharp.js +69 -6
  31. package/dist/resolver/languages/go.js +8 -7
  32. package/dist/resolver/languages/java.js +102 -17
  33. package/dist/resolver/languages/php.js +26 -5
  34. package/dist/resolver/languages/python.js +79 -3
  35. package/dist/resolver/languages/ruby.d.ts +16 -1
  36. package/dist/resolver/languages/ruby.js +58 -7
  37. package/dist/resolver/languages/rust.js +8 -7
  38. package/dist/resolver/languages/typescript.d.ts +8 -0
  39. package/dist/resolver/languages/typescript.js +193 -17
  40. package/package.json +1 -1
@@ -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 === 'python' ? 'python' : imp.lang === 'ruby' ? 'ruby' : 'typescript';
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;
@@ -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 === 'python' ? 'python' : imp.lang === 'ruby' ? 'ruby' : 'typescript';
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]);
@@ -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>): GraphData;
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;
@@ -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
- sourceQualified = resolved || `${ce.source}::unknown`;
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',
@@ -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[]): DerivedEdges;
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 {};
@@ -21,16 +21,70 @@ export function extractTestStem(testFile) {
21
21
  }
22
22
  return cleaned;
23
23
  }
24
- export function deriveEdges(graph, importEdges) {
25
- // INHERITS: class extends another class
26
- const inherits = graph.classes
27
- .filter((c) => c.extends)
28
- .map((c) => ({ source: c.qualified, target: c.extends, file: c.file }));
29
- // IMPLEMENTS: class implements interface(s)
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
- implements_.push({ source: c.qualified, target: iface, file: c.file });
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
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  import type { ParseBatchResult } from '../graph/types';
2
2
  export declare function parseBatch(files: string[], repoRoot: string, options?: {
3
3
  skipTests?: boolean;
4
+ maxMemoryMB?: number;
4
5
  }): Promise<ParseBatchResult>;
@@ -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 BATCH_SIZE = 50;
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
- for (let i = 0; i < files.length; i += BATCH_SIZE) {
25
- const batch = files.slice(i, i + BATCH_SIZE);
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 = [];
@@ -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;