@kodus/kodus-graph 0.2.8 → 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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/dist/analysis/blast-radius.d.ts +2 -0
  4. package/dist/analysis/blast-radius.js +55 -0
  5. package/dist/analysis/communities.d.ts +28 -0
  6. package/dist/analysis/communities.js +100 -0
  7. package/dist/analysis/context-builder.d.ts +34 -0
  8. package/dist/analysis/context-builder.js +92 -0
  9. package/dist/analysis/diff.d.ts +41 -0
  10. package/dist/analysis/diff.js +155 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +126 -0
  13. package/dist/analysis/flows.d.ts +27 -0
  14. package/dist/analysis/flows.js +86 -0
  15. package/dist/analysis/inheritance.d.ts +3 -0
  16. package/dist/analysis/inheritance.js +31 -0
  17. package/dist/analysis/prompt-formatter.d.ts +2 -0
  18. package/dist/analysis/prompt-formatter.js +173 -0
  19. package/dist/analysis/risk-score.d.ts +4 -0
  20. package/dist/analysis/risk-score.js +51 -0
  21. package/dist/analysis/search.d.ts +11 -0
  22. package/dist/analysis/search.js +64 -0
  23. package/dist/analysis/test-gaps.d.ts +2 -0
  24. package/dist/analysis/test-gaps.js +14 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +210 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +116 -0
  29. package/dist/commands/communities.d.ts +8 -0
  30. package/dist/commands/communities.js +9 -0
  31. package/dist/commands/context.d.ts +12 -0
  32. package/dist/commands/context.js +130 -0
  33. package/dist/commands/diff.d.ts +9 -0
  34. package/dist/commands/diff.js +89 -0
  35. package/dist/commands/flows.d.ts +8 -0
  36. package/dist/commands/flows.js +9 -0
  37. package/dist/commands/parse.d.ts +11 -0
  38. package/dist/commands/parse.js +101 -0
  39. package/dist/commands/search.d.ts +12 -0
  40. package/dist/commands/search.js +27 -0
  41. package/dist/commands/update.d.ts +7 -0
  42. package/dist/commands/update.js +154 -0
  43. package/dist/graph/builder.d.ts +6 -0
  44. package/dist/graph/builder.js +248 -0
  45. package/dist/graph/edges.d.ts +23 -0
  46. package/dist/graph/edges.js +159 -0
  47. package/dist/graph/json-writer.d.ts +9 -0
  48. package/dist/graph/json-writer.js +38 -0
  49. package/dist/graph/loader.d.ts +13 -0
  50. package/dist/graph/loader.js +101 -0
  51. package/dist/graph/merger.d.ts +7 -0
  52. package/dist/graph/merger.js +18 -0
  53. package/dist/graph/types.d.ts +252 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +5 -0
  56. package/dist/parser/batch.js +93 -0
  57. package/dist/parser/discovery.d.ts +7 -0
  58. package/dist/parser/discovery.js +61 -0
  59. package/dist/parser/extractor.d.ts +4 -0
  60. package/dist/parser/extractor.js +33 -0
  61. package/dist/parser/extractors/generic.d.ts +8 -0
  62. package/dist/parser/extractors/generic.js +471 -0
  63. package/dist/parser/extractors/python.d.ts +8 -0
  64. package/dist/parser/extractors/python.js +133 -0
  65. package/dist/parser/extractors/ruby.d.ts +8 -0
  66. package/dist/parser/extractors/ruby.js +153 -0
  67. package/dist/parser/extractors/typescript.d.ts +10 -0
  68. package/dist/parser/extractors/typescript.js +365 -0
  69. package/dist/parser/languages.d.ts +32 -0
  70. package/dist/parser/languages.js +304 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/external-detector.d.ts +11 -0
  74. package/dist/resolver/external-detector.js +820 -0
  75. package/dist/resolver/fs-cache.d.ts +8 -0
  76. package/dist/resolver/fs-cache.js +36 -0
  77. package/dist/resolver/import-map.d.ts +12 -0
  78. package/dist/resolver/import-map.js +21 -0
  79. package/dist/resolver/import-resolver.d.ts +19 -0
  80. package/dist/resolver/import-resolver.js +310 -0
  81. package/dist/resolver/languages/csharp.d.ts +3 -0
  82. package/dist/resolver/languages/csharp.js +94 -0
  83. package/dist/resolver/languages/go.d.ts +3 -0
  84. package/dist/resolver/languages/go.js +197 -0
  85. package/dist/resolver/languages/java.d.ts +1 -0
  86. package/dist/resolver/languages/java.js +193 -0
  87. package/dist/resolver/languages/php.d.ts +3 -0
  88. package/dist/resolver/languages/php.js +75 -0
  89. package/dist/resolver/languages/python.d.ts +11 -0
  90. package/dist/resolver/languages/python.js +127 -0
  91. package/dist/resolver/languages/ruby.d.ts +24 -0
  92. package/dist/resolver/languages/ruby.js +110 -0
  93. package/dist/resolver/languages/rust.d.ts +1 -0
  94. package/dist/resolver/languages/rust.js +197 -0
  95. package/dist/resolver/languages/typescript.d.ts +35 -0
  96. package/dist/resolver/languages/typescript.js +416 -0
  97. package/dist/resolver/re-export-resolver.d.ts +24 -0
  98. package/dist/resolver/re-export-resolver.js +57 -0
  99. package/dist/resolver/symbol-table.d.ts +17 -0
  100. package/dist/resolver/symbol-table.js +60 -0
  101. package/dist/shared/extract-calls.d.ts +26 -0
  102. package/dist/shared/extract-calls.js +57 -0
  103. package/dist/shared/file-hash.d.ts +3 -0
  104. package/dist/shared/file-hash.js +10 -0
  105. package/dist/shared/filters.d.ts +3 -0
  106. package/dist/shared/filters.js +240 -0
  107. package/dist/shared/logger.d.ts +6 -0
  108. package/dist/shared/logger.js +17 -0
  109. package/dist/shared/qualified-name.d.ts +1 -0
  110. package/dist/shared/qualified-name.js +9 -0
  111. package/dist/shared/safe-path.d.ts +6 -0
  112. package/dist/shared/safe-path.js +29 -0
  113. package/dist/shared/schemas.d.ts +43 -0
  114. package/dist/shared/schemas.js +30 -0
  115. package/dist/shared/temp.d.ts +11 -0
  116. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  117. package/package.json +20 -6
  118. package/src/analysis/blast-radius.ts +0 -54
  119. package/src/analysis/communities.ts +0 -135
  120. package/src/analysis/context-builder.ts +0 -130
  121. package/src/analysis/diff.ts +0 -169
  122. package/src/analysis/enrich.ts +0 -110
  123. package/src/analysis/flows.ts +0 -112
  124. package/src/analysis/inheritance.ts +0 -34
  125. package/src/analysis/prompt-formatter.ts +0 -175
  126. package/src/analysis/risk-score.ts +0 -62
  127. package/src/analysis/search.ts +0 -76
  128. package/src/analysis/test-gaps.ts +0 -21
  129. package/src/cli.ts +0 -210
  130. package/src/commands/analyze.ts +0 -128
  131. package/src/commands/communities.ts +0 -19
  132. package/src/commands/context.ts +0 -182
  133. package/src/commands/diff.ts +0 -96
  134. package/src/commands/flows.ts +0 -19
  135. package/src/commands/parse.ts +0 -124
  136. package/src/commands/search.ts +0 -41
  137. package/src/commands/update.ts +0 -166
  138. package/src/graph/builder.ts +0 -209
  139. package/src/graph/edges.ts +0 -101
  140. package/src/graph/json-writer.ts +0 -43
  141. package/src/graph/loader.ts +0 -113
  142. package/src/graph/merger.ts +0 -25
  143. package/src/graph/types.ts +0 -283
  144. package/src/parser/batch.ts +0 -82
  145. package/src/parser/discovery.ts +0 -75
  146. package/src/parser/extractor.ts +0 -37
  147. package/src/parser/extractors/generic.ts +0 -132
  148. package/src/parser/extractors/python.ts +0 -133
  149. package/src/parser/extractors/ruby.ts +0 -147
  150. package/src/parser/extractors/typescript.ts +0 -350
  151. package/src/parser/languages.ts +0 -122
  152. package/src/resolver/call-resolver.ts +0 -244
  153. package/src/resolver/import-map.ts +0 -27
  154. package/src/resolver/import-resolver.ts +0 -72
  155. package/src/resolver/languages/csharp.ts +0 -7
  156. package/src/resolver/languages/go.ts +0 -7
  157. package/src/resolver/languages/java.ts +0 -7
  158. package/src/resolver/languages/php.ts +0 -7
  159. package/src/resolver/languages/python.ts +0 -35
  160. package/src/resolver/languages/ruby.ts +0 -21
  161. package/src/resolver/languages/rust.ts +0 -7
  162. package/src/resolver/languages/typescript.ts +0 -168
  163. package/src/resolver/re-export-resolver.ts +0 -66
  164. package/src/resolver/symbol-table.ts +0 -67
  165. package/src/shared/extract-calls.ts +0 -75
  166. package/src/shared/file-hash.ts +0 -12
  167. package/src/shared/filters.ts +0 -243
  168. package/src/shared/logger.ts +0 -17
  169. package/src/shared/qualified-name.ts +0 -5
  170. package/src/shared/safe-path.ts +0 -31
  171. package/src/shared/schemas.ts +0 -32
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared filesystem cache for resolvers.
3
+ * Caches existsSync and readdirSync results to avoid repeated disk I/O.
4
+ * Call clearFsCache() between analysis runs.
5
+ */
6
+ export declare function cachedExists(path: string): boolean;
7
+ export declare function cachedReaddir(path: string): string[];
8
+ export declare function clearFsCache(): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared filesystem cache for resolvers.
3
+ * Caches existsSync and readdirSync results to avoid repeated disk I/O.
4
+ * Call clearFsCache() between analysis runs.
5
+ */
6
+ import { existsSync, readdirSync } from 'fs';
7
+ const existsCache = new Map();
8
+ const readdirCache = new Map();
9
+ export function cachedExists(path) {
10
+ const cached = existsCache.get(path);
11
+ if (cached !== undefined) {
12
+ return cached;
13
+ }
14
+ const result = existsSync(path);
15
+ existsCache.set(path, result);
16
+ return result;
17
+ }
18
+ export function cachedReaddir(path) {
19
+ const cached = readdirCache.get(path);
20
+ if (cached !== undefined) {
21
+ return cached;
22
+ }
23
+ try {
24
+ const result = readdirSync(path).sort();
25
+ readdirCache.set(path, result);
26
+ return result;
27
+ }
28
+ catch {
29
+ readdirCache.set(path, []);
30
+ return [];
31
+ }
32
+ }
33
+ export function clearFsCache() {
34
+ existsCache.clear();
35
+ readdirCache.clear();
36
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Import map: tracks which symbols are imported from where per file.
3
+ *
4
+ * For each importing file, maps symbol names to the resolved file path
5
+ * they were imported from. Used by the call resolver to connect
6
+ * function calls to their definitions across files.
7
+ */
8
+ export interface ImportMap {
9
+ add(file: string, name: string, targetFile: string): void;
10
+ lookup(file: string, name: string): string | null;
11
+ }
12
+ export declare function createImportMap(): ImportMap;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Import map: tracks which symbols are imported from where per file.
3
+ *
4
+ * For each importing file, maps symbol names to the resolved file path
5
+ * they were imported from. Used by the call resolver to connect
6
+ * function calls to their definitions across files.
7
+ */
8
+ export function createImportMap() {
9
+ const map = new Map();
10
+ return {
11
+ add(file, name, targetFile) {
12
+ if (!map.has(file)) {
13
+ map.set(file, new Map());
14
+ }
15
+ map.get(file).set(name, targetFile);
16
+ },
17
+ lookup(file, name) {
18
+ return map.get(file)?.get(name) ?? null;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Import resolver dispatcher.
3
+ *
4
+ * Routes import resolution to language-specific resolvers and
5
+ * falls back to tsconfig aliases for TypeScript/JavaScript.
6
+ */
7
+ import { loadTsconfigAliases } from './languages/typescript';
8
+ /**
9
+ * Resolve an import from one file to another.
10
+ *
11
+ * @param fromAbsFile - Absolute path of the importing file
12
+ * @param modulePath - The import specifier (e.g., './auth', 'express', '@/lib/db')
13
+ * @param lang - Language key (ts, javascript, typescript, python, ruby, etc.)
14
+ * @param repoRoot - Absolute path to the repository root
15
+ * @param tsconfigAliases - Optional pre-loaded tsconfig aliases for TS/JS
16
+ * @returns Absolute path to the resolved file, or null if unresolvable
17
+ */
18
+ export declare function resolveImport(fromAbsFile: string, modulePath: string, lang: string, repoRoot: string, tsconfigAliases?: Map<string, string[]>): string | null;
19
+ export { loadTsconfigAliases };
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Import resolver dispatcher.
3
+ *
4
+ * Routes import resolution to language-specific resolvers and
5
+ * falls back to tsconfig aliases for TypeScript/JavaScript.
6
+ */
7
+ import { readdirSync, readFileSync } from 'fs';
8
+ import { join, resolve as resolvePath } from 'path';
9
+ import { log } from '../shared/logger';
10
+ import { ensureWithinRoot } from '../shared/safe-path';
11
+ import { detectExternal } from './external-detector';
12
+ import { cachedExists } from './fs-cache';
13
+ import { resolve as resolveCsImport } from './languages/csharp';
14
+ import { resolve as resolveGoImport } from './languages/go';
15
+ import { resolve as resolveJavaImport } from './languages/java';
16
+ import { resolve as resolvePhpImport } from './languages/php';
17
+ import { resolve as resolvePyImport } from './languages/python';
18
+ import { resolve as resolveRbImport } from './languages/ruby';
19
+ import { resolve as resolveRustImport } from './languages/rust';
20
+ import { loadBundlerAliases, loadTsconfigAliases, resolve as resolveTsImport, resolveWithAliases, } from './languages/typescript';
21
+ /**
22
+ * Registered import resolvers by language key.
23
+ *
24
+ * IMPORTANT: When adding a new language, you MUST:
25
+ * 1. Create a resolver in src/resolver/languages/<lang>.ts
26
+ * 2. Add it to this map
27
+ * 3. Add tests in tests/resolver/<lang>.test.ts
28
+ * 4. Add external detection in src/resolver/external-detector.ts
29
+ *
30
+ * If a language key from the parser is not in this map, resolveImport()
31
+ * will log a warning and return null. This is intentional — silent
32
+ * failures that default to another language's resolver cause wrong results.
33
+ */
34
+ const RESOLVERS = {
35
+ ts: resolveTsImport,
36
+ javascript: resolveTsImport,
37
+ typescript: resolveTsImport,
38
+ python: resolvePyImport,
39
+ ruby: resolveRbImport,
40
+ go: resolveGoImport,
41
+ java: resolveJavaImport,
42
+ rust: resolveRustImport,
43
+ csharp: resolveCsImport,
44
+ php: resolvePhpImport,
45
+ };
46
+ /**
47
+ * Resolve package.json #imports (Node.js subpath imports).
48
+ * Handles both exact matches and wildcard patterns.
49
+ */
50
+ function resolveHashImport(modulePath, repoRoot) {
51
+ const pkgPath = join(repoRoot, 'package.json');
52
+ if (!cachedExists(pkgPath)) {
53
+ return null;
54
+ }
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
57
+ const imports = pkg?.imports;
58
+ if (!imports) {
59
+ return null;
60
+ }
61
+ for (const [pattern, target] of Object.entries(imports)) {
62
+ if (typeof target !== 'string') {
63
+ continue;
64
+ }
65
+ if (pattern === modulePath) {
66
+ // Exact match: "#utils" -> "./src/shared/utils.ts"
67
+ const resolved = resolvePath(repoRoot, target);
68
+ if (cachedExists(resolved)) {
69
+ return resolved;
70
+ }
71
+ }
72
+ // Wildcard match: "#db/*" -> "./src/db/*.ts"
73
+ if (pattern.includes('*')) {
74
+ const prefix = pattern.split('*')[0]; // "#db/"
75
+ if (modulePath.startsWith(prefix)) {
76
+ const rest = modulePath.slice(prefix.length); // "connection"
77
+ const resolved = resolvePath(repoRoot, target.replace('*', rest));
78
+ if (cachedExists(resolved)) {
79
+ return resolved;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // ignore parse errors
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * Resolve a conditional export value to a single string path.
92
+ * When the value is a plain string, return it directly.
93
+ * When it's an object with condition keys, prefer: types > import > default > first value.
94
+ */
95
+ function resolveExportValue(value) {
96
+ if (typeof value === 'string') {
97
+ return value;
98
+ }
99
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
100
+ const obj = value;
101
+ for (const key of ['types', 'import', 'default']) {
102
+ if (typeof obj[key] === 'string') {
103
+ return obj[key];
104
+ }
105
+ }
106
+ // Fallback: first value that is a string
107
+ for (const v of Object.values(obj)) {
108
+ if (typeof v === 'string') {
109
+ return v;
110
+ }
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Resolve monorepo workspace package exports.
117
+ * Scans workspace directories to find packages matching the import specifier.
118
+ */
119
+ function resolveWorkspaceExport(modulePath, repoRoot) {
120
+ const rootPkgPath = join(repoRoot, 'package.json');
121
+ if (!cachedExists(rootPkgPath)) {
122
+ return null;
123
+ }
124
+ try {
125
+ const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
126
+ let workspaceGlobs;
127
+ if (Array.isArray(rootPkg?.workspaces)) {
128
+ workspaceGlobs = rootPkg.workspaces;
129
+ }
130
+ else if (rootPkg?.workspaces?.packages && Array.isArray(rootPkg.workspaces.packages)) {
131
+ workspaceGlobs = rootPkg.workspaces.packages;
132
+ }
133
+ if (!workspaceGlobs) {
134
+ return null;
135
+ }
136
+ // Collect all workspace package directories
137
+ const pkgDirs = [];
138
+ for (const ws of workspaceGlobs) {
139
+ if (ws.endsWith('/*')) {
140
+ // Glob pattern like "packages/*"
141
+ const parentDir = join(repoRoot, ws.slice(0, -2));
142
+ if (cachedExists(parentDir)) {
143
+ const entries = readdirSync(parentDir, { withFileTypes: true });
144
+ for (const entry of entries) {
145
+ if (entry.isDirectory()) {
146
+ pkgDirs.push(join(parentDir, entry.name));
147
+ }
148
+ }
149
+ }
150
+ }
151
+ else {
152
+ pkgDirs.push(join(repoRoot, ws));
153
+ }
154
+ }
155
+ // Search each workspace package for a matching name + exports/main/module
156
+ for (const pkgDir of pkgDirs) {
157
+ const pkgJsonPath = join(pkgDir, 'package.json');
158
+ if (!cachedExists(pkgJsonPath)) {
159
+ continue;
160
+ }
161
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
162
+ const pkgName = pkg?.name;
163
+ if (!pkgName) {
164
+ continue;
165
+ }
166
+ const exports = pkg?.exports;
167
+ // Check if modulePath matches this package (exact or subpath)
168
+ if (modulePath === pkgName) {
169
+ if (exports && typeof exports === 'object') {
170
+ // Root export: "." entry
171
+ const target = resolveExportValue(exports['.']);
172
+ if (target) {
173
+ const resolved = resolvePath(pkgDir, target);
174
+ if (cachedExists(resolved)) {
175
+ return resolved;
176
+ }
177
+ }
178
+ }
179
+ else if (!exports) {
180
+ // Fallback to main or module fields
181
+ const fallback = pkg.main ?? pkg.module;
182
+ if (typeof fallback === 'string') {
183
+ const resolved = resolvePath(pkgDir, fallback);
184
+ if (cachedExists(resolved)) {
185
+ return resolved;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ else if (modulePath.startsWith(`${pkgName}/`)) {
191
+ const subpath = modulePath.slice(pkgName.length + 1);
192
+ // 1. Try exports field first
193
+ if (exports && typeof exports === 'object') {
194
+ const exportKey = `./${subpath}`;
195
+ const target = resolveExportValue(exports[exportKey]);
196
+ if (target) {
197
+ const resolved = resolvePath(pkgDir, target);
198
+ if (cachedExists(resolved)) {
199
+ return resolved;
200
+ }
201
+ }
202
+ }
203
+ // 2. No exports or no match? Resolve subpath directly in package directory
204
+ const directBase = join(pkgDir, subpath);
205
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
206
+ if (cachedExists(directBase + ext)) {
207
+ return resolvePath(directBase + ext);
208
+ }
209
+ }
210
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
211
+ const idx = join(directBase, `index${ext}`);
212
+ if (cachedExists(idx)) {
213
+ return resolvePath(idx);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ catch {
220
+ // ignore parse errors
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * Resolve an import from one file to another.
226
+ *
227
+ * @param fromAbsFile - Absolute path of the importing file
228
+ * @param modulePath - The import specifier (e.g., './auth', 'express', '@/lib/db')
229
+ * @param lang - Language key (ts, javascript, typescript, python, ruby, etc.)
230
+ * @param repoRoot - Absolute path to the repository root
231
+ * @param tsconfigAliases - Optional pre-loaded tsconfig aliases for TS/JS
232
+ * @returns Absolute path to the resolved file, or null if unresolvable
233
+ */
234
+ export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigAliases) {
235
+ const resolver = RESOLVERS[lang];
236
+ if (!resolver) {
237
+ log.warn('No import resolver registered for language', { lang, module: modulePath, from: fromAbsFile });
238
+ return null;
239
+ }
240
+ // Strip webpack/rollup loader syntax: !!loader1!loader2!actual/path
241
+ // The actual import path is always the last segment after the final '!'
242
+ if (modulePath.includes('!')) {
243
+ modulePath = modulePath.split('!').pop() || modulePath;
244
+ }
245
+ // TS/JS-specific fallbacks: tsconfig aliases, bundler aliases, #imports, workspace exports.
246
+ // These are Node.js/npm ecosystem features that don't apply to other languages.
247
+ // Other languages handle their own workspace/monorepo patterns inside their resolvers
248
+ // (Go: go.work, Rust: Cargo workspace, Java: Maven/Gradle modules).
249
+ const isTsOrJs = lang === 'ts' || lang === 'javascript' || lang === 'typescript';
250
+ // Handle package.json #imports (TS/JS only)
251
+ if (isTsOrJs && modulePath.startsWith('#')) {
252
+ const result = resolveHashImport(modulePath, repoRoot);
253
+ if (result) {
254
+ try {
255
+ ensureWithinRoot(result, repoRoot);
256
+ return result;
257
+ }
258
+ catch {
259
+ log.warn('Import resolves outside repository root', {
260
+ from: fromAbsFile,
261
+ module: modulePath,
262
+ resolved: result,
263
+ });
264
+ return null;
265
+ }
266
+ }
267
+ }
268
+ let result = resolver(fromAbsFile, modulePath, repoRoot);
269
+ // Fallback: tsconfig aliases for TS/JS
270
+ if (!result && isTsOrJs && tsconfigAliases?.size) {
271
+ result = resolveWithAliases(modulePath, tsconfigAliases, repoRoot);
272
+ }
273
+ // Fallback: bundler aliases (webpack/vite) for TS/JS bare specifiers
274
+ if (!result && isTsOrJs && !modulePath.startsWith('.')) {
275
+ const bundlerAliases = loadBundlerAliases(repoRoot);
276
+ if (bundlerAliases.size > 0) {
277
+ result = resolveWithAliases(modulePath, bundlerAliases, repoRoot);
278
+ }
279
+ }
280
+ // Fallback: monorepo workspace exports for TS/JS bare specifiers
281
+ if (!result && isTsOrJs && !modulePath.startsWith('.')) {
282
+ result = resolveWorkspaceExport(modulePath, repoRoot);
283
+ }
284
+ // Validate resolved path is within repo root
285
+ if (result) {
286
+ try {
287
+ ensureWithinRoot(result, repoRoot);
288
+ }
289
+ catch {
290
+ log.warn('Import resolves outside repository root', {
291
+ from: fromAbsFile,
292
+ module: modulePath,
293
+ resolved: result,
294
+ });
295
+ return null;
296
+ }
297
+ }
298
+ // If still unresolved, check if it's an external package (for logging/debugging)
299
+ if (!result) {
300
+ const externalPkg = detectExternal(modulePath, lang, repoRoot);
301
+ if (externalPkg) {
302
+ // External package — expected null, don't log as warning
303
+ return null;
304
+ }
305
+ // Truly unresolved local import
306
+ log.debug('Unresolved local import', { from: fromAbsFile, module: modulePath });
307
+ }
308
+ return result;
309
+ }
310
+ export { loadTsconfigAliases };
@@ -0,0 +1,3 @@
1
+ /** Clear cached .sln data. Call between analysis runs or when switching repos. */
2
+ export declare function clearCache(): void;
3
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,94 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs';
2
+ import { dirname, join, resolve as resolvePath } from 'path';
3
+ import { cachedExists } from '../fs-cache';
4
+ const STDLIB_PREFIXES = ['System.', 'System', 'Microsoft.', 'Newtonsoft.'];
5
+ const slnProjectsCache = new Map();
6
+ /** Clear cached .sln data. Call between analysis runs or when switching repos. */
7
+ export function clearCache() {
8
+ slnProjectsCache.clear();
9
+ }
10
+ /**
11
+ * Parse .sln files at repoRoot to discover project directories.
12
+ * Lines like: Project("{FAE04EC0}") = "Name", "path/to/Project.csproj", "{GUID}"
13
+ */
14
+ function getSlnProjectDirs(repoRoot) {
15
+ const cached = slnProjectsCache.get(repoRoot);
16
+ if (cached) {
17
+ return cached;
18
+ }
19
+ const dirs = [];
20
+ try {
21
+ const entries = readdirSync(repoRoot);
22
+ for (const entry of entries) {
23
+ if (entry.endsWith('.sln')) {
24
+ const slnPath = join(repoRoot, entry);
25
+ try {
26
+ const content = readFileSync(slnPath, 'utf-8');
27
+ const projectRe = /^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/gm;
28
+ let m = projectRe.exec(content);
29
+ while (m !== null) {
30
+ const csprojRelPath = m[1].replace(/\\/g, '/');
31
+ const projectDir = dirname(join(repoRoot, csprojRelPath));
32
+ if (cachedExists(projectDir)) {
33
+ dirs.push(projectDir);
34
+ }
35
+ m = projectRe.exec(content);
36
+ }
37
+ }
38
+ catch {
39
+ /* ignore unreadable sln */
40
+ }
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ /* ignore */
46
+ }
47
+ slnProjectsCache.set(repoRoot, dirs);
48
+ return dirs;
49
+ }
50
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
51
+ if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
52
+ return null;
53
+ }
54
+ const segments = modulePath.split('.');
55
+ // Collect search base directories: standard ones + .sln-discovered project dirs
56
+ const standardBases = ['', 'src', 'lib', 'Source'];
57
+ const slnDirs = getSlnProjectDirs(repoRoot);
58
+ // Try resolving as a .cs file first
59
+ for (let i = segments.length - 1; i >= 0; i--) {
60
+ const pathPart = segments.slice(i).join('/');
61
+ const candidate = `${pathPart}.cs`;
62
+ for (const base of standardBases) {
63
+ const full = join(repoRoot, base, candidate);
64
+ if (cachedExists(full)) {
65
+ return resolvePath(full);
66
+ }
67
+ }
68
+ // Also search in .sln-discovered project directories
69
+ for (const projDir of slnDirs) {
70
+ const full = join(projDir, candidate);
71
+ if (cachedExists(full)) {
72
+ return resolvePath(full);
73
+ }
74
+ }
75
+ }
76
+ // Try resolving as a directory (namespace → folder mapping)
77
+ for (let i = segments.length - 1; i >= 0; i--) {
78
+ const pathPart = segments.slice(i).join('/');
79
+ for (const base of standardBases) {
80
+ const full = join(repoRoot, base, pathPart);
81
+ if (cachedExists(full) && statSync(full).isDirectory()) {
82
+ return resolvePath(full);
83
+ }
84
+ }
85
+ // Also search in .sln-discovered project directories
86
+ for (const projDir of slnDirs) {
87
+ const full = join(projDir, pathPart);
88
+ if (cachedExists(full) && statSync(full).isDirectory()) {
89
+ return resolvePath(full);
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
@@ -0,0 +1,3 @@
1
+ /** Clear cached go.mod data. Call between analysis runs or when switching repos. */
2
+ export declare function clearCache(): void;
3
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;