@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,820 @@
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
+ import { readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { cachedExists } from './fs-cache';
9
+ const depsCache = new Map();
10
+ export function clearExternalCache() {
11
+ depsCache.clear();
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // Built-in / stdlib lists
15
+ // ---------------------------------------------------------------------------
16
+ const NODE_BUILTINS = new Set([
17
+ 'fs',
18
+ 'path',
19
+ 'os',
20
+ 'http',
21
+ 'https',
22
+ 'http2',
23
+ 'net',
24
+ 'stream',
25
+ 'buffer',
26
+ 'url',
27
+ 'util',
28
+ 'crypto',
29
+ 'events',
30
+ 'child_process',
31
+ 'cluster',
32
+ 'dns',
33
+ 'readline',
34
+ 'repl',
35
+ 'tls',
36
+ 'vm',
37
+ 'zlib',
38
+ 'assert',
39
+ 'async_hooks',
40
+ 'console',
41
+ 'constants',
42
+ 'dgram',
43
+ 'diagnostics_channel',
44
+ 'domain',
45
+ 'inspector',
46
+ 'module',
47
+ 'perf_hooks',
48
+ 'process',
49
+ 'punycode',
50
+ 'querystring',
51
+ 'string_decoder',
52
+ 'timers',
53
+ 'tty',
54
+ 'v8',
55
+ 'wasi',
56
+ 'worker_threads',
57
+ ]);
58
+ const PYTHON_STDLIB = new Set([
59
+ 'os',
60
+ 'sys',
61
+ 'json',
62
+ 'typing',
63
+ 'collections',
64
+ 'datetime',
65
+ 're',
66
+ 'math',
67
+ 'pathlib',
68
+ 'functools',
69
+ 'itertools',
70
+ 'abc',
71
+ 'dataclasses',
72
+ 'enum',
73
+ 'logging',
74
+ 'unittest',
75
+ 'io',
76
+ 'copy',
77
+ 'hashlib',
78
+ 'hmac',
79
+ 'secrets',
80
+ 'socket',
81
+ 'http',
82
+ 'urllib',
83
+ 'email',
84
+ 'html',
85
+ 'xml',
86
+ 'sqlite3',
87
+ 'csv',
88
+ 'configparser',
89
+ 'argparse',
90
+ 'subprocess',
91
+ 'threading',
92
+ 'multiprocessing',
93
+ 'asyncio',
94
+ 'signal',
95
+ 'shutil',
96
+ 'tempfile',
97
+ 'glob',
98
+ 'fnmatch',
99
+ 'struct',
100
+ 'codecs',
101
+ 'pprint',
102
+ 'textwrap',
103
+ 'difflib',
104
+ 'traceback',
105
+ 'warnings',
106
+ 'contextlib',
107
+ 'weakref',
108
+ 'types',
109
+ 'inspect',
110
+ 'dis',
111
+ 'importlib',
112
+ 'pkgutil',
113
+ 'pdb',
114
+ 'cProfile',
115
+ 'time',
116
+ 'calendar',
117
+ 'random',
118
+ 'statistics',
119
+ 'fractions',
120
+ 'decimal',
121
+ 'operator',
122
+ 'string',
123
+ 'base64',
124
+ 'binascii',
125
+ 'zlib',
126
+ 'gzip',
127
+ 'bz2',
128
+ 'lzma',
129
+ 'zipfile',
130
+ 'tarfile',
131
+ // additional commonly used stdlib modules
132
+ 'builtins',
133
+ 'array',
134
+ 'bisect',
135
+ 'heapq',
136
+ 'queue',
137
+ 'sched',
138
+ 'selectors',
139
+ 'mmap',
140
+ 'ctypes',
141
+ 'concurrent',
142
+ 'test',
143
+ 'profile',
144
+ 'cmath',
145
+ 'numbers',
146
+ 'locale',
147
+ 'gettext',
148
+ 'unicodedata',
149
+ 'stringprep',
150
+ 'rlcompleter',
151
+ 'code',
152
+ 'codeop',
153
+ 'compileall',
154
+ 'py_compile',
155
+ 'zipimport',
156
+ 'winreg',
157
+ 'winsound',
158
+ 'msvcrt',
159
+ 'posixpath',
160
+ 'ntpath',
161
+ 'genericpath',
162
+ 'posix',
163
+ 'nt',
164
+ 'token',
165
+ 'tokenize',
166
+ 'keyword',
167
+ 'linecache',
168
+ 'pickle',
169
+ 'shelve',
170
+ 'marshal',
171
+ 'dbm',
172
+ 'platform',
173
+ 'errno',
174
+ 'faulthandler',
175
+ 'atexit',
176
+ 'site',
177
+ 'sysconfig',
178
+ 'zipapp',
179
+ 'venv',
180
+ 'ensurepip',
181
+ 'distutils',
182
+ 'setuptools',
183
+ '_thread',
184
+ '__future__',
185
+ 'colorsys',
186
+ 'fileinput',
187
+ 'filecmp',
188
+ 'stat',
189
+ 'grp',
190
+ 'pwd',
191
+ 'resource',
192
+ 'termios',
193
+ 'fcntl',
194
+ 'pty',
195
+ 'pipes',
196
+ 'mailbox',
197
+ 'mailcap',
198
+ 'mimetypes',
199
+ 'imaplib',
200
+ 'poplib',
201
+ 'smtplib',
202
+ 'ftplib',
203
+ 'telnetlib',
204
+ 'xmlrpc',
205
+ 'ipaddress',
206
+ 'ssl',
207
+ 'cgi',
208
+ 'cgitb',
209
+ 'wsgiref',
210
+ 'webbrowser',
211
+ 'uuid',
212
+ 'getpass',
213
+ 'curses',
214
+ 'turtle',
215
+ 'cmd',
216
+ 'shlex',
217
+ 'tkinter',
218
+ 'idlelib',
219
+ 'doctest',
220
+ 'pydoc',
221
+ 'ast',
222
+ 'symtable',
223
+ 'tabnanny',
224
+ ]);
225
+ const RUBY_STDLIB = new Set([
226
+ 'json',
227
+ 'net/http',
228
+ 'uri',
229
+ 'fileutils',
230
+ 'set',
231
+ 'csv',
232
+ 'yaml',
233
+ 'openssl',
234
+ 'pathname',
235
+ 'tempfile',
236
+ 'socket',
237
+ 'open-uri',
238
+ 'erb',
239
+ 'cgi',
240
+ 'digest',
241
+ 'base64',
242
+ 'securerandom',
243
+ 'optparse',
244
+ 'logger',
245
+ 'stringio',
246
+ 'strscan',
247
+ 'date',
248
+ 'time',
249
+ 'bigdecimal',
250
+ 'fiddle',
251
+ 'readline',
252
+ 'io/console',
253
+ 'benchmark',
254
+ 'minitest',
255
+ 'pp',
256
+ 'irb',
257
+ 'rdoc',
258
+ 'psych',
259
+ 'zlib',
260
+ 'webrick',
261
+ 'rexml',
262
+ 'rss',
263
+ 'drb',
264
+ 'mutex_m',
265
+ 'observer',
266
+ 'singleton',
267
+ 'forwardable',
268
+ 'delegate',
269
+ 'ostruct',
270
+ 'open3',
271
+ 'shellwords',
272
+ 'abbrev',
273
+ 'english',
274
+ 'find',
275
+ 'resolv',
276
+ 'ipaddr',
277
+ 'un',
278
+ 'mkmf',
279
+ ]);
280
+ const JAVA_STDLIB_PREFIXES = ['java.', 'javax.', 'jakarta.', 'sun.', 'com.sun.', 'jdk.'];
281
+ const RUST_STDLIB_CRATES = new Set(['std', 'core', 'alloc']);
282
+ // ---------------------------------------------------------------------------
283
+ // Manifest parsers
284
+ // ---------------------------------------------------------------------------
285
+ function safeRead(filePath) {
286
+ if (!cachedExists(filePath)) {
287
+ return null;
288
+ }
289
+ try {
290
+ return readFileSync(filePath, 'utf-8');
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ function safeParseJson(filePath) {
297
+ const text = safeRead(filePath);
298
+ if (!text) {
299
+ return null;
300
+ }
301
+ try {
302
+ return JSON.parse(text);
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ }
308
+ function loadNodeDeps(repoRoot) {
309
+ const pkgs = new Set();
310
+ const pkg = safeParseJson(join(repoRoot, 'package.json'));
311
+ if (pkg) {
312
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
313
+ const deps = pkg[field];
314
+ if (deps && typeof deps === 'object') {
315
+ for (const name of Object.keys(deps)) {
316
+ pkgs.add(name);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ return { packages: pkgs };
322
+ }
323
+ function loadPythonDeps(repoRoot) {
324
+ const pkgs = new Set();
325
+ // requirements.txt
326
+ const reqText = safeRead(join(repoRoot, 'requirements.txt'));
327
+ if (reqText) {
328
+ for (const line of reqText.split('\n')) {
329
+ const trimmed = line.trim();
330
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
331
+ continue;
332
+ }
333
+ // Strip version specifiers: django>=4.0 -> django
334
+ const name = trimmed
335
+ .split(/[>=<!~;\s[]/)[0]
336
+ .trim()
337
+ .toLowerCase()
338
+ .replace(/-/g, '_');
339
+ if (name) {
340
+ pkgs.add(name);
341
+ }
342
+ }
343
+ }
344
+ // pyproject.toml — simple line-based parsing
345
+ const pyproject = safeRead(join(repoRoot, 'pyproject.toml'));
346
+ if (pyproject) {
347
+ let inDeps = false;
348
+ for (const line of pyproject.split('\n')) {
349
+ const trimmed = line.trim();
350
+ if (/^\[(project|tool\.poetry)\.?dependencies\]$/i.test(trimmed) ||
351
+ trimmed === '[project]' ||
352
+ trimmed === '[tool.poetry.dependencies]') {
353
+ inDeps = true;
354
+ continue;
355
+ }
356
+ if (trimmed.startsWith('[') && inDeps) {
357
+ inDeps = false;
358
+ continue;
359
+ }
360
+ if (inDeps) {
361
+ // TOML key = value or "name>=version" in a list
362
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
363
+ if (match) {
364
+ const name = match[1].toLowerCase().replace(/-/g, '_');
365
+ if (name !== 'python') {
366
+ pkgs.add(name);
367
+ }
368
+ }
369
+ // List items: "django>=4.0"
370
+ const listMatch = trimmed.match(/^"([a-zA-Z0-9_-]+)/);
371
+ if (listMatch) {
372
+ pkgs.add(listMatch[1].toLowerCase().replace(/-/g, '_'));
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return { packages: pkgs };
378
+ }
379
+ function loadGoDeps(repoRoot) {
380
+ const pkgs = new Set();
381
+ const meta = {};
382
+ const gomod = safeRead(join(repoRoot, 'go.mod'));
383
+ if (gomod) {
384
+ // Extract module name
385
+ const modMatch = gomod.match(/^module\s+(.+)$/m);
386
+ if (modMatch) {
387
+ meta.module = modMatch[1].trim();
388
+ }
389
+ // Extract require block
390
+ let inRequire = false;
391
+ for (const line of gomod.split('\n')) {
392
+ const trimmed = line.trim();
393
+ if (trimmed === 'require (') {
394
+ inRequire = true;
395
+ continue;
396
+ }
397
+ if (trimmed === ')') {
398
+ inRequire = false;
399
+ continue;
400
+ }
401
+ if (inRequire) {
402
+ const match = trimmed.match(/^(\S+)\s+/);
403
+ if (match) {
404
+ pkgs.add(match[1]);
405
+ }
406
+ }
407
+ // Single-line require
408
+ const singleMatch = trimmed.match(/^require\s+(\S+)\s+/);
409
+ if (singleMatch) {
410
+ pkgs.add(singleMatch[1]);
411
+ }
412
+ }
413
+ }
414
+ return { packages: pkgs, meta };
415
+ }
416
+ function loadRustDeps(repoRoot) {
417
+ const pkgs = new Set();
418
+ const cargo = safeRead(join(repoRoot, 'Cargo.toml'));
419
+ if (cargo) {
420
+ let inDeps = false;
421
+ for (const line of cargo.split('\n')) {
422
+ const trimmed = line.trim();
423
+ if (/^\[(.*dependencies.*)\]$/i.test(trimmed)) {
424
+ inDeps = true;
425
+ continue;
426
+ }
427
+ if (trimmed.startsWith('[') && inDeps) {
428
+ inDeps = false;
429
+ continue;
430
+ }
431
+ if (inDeps) {
432
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
433
+ if (match) {
434
+ pkgs.add(match[1]);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ return { packages: pkgs };
440
+ }
441
+ function loadJavaDeps(repoRoot) {
442
+ const pkgs = new Set();
443
+ // pom.xml — simple regex-based parsing
444
+ const pom = safeRead(join(repoRoot, 'pom.xml'));
445
+ if (pom) {
446
+ const depRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>/gs;
447
+ let m = depRegex.exec(pom);
448
+ while (m !== null) {
449
+ // Store as "groupId:artifactId" for later matching
450
+ pkgs.add(`${m[1]}:${m[2]}`);
451
+ m = depRegex.exec(pom);
452
+ }
453
+ }
454
+ // build.gradle — basic regex
455
+ const gradle = safeRead(join(repoRoot, 'build.gradle'));
456
+ const gradleKts = safeRead(join(repoRoot, 'build.gradle.kts'));
457
+ for (const text of [gradle, gradleKts]) {
458
+ if (!text) {
459
+ continue;
460
+ }
461
+ // Matches: implementation 'group:artifact:version' or "group:artifact:version"
462
+ const regex = /(?:implementation|api|compileOnly|runtimeOnly|testImplementation)\s+['"]([^'"]+)['"]/g;
463
+ let gm = regex.exec(text);
464
+ while (gm !== null) {
465
+ const parts = gm[1].split(':');
466
+ if (parts.length >= 2) {
467
+ pkgs.add(`${parts[0]}:${parts[1]}`);
468
+ }
469
+ gm = regex.exec(text);
470
+ }
471
+ }
472
+ return { packages: pkgs };
473
+ }
474
+ function loadPhpDeps(repoRoot) {
475
+ const pkgs = new Set();
476
+ const composer = safeParseJson(join(repoRoot, 'composer.json'));
477
+ if (composer) {
478
+ for (const field of ['require', 'require-dev']) {
479
+ const deps = composer[field];
480
+ if (deps && typeof deps === 'object') {
481
+ for (const name of Object.keys(deps)) {
482
+ if (name === 'php') {
483
+ continue;
484
+ }
485
+ pkgs.add(name);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ return { packages: pkgs };
491
+ }
492
+ function loadRubyDeps(repoRoot) {
493
+ const pkgs = new Set();
494
+ const gemfile = safeRead(join(repoRoot, 'Gemfile'));
495
+ if (gemfile) {
496
+ const regex = /gem\s+['"]([^'"]+)['"]/g;
497
+ let m = regex.exec(gemfile);
498
+ while (m !== null) {
499
+ pkgs.add(m[1]);
500
+ m = regex.exec(gemfile);
501
+ }
502
+ }
503
+ return { packages: pkgs };
504
+ }
505
+ function loadCsharpDeps(repoRoot) {
506
+ const pkgs = new Set();
507
+ // Find .csproj files at root or one level deep
508
+ const candidates = [];
509
+ try {
510
+ const entries = require('fs').readdirSync(repoRoot);
511
+ for (const e of entries) {
512
+ if (e.endsWith('.csproj')) {
513
+ candidates.push(join(repoRoot, e));
514
+ }
515
+ }
516
+ }
517
+ catch {
518
+ /* ignore */
519
+ }
520
+ for (const csproj of candidates) {
521
+ const text = safeRead(csproj);
522
+ if (!text) {
523
+ continue;
524
+ }
525
+ const regex = /<PackageReference\s+Include="([^"]+)"/gi;
526
+ let m = regex.exec(text);
527
+ while (m !== null) {
528
+ pkgs.add(m[1]);
529
+ m = regex.exec(text);
530
+ }
531
+ }
532
+ return { packages: pkgs };
533
+ }
534
+ // ---------------------------------------------------------------------------
535
+ // Loader
536
+ // ---------------------------------------------------------------------------
537
+ function loadDeps(repoRoot) {
538
+ const cached = depsCache.get(repoRoot);
539
+ if (cached) {
540
+ return cached;
541
+ }
542
+ const result = new Map();
543
+ // TypeScript / JavaScript
544
+ if (cachedExists(join(repoRoot, 'package.json'))) {
545
+ const nodeDeps = loadNodeDeps(repoRoot);
546
+ result.set('typescript', nodeDeps);
547
+ result.set('javascript', nodeDeps);
548
+ result.set('ts', nodeDeps);
549
+ }
550
+ // Python
551
+ if (cachedExists(join(repoRoot, 'requirements.txt')) || cachedExists(join(repoRoot, 'pyproject.toml'))) {
552
+ result.set('python', loadPythonDeps(repoRoot));
553
+ }
554
+ // Go
555
+ if (cachedExists(join(repoRoot, 'go.mod'))) {
556
+ result.set('go', loadGoDeps(repoRoot));
557
+ }
558
+ // Rust
559
+ if (cachedExists(join(repoRoot, 'Cargo.toml'))) {
560
+ result.set('rust', loadRustDeps(repoRoot));
561
+ }
562
+ // Java
563
+ if (cachedExists(join(repoRoot, 'pom.xml')) ||
564
+ cachedExists(join(repoRoot, 'build.gradle')) ||
565
+ cachedExists(join(repoRoot, 'build.gradle.kts'))) {
566
+ result.set('java', loadJavaDeps(repoRoot));
567
+ }
568
+ // PHP
569
+ if (cachedExists(join(repoRoot, 'composer.json'))) {
570
+ result.set('php', loadPhpDeps(repoRoot));
571
+ }
572
+ // Ruby
573
+ if (cachedExists(join(repoRoot, 'Gemfile'))) {
574
+ result.set('ruby', loadRubyDeps(repoRoot));
575
+ }
576
+ // C#
577
+ result.set('csharp', loadCsharpDeps(repoRoot));
578
+ depsCache.set(repoRoot, result);
579
+ return result;
580
+ }
581
+ // ---------------------------------------------------------------------------
582
+ // Public API
583
+ // ---------------------------------------------------------------------------
584
+ /**
585
+ * Check if an import is an external (third-party) package.
586
+ * Returns the package name if external, null if not detected as external.
587
+ */
588
+ export function detectExternal(modulePath, lang, repoRoot) {
589
+ // Normalize language key
590
+ const langKey = lang === 'ts' ? 'typescript' : lang;
591
+ // ----- TypeScript / JavaScript -----
592
+ if (langKey === 'typescript' || langKey === 'javascript') {
593
+ // Relative imports are never external
594
+ if (modulePath.startsWith('.') || modulePath.startsWith('#')) {
595
+ return null;
596
+ }
597
+ // Node builtin (with or without node: prefix)
598
+ if (modulePath.startsWith('node:')) {
599
+ return modulePath;
600
+ }
601
+ if (modulePath.startsWith('bun:')) {
602
+ return modulePath;
603
+ }
604
+ if (NODE_BUILTINS.has(modulePath)) {
605
+ return modulePath;
606
+ }
607
+ // Also handle node:XXX/subpath
608
+ const bareNode = modulePath.split('/')[0];
609
+ if (NODE_BUILTINS.has(bareNode)) {
610
+ return bareNode;
611
+ }
612
+ const deps = loadDeps(repoRoot);
613
+ const langDeps = deps.get('typescript');
614
+ if (!langDeps) {
615
+ return null;
616
+ }
617
+ // Scoped package: @scope/name or @scope/name/subpath
618
+ if (modulePath.startsWith('@')) {
619
+ const parts = modulePath.split('/');
620
+ const scopedName = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : modulePath;
621
+ if (langDeps.packages.has(scopedName)) {
622
+ return scopedName;
623
+ }
624
+ // Bare specifier not in deps but doesn't start with . or # → likely external
625
+ return scopedName;
626
+ }
627
+ // Non-scoped: bare specifier
628
+ const topLevel = modulePath.split('/')[0];
629
+ if (langDeps.packages.has(topLevel)) {
630
+ return topLevel;
631
+ }
632
+ // Bare specifier not found in deps — still likely external (unlisted dep)
633
+ return topLevel;
634
+ }
635
+ // ----- Python -----
636
+ if (langKey === 'python') {
637
+ // Relative imports start with .
638
+ if (modulePath.startsWith('.')) {
639
+ return null;
640
+ }
641
+ const topLevel = modulePath.split('.')[0].toLowerCase().replace(/-/g, '_');
642
+ // Python stdlib
643
+ if (PYTHON_STDLIB.has(topLevel)) {
644
+ return topLevel;
645
+ }
646
+ const deps = loadDeps(repoRoot);
647
+ const langDeps = deps.get('python');
648
+ if (!langDeps) {
649
+ // No manifest found — check stdlib only
650
+ return PYTHON_STDLIB.has(topLevel) ? topLevel : null;
651
+ }
652
+ if (langDeps.packages.has(topLevel)) {
653
+ return topLevel;
654
+ }
655
+ return null;
656
+ }
657
+ // ----- Go -----
658
+ if (langKey === 'go') {
659
+ // Go stdlib: no dot in first segment
660
+ const firstSegment = modulePath.split('/')[0];
661
+ if (!firstSegment.includes('.')) {
662
+ return modulePath;
663
+ }
664
+ const deps = loadDeps(repoRoot);
665
+ const langDeps = deps.get('go');
666
+ if (!langDeps) {
667
+ return null;
668
+ }
669
+ // Check if it's the project's own module
670
+ const ownModule = langDeps.meta?.module;
671
+ if (ownModule && modulePath.startsWith(ownModule)) {
672
+ return null;
673
+ }
674
+ // Check require list — match prefix
675
+ for (const dep of langDeps.packages) {
676
+ if (modulePath === dep || modulePath.startsWith(`${dep}/`)) {
677
+ return dep;
678
+ }
679
+ }
680
+ // Has a dot in first segment but not in require list — still likely external
681
+ return modulePath;
682
+ }
683
+ // ----- Rust -----
684
+ if (langKey === 'rust') {
685
+ const firstSegment = modulePath.split('::')[0];
686
+ // crate:: and super:: and self:: are local
687
+ if (firstSegment === 'crate' || firstSegment === 'super' || firstSegment === 'self') {
688
+ return null;
689
+ }
690
+ // stdlib crates
691
+ if (RUST_STDLIB_CRATES.has(firstSegment)) {
692
+ return firstSegment;
693
+ }
694
+ const deps = loadDeps(repoRoot);
695
+ const langDeps = deps.get('rust');
696
+ if (!langDeps) {
697
+ return null;
698
+ }
699
+ // Cargo dependency names use hyphens but Rust uses underscores
700
+ const normalized = firstSegment.replace(/-/g, '_');
701
+ for (const dep of langDeps.packages) {
702
+ if (dep.replace(/-/g, '_') === normalized) {
703
+ return dep;
704
+ }
705
+ }
706
+ return null;
707
+ }
708
+ // ----- Java -----
709
+ if (langKey === 'java') {
710
+ // Java stdlib
711
+ for (const prefix of JAVA_STDLIB_PREFIXES) {
712
+ if (modulePath.startsWith(prefix)) {
713
+ // Return the first two segments (e.g. java.util)
714
+ const parts = modulePath.split('.');
715
+ return parts.slice(0, 2).join('.');
716
+ }
717
+ }
718
+ const deps = loadDeps(repoRoot);
719
+ const langDeps = deps.get('java');
720
+ if (!langDeps) {
721
+ return null;
722
+ }
723
+ // Match groupId prefix against import path
724
+ // e.g. groupId "org.springframework.boot" -> import "org.springframework.boot.SpringApplication"
725
+ for (const dep of langDeps.packages) {
726
+ const [groupId, artifactId] = dep.split(':');
727
+ if (modulePath.startsWith(groupId)) {
728
+ return artifactId;
729
+ }
730
+ }
731
+ return null;
732
+ }
733
+ // ----- PHP -----
734
+ if (langKey === 'php') {
735
+ const deps = loadDeps(repoRoot);
736
+ const langDeps = deps.get('php');
737
+ if (!langDeps) {
738
+ return null;
739
+ }
740
+ // Get composer.json autoload info for local namespace detection
741
+ const composer = safeParseJson(join(repoRoot, 'composer.json'));
742
+ if (composer) {
743
+ const autoload = composer.autoload;
744
+ if (autoload) {
745
+ const psr4 = autoload['psr-4'];
746
+ if (psr4) {
747
+ // Normalize import path separators
748
+ const normalized = modulePath.replace(/\//g, '\\');
749
+ for (const ns of Object.keys(psr4)) {
750
+ if (normalized.startsWith(ns)) {
751
+ return null; // local namespace
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ // Check known package → namespace mappings
758
+ // Common Composer package namespace mappings
759
+ const COMPOSER_NS_MAP = {
760
+ 'laravel/framework': ['Illuminate\\'],
761
+ 'guzzlehttp/guzzle': ['GuzzleHttp\\'],
762
+ 'symfony/console': ['Symfony\\Component\\Console\\'],
763
+ 'symfony/http-foundation': ['Symfony\\Component\\HttpFoundation\\'],
764
+ 'monolog/monolog': ['Monolog\\'],
765
+ 'doctrine/orm': ['Doctrine\\ORM\\'],
766
+ 'phpunit/phpunit': ['PHPUnit\\'],
767
+ };
768
+ const normalized = modulePath.replace(/\//g, '\\');
769
+ for (const dep of langDeps.packages) {
770
+ const namespaces = COMPOSER_NS_MAP[dep];
771
+ if (namespaces) {
772
+ for (const ns of namespaces) {
773
+ if (normalized.startsWith(ns)) {
774
+ return dep;
775
+ }
776
+ }
777
+ }
778
+ }
779
+ return null;
780
+ }
781
+ // ----- Ruby -----
782
+ if (langKey === 'ruby') {
783
+ // Ruby stdlib
784
+ if (RUBY_STDLIB.has(modulePath)) {
785
+ return modulePath;
786
+ }
787
+ const deps = loadDeps(repoRoot);
788
+ const langDeps = deps.get('ruby');
789
+ if (!langDeps) {
790
+ return null;
791
+ }
792
+ if (langDeps.packages.has(modulePath)) {
793
+ return modulePath;
794
+ }
795
+ return null;
796
+ }
797
+ // ----- C# -----
798
+ if (langKey === 'csharp') {
799
+ // Framework namespaces
800
+ if (modulePath.startsWith('System.') ||
801
+ modulePath === 'System' ||
802
+ modulePath.startsWith('Microsoft.') ||
803
+ modulePath === 'Microsoft') {
804
+ return modulePath.split('.').slice(0, 2).join('.');
805
+ }
806
+ const deps = loadDeps(repoRoot);
807
+ const langDeps = deps.get('csharp');
808
+ if (!langDeps) {
809
+ return null;
810
+ }
811
+ // Match PackageReference names against import namespace
812
+ for (const dep of langDeps.packages) {
813
+ if (modulePath.startsWith(dep)) {
814
+ return dep;
815
+ }
816
+ }
817
+ return null;
818
+ }
819
+ return null;
820
+ }