@kodus/kodus-graph 0.2.7 → 0.2.9

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 (167) 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 +57 -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 +83 -0
  9. package/dist/analysis/diff.d.ts +35 -0
  10. package/dist/analysis/diff.js +140 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +98 -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 +166 -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 +208 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +114 -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 +10 -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 +2 -0
  44. package/dist/graph/builder.js +216 -0
  45. package/dist/graph/edges.d.ts +19 -0
  46. package/dist/graph/edges.js +105 -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 +249 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +4 -0
  56. package/dist/parser/batch.js +78 -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 +303 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/import-map.d.ts +12 -0
  74. package/dist/resolver/import-map.js +21 -0
  75. package/dist/resolver/import-resolver.d.ts +19 -0
  76. package/dist/resolver/import-resolver.js +212 -0
  77. package/dist/resolver/languages/csharp.d.ts +1 -0
  78. package/dist/resolver/languages/csharp.js +31 -0
  79. package/dist/resolver/languages/go.d.ts +3 -0
  80. package/dist/resolver/languages/go.js +196 -0
  81. package/dist/resolver/languages/java.d.ts +1 -0
  82. package/dist/resolver/languages/java.js +108 -0
  83. package/dist/resolver/languages/php.d.ts +3 -0
  84. package/dist/resolver/languages/php.js +54 -0
  85. package/dist/resolver/languages/python.d.ts +11 -0
  86. package/dist/resolver/languages/python.js +51 -0
  87. package/dist/resolver/languages/ruby.d.ts +9 -0
  88. package/dist/resolver/languages/ruby.js +59 -0
  89. package/dist/resolver/languages/rust.d.ts +1 -0
  90. package/dist/resolver/languages/rust.js +196 -0
  91. package/dist/resolver/languages/typescript.d.ts +27 -0
  92. package/dist/resolver/languages/typescript.js +240 -0
  93. package/dist/resolver/re-export-resolver.d.ts +24 -0
  94. package/dist/resolver/re-export-resolver.js +57 -0
  95. package/dist/resolver/symbol-table.d.ts +17 -0
  96. package/dist/resolver/symbol-table.js +60 -0
  97. package/dist/shared/extract-calls.d.ts +26 -0
  98. package/dist/shared/extract-calls.js +57 -0
  99. package/dist/shared/file-hash.d.ts +3 -0
  100. package/dist/shared/file-hash.js +10 -0
  101. package/dist/shared/filters.d.ts +3 -0
  102. package/dist/shared/filters.js +240 -0
  103. package/dist/shared/logger.d.ts +6 -0
  104. package/dist/shared/logger.js +17 -0
  105. package/dist/shared/qualified-name.d.ts +1 -0
  106. package/dist/shared/qualified-name.js +9 -0
  107. package/dist/shared/safe-path.d.ts +6 -0
  108. package/dist/shared/safe-path.js +29 -0
  109. package/dist/shared/schemas.d.ts +43 -0
  110. package/dist/shared/schemas.js +30 -0
  111. package/dist/shared/temp.d.ts +11 -0
  112. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  113. package/package.json +20 -6
  114. package/src/analysis/blast-radius.ts +0 -54
  115. package/src/analysis/communities.ts +0 -135
  116. package/src/analysis/context-builder.ts +0 -130
  117. package/src/analysis/diff.ts +0 -131
  118. package/src/analysis/enrich.ts +0 -110
  119. package/src/analysis/flows.ts +0 -112
  120. package/src/analysis/inheritance.ts +0 -34
  121. package/src/analysis/prompt-formatter.ts +0 -175
  122. package/src/analysis/risk-score.ts +0 -62
  123. package/src/analysis/search.ts +0 -76
  124. package/src/analysis/test-gaps.ts +0 -21
  125. package/src/cli.ts +0 -207
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -139
  129. package/src/commands/diff.ts +0 -96
  130. package/src/commands/flows.ts +0 -19
  131. package/src/commands/parse.ts +0 -124
  132. package/src/commands/search.ts +0 -41
  133. package/src/commands/update.ts +0 -166
  134. package/src/graph/builder.ts +0 -209
  135. package/src/graph/edges.ts +0 -101
  136. package/src/graph/json-writer.ts +0 -43
  137. package/src/graph/loader.ts +0 -113
  138. package/src/graph/merger.ts +0 -25
  139. package/src/graph/types.ts +0 -283
  140. package/src/parser/batch.ts +0 -82
  141. package/src/parser/discovery.ts +0 -75
  142. package/src/parser/extractor.ts +0 -37
  143. package/src/parser/extractors/generic.ts +0 -132
  144. package/src/parser/extractors/python.ts +0 -133
  145. package/src/parser/extractors/ruby.ts +0 -147
  146. package/src/parser/extractors/typescript.ts +0 -350
  147. package/src/parser/languages.ts +0 -122
  148. package/src/resolver/call-resolver.ts +0 -244
  149. package/src/resolver/import-map.ts +0 -27
  150. package/src/resolver/import-resolver.ts +0 -72
  151. package/src/resolver/languages/csharp.ts +0 -7
  152. package/src/resolver/languages/go.ts +0 -7
  153. package/src/resolver/languages/java.ts +0 -7
  154. package/src/resolver/languages/php.ts +0 -7
  155. package/src/resolver/languages/python.ts +0 -35
  156. package/src/resolver/languages/ruby.ts +0 -21
  157. package/src/resolver/languages/rust.ts +0 -7
  158. package/src/resolver/languages/typescript.ts +0 -168
  159. package/src/resolver/re-export-resolver.ts +0 -66
  160. package/src/resolver/symbol-table.ts +0 -67
  161. package/src/shared/extract-calls.ts +0 -75
  162. package/src/shared/file-hash.ts +0 -12
  163. package/src/shared/filters.ts +0 -243
  164. package/src/shared/logger.ts +0 -14
  165. package/src/shared/qualified-name.ts +0 -5
  166. package/src/shared/safe-path.ts +0 -31
  167. package/src/shared/schemas.ts +0 -32
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Ruby import resolver.
3
+ *
4
+ * Handles require_relative paths and Gemfile path: gems.
5
+ */
6
+ /**
7
+ * Resolve a Ruby require/require_relative to a file path.
8
+ */
9
+ export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ruby import resolver.
3
+ *
4
+ * Handles require_relative paths and Gemfile path: gems.
5
+ */
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { dirname, join, resolve as resolvePath } from 'path';
8
+ /** Cache parsed Gemfile path gems per repo root. */
9
+ const gemfileCache = new Map();
10
+ /**
11
+ * Parse Gemfile for path: gems and return their lib directories (absolute).
12
+ */
13
+ function getGemPathLibDirs(repoRoot) {
14
+ if (gemfileCache.has(repoRoot)) {
15
+ return gemfileCache.get(repoRoot);
16
+ }
17
+ const gemfilePath = join(repoRoot, 'Gemfile');
18
+ const libDirs = [];
19
+ if (existsSync(gemfilePath)) {
20
+ const content = readFileSync(gemfilePath, 'utf-8');
21
+ // Match lines like: gem 'mylib', path: './libs/mylib'
22
+ const regex = /^\s*gem\s+['"][^'"]+['"]\s*,\s*path:\s*['"]([^'"]+)['"]/gm;
23
+ let match = regex.exec(content);
24
+ while (match !== null) {
25
+ const gemPath = match[1];
26
+ libDirs.push(resolvePath(join(repoRoot, gemPath, 'lib')));
27
+ match = regex.exec(content);
28
+ }
29
+ }
30
+ gemfileCache.set(repoRoot, libDirs);
31
+ return libDirs;
32
+ }
33
+ /**
34
+ * Resolve a Ruby require/require_relative to a file path.
35
+ */
36
+ export function resolve(fromAbsFile, modulePath, repoRoot) {
37
+ if (!modulePath) {
38
+ return null;
39
+ }
40
+ // 1. Try relative resolution (require_relative style)
41
+ const base = join(dirname(fromAbsFile), modulePath);
42
+ if (existsSync(`${base}.rb`)) {
43
+ return resolvePath(`${base}.rb`);
44
+ }
45
+ if (existsSync(base)) {
46
+ return resolvePath(base);
47
+ }
48
+ // 2. Try Gemfile path: gems
49
+ for (const libDir of getGemPathLibDirs(repoRoot)) {
50
+ const candidate = join(libDir, modulePath);
51
+ if (existsSync(`${candidate}.rb`)) {
52
+ return resolvePath(`${candidate}.rb`);
53
+ }
54
+ if (existsSync(candidate)) {
55
+ return resolvePath(candidate);
56
+ }
57
+ }
58
+ return null;
59
+ }
@@ -0,0 +1 @@
1
+ export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,196 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { basename, dirname, join, resolve as resolvePath } from 'path';
3
+ function probeRustPath(baseDir, relPath) {
4
+ const asFile = join(baseDir, `${relPath}.rs`);
5
+ if (existsSync(asFile)) {
6
+ return resolvePath(asFile);
7
+ }
8
+ const asMod = join(baseDir, relPath, 'mod.rs');
9
+ if (existsSync(asMod)) {
10
+ return resolvePath(asMod);
11
+ }
12
+ const asLib = join(baseDir, relPath, 'lib.rs');
13
+ if (existsSync(asLib)) {
14
+ return resolvePath(asLib);
15
+ }
16
+ return null;
17
+ }
18
+ export function resolve(fromAbsFile, modulePath, repoRoot) {
19
+ if (modulePath.startsWith('std::')) {
20
+ return null;
21
+ }
22
+ if (modulePath.startsWith('crate::')) {
23
+ const rest = modulePath.slice('crate::'.length).replace(/::/g, '/');
24
+ return probeRustPath(join(repoRoot, 'src'), rest);
25
+ }
26
+ if (modulePath.startsWith('super::')) {
27
+ const rest = modulePath.slice('super::'.length).replace(/::/g, '/');
28
+ const fileName = basename(fromAbsFile);
29
+ // mod.rs and lib.rs represent their parent directory, so super:: goes up two levels
30
+ const parentDir = fileName === 'mod.rs' || fileName === 'lib.rs' ? dirname(dirname(fromAbsFile)) : dirname(fromAbsFile);
31
+ return probeRustPath(parentDir, rest);
32
+ }
33
+ if (modulePath.startsWith('self::')) {
34
+ const rest = modulePath.slice('self::'.length).replace(/::/g, '/');
35
+ return probeRustPath(dirname(fromAbsFile), rest);
36
+ }
37
+ // Try workspace path dependency resolution
38
+ const firstSep = modulePath.indexOf('::');
39
+ if (firstSep !== -1) {
40
+ const crateName = modulePath.slice(0, firstSep);
41
+ const rest = modulePath.slice(firstSep + 2).replace(/::/g, '/');
42
+ const depPath = resolveWorkspacePathDep(fromAbsFile, crateName);
43
+ if (depPath) {
44
+ const srcDir = join(depPath, 'src');
45
+ // Try the full path first, then progressively strip trailing segments
46
+ // (they may be items like functions/structs inside a module file)
47
+ const segments = rest.split('/');
48
+ for (let i = segments.length; i >= 1; i--) {
49
+ const partial = segments.slice(0, i).join('/');
50
+ const result = probeRustPath(srcDir, partial);
51
+ if (result) {
52
+ return result;
53
+ }
54
+ }
55
+ }
56
+ // bin+lib same crate: if the first segment matches the local [package] name,
57
+ // treat it like a crate:: import (resolve from src/)
58
+ const localPkgName = findLocalPackageName(fromAbsFile);
59
+ if (localPkgName && crateName === localPkgName) {
60
+ const srcDir = join(findCrateDir(fromAbsFile), 'src');
61
+ const segments = rest.split('/');
62
+ for (let i = segments.length; i >= 1; i--) {
63
+ const partial = segments.slice(0, i).join('/');
64
+ const result = probeRustPath(srcDir, partial);
65
+ if (result) {
66
+ return result;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ /** Cache: crate dir → parsed {depName → resolved absolute path} */
74
+ const pathDepCache = new Map();
75
+ /**
76
+ * Walk up from `fromAbsFile` to find the nearest Cargo.toml,
77
+ * parse its [dependencies] for `path = "..."` entries,
78
+ * and return the absolute path of the dependency crate if it matches `depName`.
79
+ */
80
+ function resolveWorkspacePathDep(fromAbsFile, depName) {
81
+ const crateDir = findCrateDir(fromAbsFile);
82
+ if (!crateDir) {
83
+ return null;
84
+ }
85
+ let deps = pathDepCache.get(crateDir);
86
+ if (!deps) {
87
+ deps = parsePathDeps(crateDir);
88
+ pathDepCache.set(crateDir, deps);
89
+ }
90
+ return deps.get(depName) ?? null;
91
+ }
92
+ /**
93
+ * Walk up from a file to find the nearest directory containing Cargo.toml.
94
+ */
95
+ function findCrateDir(fromAbsFile) {
96
+ let dir = dirname(fromAbsFile);
97
+ const root = resolvePath('/');
98
+ while (dir !== root) {
99
+ if (existsSync(join(dir, 'Cargo.toml'))) {
100
+ return dir;
101
+ }
102
+ const parent = dirname(dir);
103
+ if (parent === dir) {
104
+ break;
105
+ }
106
+ dir = parent;
107
+ }
108
+ return null;
109
+ }
110
+ /** Cache: crate dir → parsed package name */
111
+ const pkgNameCache = new Map();
112
+ /**
113
+ * Find the [package] name from the nearest Cargo.toml for the given file.
114
+ * Used to detect bin+lib same-crate imports (e.g. `use myapp::foo` from main.rs).
115
+ */
116
+ function findLocalPackageName(fromAbsFile) {
117
+ const crateDir = findCrateDir(fromAbsFile);
118
+ if (!crateDir) {
119
+ return null;
120
+ }
121
+ const cached = pkgNameCache.get(crateDir);
122
+ if (cached !== undefined) {
123
+ return cached;
124
+ }
125
+ const cargoPath = join(crateDir, 'Cargo.toml');
126
+ if (!existsSync(cargoPath)) {
127
+ pkgNameCache.set(crateDir, null);
128
+ return null;
129
+ }
130
+ const content = readFileSync(cargoPath, 'utf-8');
131
+ const match = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
132
+ // Cargo crate names use hyphens but Rust imports use underscores
133
+ const name = match ? match[1].replace(/-/g, '_') : null;
134
+ pkgNameCache.set(crateDir, name);
135
+ return name;
136
+ }
137
+ /**
138
+ * Parse Cargo.toml in `crateDir` for path dependencies.
139
+ * Returns a map of dependency name → resolved absolute directory.
140
+ *
141
+ * Handles both inline table and multi-line table forms:
142
+ * shared = { path = "../shared" }
143
+ * [dependencies.shared]
144
+ * path = "../shared"
145
+ */
146
+ function parsePathDeps(crateDir) {
147
+ const result = new Map();
148
+ const cargoPath = join(crateDir, 'Cargo.toml');
149
+ if (!existsSync(cargoPath)) {
150
+ return result;
151
+ }
152
+ const content = readFileSync(cargoPath, 'utf-8');
153
+ const lines = content.split('\n');
154
+ let inDepsSection = false;
155
+ let depsTableDep = null; // for [dependencies.foo] style
156
+ for (const line of lines) {
157
+ const trimmed = line.trim();
158
+ // Detect section headers
159
+ if (trimmed.startsWith('[')) {
160
+ // Check for [dependencies.foo] form
161
+ const subMatch = trimmed.match(/^\[dependencies\.(\S+)\]$/);
162
+ if (subMatch) {
163
+ depsTableDep = subMatch[1];
164
+ inDepsSection = false;
165
+ continue;
166
+ }
167
+ depsTableDep = null;
168
+ if (trimmed === '[dependencies]') {
169
+ inDepsSection = true;
170
+ continue;
171
+ }
172
+ // Any other section header ends [dependencies]
173
+ inDepsSection = false;
174
+ continue;
175
+ }
176
+ // Inside [dependencies.foo], look for path = "..."
177
+ if (depsTableDep) {
178
+ const pathMatch = trimmed.match(/^path\s*=\s*"([^"]+)"/);
179
+ if (pathMatch) {
180
+ const resolved = resolvePath(crateDir, pathMatch[1]);
181
+ result.set(depsTableDep, resolved);
182
+ }
183
+ continue;
184
+ }
185
+ // Inside [dependencies], look for inline table with path
186
+ if (inDepsSection && trimmed.length > 0 && !trimmed.startsWith('#')) {
187
+ // name = { path = "..." ... }
188
+ const inlineMatch = trimmed.match(/^(\S+)\s*=\s*\{[^}]*path\s*=\s*"([^"]+)"[^}]*\}/);
189
+ if (inlineMatch) {
190
+ const resolved = resolvePath(crateDir, inlineMatch[2]);
191
+ result.set(inlineMatch[1], resolved);
192
+ }
193
+ }
194
+ }
195
+ return result;
196
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * TypeScript/JavaScript import resolver.
3
+ *
4
+ * Handles:
5
+ * - Relative imports with extension probing (.ts, .tsx, .js, .jsx)
6
+ * - ESM .js → .ts remapping
7
+ * - Directory index files
8
+ * - tsconfig path aliases
9
+ */
10
+ /**
11
+ * Resolve a TypeScript/JavaScript relative import to an absolute file path.
12
+ * Returns null for non-relative (external package) imports.
13
+ */
14
+ export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
15
+ /**
16
+ * Load and parse tsconfig.json path aliases.
17
+ *
18
+ * Tries tsconfig.json first, then tsconfig.base.json.
19
+ * Converts alias patterns like "@libs/*" into prefix → resolved dirs.
20
+ */
21
+ export declare function loadTsconfigAliases(repoRoot: string): Map<string, string[]>;
22
+ /**
23
+ * Resolve an import path using tsconfig aliases.
24
+ *
25
+ * Tries each alias prefix, and for matches, probes extensions and index files.
26
+ */
27
+ export declare function resolveWithAliases(modulePath: string, aliases: Map<string, string[]>, _repoRoot: string): string | null;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * TypeScript/JavaScript import resolver.
3
+ *
4
+ * Handles:
5
+ * - Relative imports with extension probing (.ts, .tsx, .js, .jsx)
6
+ * - ESM .js → .ts remapping
7
+ * - Directory index files
8
+ * - tsconfig path aliases
9
+ */
10
+ import { existsSync, readFileSync } from 'fs';
11
+ import { dirname, join, resolve as resolvePath } from 'path';
12
+ import { log } from '../../shared/logger';
13
+ const TS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
14
+ /**
15
+ * Probe a base path for TS/JS files: try extensions, then index files.
16
+ * Returns the resolved absolute path or null.
17
+ */
18
+ function probeExtensions(base) {
19
+ for (const ext of TS_EXTENSIONS) {
20
+ const candidate = base + ext;
21
+ if (existsSync(candidate)) {
22
+ return resolvePath(candidate);
23
+ }
24
+ }
25
+ for (const ext of TS_EXTENSIONS) {
26
+ const candidate = join(base, `index${ext}`);
27
+ if (existsSync(candidate)) {
28
+ return resolvePath(candidate);
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ /** Cache for parsed tsconfig.json (keyed by repoRoot). */
34
+ const tsconfigCache = new Map();
35
+ function loadTsconfigCompilerOptions(repoRoot) {
36
+ const cached = tsconfigCache.get(repoRoot);
37
+ if (cached !== undefined) {
38
+ return cached;
39
+ }
40
+ const tsconfigPath = join(repoRoot, 'tsconfig.json');
41
+ let result = {};
42
+ if (existsSync(tsconfigPath)) {
43
+ try {
44
+ const content = readFileSync(tsconfigPath, 'utf-8');
45
+ const cleaned = stripJsonComments(content);
46
+ const config = JSON.parse(cleaned);
47
+ const rootDirs = config?.compilerOptions?.rootDirs;
48
+ if (Array.isArray(rootDirs)) {
49
+ result = { rootDirs };
50
+ }
51
+ }
52
+ catch {
53
+ // ignore parse errors
54
+ }
55
+ }
56
+ tsconfigCache.set(repoRoot, result);
57
+ return result;
58
+ }
59
+ /**
60
+ * Resolve a TypeScript/JavaScript relative import to an absolute file path.
61
+ * Returns null for non-relative (external package) imports.
62
+ */
63
+ export function resolve(fromAbsFile, modulePath, repoRoot) {
64
+ // Strip Vite-style query suffixes (?raw, ?url, ?worker, etc.)
65
+ const queryIdx = modulePath.indexOf('?');
66
+ if (queryIdx !== -1) {
67
+ modulePath = modulePath.slice(0, queryIdx);
68
+ }
69
+ if (!modulePath.startsWith('.')) {
70
+ return null;
71
+ }
72
+ let base = join(dirname(fromAbsFile), modulePath);
73
+ // If the path has a non-TS/JS extension (e.g. .txt, .svg), try exact match
74
+ if (/\.\w+$/.test(modulePath) && !TS_EXTENSIONS.some((ext) => modulePath.endsWith(ext))) {
75
+ if (existsSync(base)) {
76
+ return resolvePath(base);
77
+ }
78
+ }
79
+ // ESM convention: .js in import -> .ts on disk
80
+ if (modulePath.endsWith('.js')) {
81
+ base = base.slice(0, -3);
82
+ }
83
+ const direct = probeExtensions(base);
84
+ if (direct) {
85
+ return direct;
86
+ }
87
+ // rootDirs fallback: try the same relative import from other root directories
88
+ const { rootDirs } = loadTsconfigCompilerOptions(repoRoot);
89
+ if (rootDirs && rootDirs.length > 0) {
90
+ const fromDir = dirname(fromAbsFile);
91
+ for (const rd of rootDirs) {
92
+ const absRd = resolvePath(repoRoot, rd);
93
+ // Check if fromDir is inside this rootDir
94
+ if (fromDir.startsWith(`${absRd}/`) || fromDir === absRd) {
95
+ const relFromRoot = fromDir.slice(absRd.length); // e.g. "" or "/sub"
96
+ // Try the same relative path under each other rootDir
97
+ for (const otherRd of rootDirs) {
98
+ if (otherRd === rd) {
99
+ continue;
100
+ }
101
+ const absOtherRd = resolvePath(repoRoot, otherRd);
102
+ const relModule = modulePath.startsWith('./') ? modulePath.slice(2) : modulePath;
103
+ let otherBase = join(absOtherRd, relFromRoot, relModule);
104
+ if (modulePath.endsWith('.js')) {
105
+ otherBase = otherBase.slice(0, -3);
106
+ }
107
+ const result = probeExtensions(otherBase);
108
+ if (result) {
109
+ return result;
110
+ }
111
+ }
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * Strip comments and trailing commas from JSON (tsconfig-compatible).
120
+ * Handles strings correctly -- won't strip // inside "url://..." etc.
121
+ */
122
+ function stripJsonComments(str) {
123
+ let result = '';
124
+ let i = 0;
125
+ const len = str.length;
126
+ while (i < len) {
127
+ // String literal -- copy as-is
128
+ if (str[i] === '"') {
129
+ let j = i + 1;
130
+ while (j < len && str[j] !== '"') {
131
+ if (str[j] === '\\') {
132
+ j++; // skip escaped char
133
+ }
134
+ j++;
135
+ }
136
+ result += str.substring(i, j + 1);
137
+ i = j + 1;
138
+ continue;
139
+ }
140
+ // Single-line comment
141
+ if (str[i] === '/' && str[i + 1] === '/') {
142
+ while (i < len && str[i] !== '\n') {
143
+ i++;
144
+ }
145
+ continue;
146
+ }
147
+ // Block comment
148
+ if (str[i] === '/' && str[i + 1] === '*') {
149
+ i += 2;
150
+ while (i < len && !(str[i] === '*' && str[i + 1] === '/')) {
151
+ i++;
152
+ }
153
+ i += 2;
154
+ continue;
155
+ }
156
+ // Trailing comma: comma followed by optional whitespace + closing bracket
157
+ if (str[i] === ',') {
158
+ let j = i + 1;
159
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) {
160
+ j++;
161
+ }
162
+ if (str[j] === '}' || str[j] === ']') {
163
+ i++;
164
+ continue;
165
+ }
166
+ }
167
+ result += str[i];
168
+ i++;
169
+ }
170
+ return result;
171
+ }
172
+ /**
173
+ * Load and parse tsconfig.json path aliases.
174
+ *
175
+ * Tries tsconfig.json first, then tsconfig.base.json.
176
+ * Converts alias patterns like "@libs/*" into prefix → resolved dirs.
177
+ */
178
+ export function loadTsconfigAliases(repoRoot) {
179
+ const aliases = new Map();
180
+ for (const filename of ['tsconfig.json', 'tsconfig.base.json']) {
181
+ const tsconfigPath = join(repoRoot, filename);
182
+ if (!existsSync(tsconfigPath)) {
183
+ continue;
184
+ }
185
+ try {
186
+ const content = readFileSync(tsconfigPath, 'utf-8');
187
+ const cleaned = stripJsonComments(content);
188
+ const config = JSON.parse(cleaned);
189
+ const paths = config?.compilerOptions?.paths;
190
+ const baseUrl = config?.compilerOptions?.baseUrl || '.';
191
+ const baseDir = join(repoRoot, baseUrl);
192
+ if (paths) {
193
+ for (const [alias, targets] of Object.entries(paths)) {
194
+ // Convert alias pattern: "@libs/*" -> prefix "@libs/"
195
+ const prefix = alias.replace('/*', '/').replace('*', '');
196
+ const resolvedTargets = targets.map((t) => {
197
+ const targetPath = t.replace('/*', '').replace('*', '');
198
+ return join(baseDir, targetPath);
199
+ });
200
+ aliases.set(prefix, resolvedTargets);
201
+ }
202
+ }
203
+ }
204
+ catch (err) {
205
+ log.warn('Failed to parse tsconfig', { file: tsconfigPath, error: String(err) });
206
+ }
207
+ }
208
+ return aliases;
209
+ }
210
+ /**
211
+ * Resolve an import path using tsconfig aliases.
212
+ *
213
+ * Tries each alias prefix, and for matches, probes extensions and index files.
214
+ */
215
+ export function resolveWithAliases(modulePath, aliases, _repoRoot) {
216
+ for (const [prefix, targets] of aliases) {
217
+ if (modulePath.startsWith(prefix)) {
218
+ const rest = modulePath.slice(prefix.length);
219
+ for (const targetBase of targets) {
220
+ const base = join(targetBase, rest);
221
+ for (const ext of TS_EXTENSIONS) {
222
+ if (existsSync(base + ext)) {
223
+ return resolvePath(base + ext);
224
+ }
225
+ }
226
+ for (const ext of TS_EXTENSIONS) {
227
+ const idx = join(base, `index${ext}`);
228
+ if (existsSync(idx)) {
229
+ return resolvePath(idx);
230
+ }
231
+ }
232
+ // Try exact match (for directories with index)
233
+ if (existsSync(base)) {
234
+ return resolvePath(base);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return null;
240
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Re-export (barrel) resolver.
3
+ *
4
+ * Follows `export { X } from './module'` chains so that the import map
5
+ * points to the file where the symbol is actually defined, not the
6
+ * barrel index file.
7
+ *
8
+ * Without this, an import like `import { Foo } from '@lib'` resolves
9
+ * to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
10
+ * The call resolver can't find `Foo` in the barrel's symbol table and
11
+ * falls to lower-confidence tiers.
12
+ */
13
+ interface RawReExport {
14
+ module: string;
15
+ file: string;
16
+ line: number;
17
+ }
18
+ /**
19
+ * Build a map: barrel file (relative) → list of resolved re-export target files (relative).
20
+ *
21
+ * Follows one level of re-exports (covers >95% of real-world barrel patterns).
22
+ */
23
+ export declare function buildReExportMap(reExports: RawReExport[], repoDir: string, tsconfigAliases?: Map<string, string[]>): Map<string, string[]>;
24
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Re-export (barrel) resolver.
3
+ *
4
+ * Follows `export { X } from './module'` chains so that the import map
5
+ * points to the file where the symbol is actually defined, not the
6
+ * barrel index file.
7
+ *
8
+ * Without this, an import like `import { Foo } from '@lib'` resolves
9
+ * to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
10
+ * The call resolver can't find `Foo` in the barrel's symbol table and
11
+ * falls to lower-confidence tiers.
12
+ */
13
+ import { relative, resolve } from 'path';
14
+ import { resolveImport } from './import-resolver';
15
+ /**
16
+ * Build a map: barrel file (relative) → list of resolved re-export target files (relative).
17
+ *
18
+ * Follows one level of re-exports (covers >95% of real-world barrel patterns).
19
+ */
20
+ export function buildReExportMap(reExports, repoDir, tsconfigAliases) {
21
+ const barrelMap = new Map();
22
+ for (const re of reExports) {
23
+ const absFrom = resolve(repoDir, re.file);
24
+ const resolved = resolveImport(absFrom, re.module, 'typescript', repoDir, tsconfigAliases);
25
+ if (!resolved) {
26
+ continue;
27
+ }
28
+ const resolvedRel = relative(repoDir, resolved);
29
+ const list = barrelMap.get(re.file);
30
+ if (list) {
31
+ if (!list.includes(resolvedRel)) {
32
+ list.push(resolvedRel);
33
+ }
34
+ }
35
+ else {
36
+ barrelMap.set(re.file, [resolvedRel]);
37
+ }
38
+ }
39
+ // Follow one extra level: if a re-export target is itself a barrel, flatten
40
+ for (const [barrel, targets] of barrelMap) {
41
+ const extra = [];
42
+ for (const target of targets) {
43
+ const nested = barrelMap.get(target);
44
+ if (nested) {
45
+ for (const n of nested) {
46
+ if (n !== barrel && !targets.includes(n) && !extra.includes(n)) {
47
+ extra.push(n);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ if (extra.length > 0) {
53
+ targets.push(...extra);
54
+ }
55
+ }
56
+ return barrelMap;
57
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Symbol table with dual-index lookup (GitNexus pattern).
3
+ *
4
+ * Provides both exact (file + name) and global (name-only) lookups.
5
+ * The exact lookup is high-confidence; global lookup is lower-confidence
6
+ * but useful when import resolution fails.
7
+ */
8
+ export interface SymbolTable {
9
+ add(file: string, name: string, qualified: string): void;
10
+ lookupExact(file: string, name: string): string | null;
11
+ lookupInFile(file: string, name: string, className: string): string | null;
12
+ isUnique(name: string): boolean;
13
+ lookupGlobal(name: string): string[];
14
+ readonly size: number;
15
+ readonly fileCount: number;
16
+ }
17
+ export declare function createSymbolTable(): SymbolTable;