ucn 5.4.2 → 5.5.1

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 (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
package/core/imports.js CHANGED
@@ -78,6 +78,14 @@ function resolveImport(importPath, fromFile, config = {}) {
78
78
  // Strip query strings (e.g., ?raw, ?url)
79
79
  importPath = importPath.split('?')[0];
80
80
 
81
+ // C/C++ angle-bracket includes search the -I directories (never the
82
+ // including file's directory); a project header found there is part of
83
+ // the translation unit exactly like a quoted include.
84
+ if ((config.language === 'c' || config.language === 'cpp') &&
85
+ !importPath.startsWith('.') && !importPath.startsWith('/')) {
86
+ return resolveCIncludeFromDirectories(importPath, fromFile, config);
87
+ }
88
+
81
89
  // External packages (not relative or alias)
82
90
  if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
83
91
  // Check aliases
@@ -124,6 +132,9 @@ function resolveImport(importPath, fromFile, config = {}) {
124
132
  // Package self-reference (import own package by name)
125
133
  const selfResolved = resolveSelfReference(importPath, fromDir, config);
126
134
  if (selfResolved) return selfResolved;
135
+ // Another package of the same repository (fix #397).
136
+ const workspaceResolved = resolveWorkspacePackage(importPath, config);
137
+ if (workspaceResolved) return workspaceResolved;
127
138
  }
128
139
 
129
140
  // Check Go module imports
@@ -154,6 +165,15 @@ function resolveImport(importPath, fromFile, config = {}) {
154
165
  const srcPath = path.join(config.root, 'src', modulePath);
155
166
  const srcResolved = resolveFilePath(srcPath, getExtensions('python'));
156
167
  if (srcResolved) return srcResolved;
168
+ // Nested source root (fix #366): an application under a
169
+ // subdirectory (`backend/app/...`) runs with that subdirectory on
170
+ // sys.path, so `from app.core import x` means backend/app/core.
171
+ // The nearest ancestor of the importing file that is NOT itself a
172
+ // package and directly contains the module's first segment is
173
+ // the source root the interpreter would use. Package directories
174
+ // are skipped so a sibling module never answers an absolute import.
175
+ const nested = _resolvePythonNestedRoot(importPath, fromDir, config);
176
+ if (nested) return nested;
157
177
  }
158
178
 
159
179
  return null; // External package
@@ -186,34 +206,53 @@ function resolveImport(importPath, fromFile, config = {}) {
186
206
  if (direct) return direct;
187
207
 
188
208
  // C/C++ quoted includes may be rooted at compiler -I/-iquote paths rather
189
- // than the importing file. compile_commands.json is the authoritative
190
- // build metadata when present; unresolved system includes remain external.
209
+ // than the importing file.
191
210
  if (config.language === 'c' || config.language === 'cpp') {
192
- const { includeDirectoriesForFile } = require('./compilation-database');
193
- const includeName = normalizedPath.replace(/^\.\//, '');
194
- const includeDirs = includeDirectoriesForFile(fromFile, config.root);
195
- for (const configured of config.includePaths || []) {
196
- if (typeof configured !== 'string' || !configured.trim()) continue;
197
- includeDirs.push(path.isAbsolute(configured)
198
- ? configured
199
- : path.resolve(config.root || fromDir, configured));
200
- }
201
- // Header-only/source-distribution projects commonly omit a generated
202
- // compile_commands.json but still use the conventional public
203
- // `include/` root (`#include "fmt/format.h"`). These are project-owned
204
- // files, not external packages. Try explicit compiler metadata first,
205
- // then deterministic project roots; never search arbitrary parents.
206
- if (config.root) {
207
- includeDirs.push(config.root, path.join(config.root, 'include'));
208
- }
209
- for (const includeDir of [...new Set(includeDirs)]) {
210
- const candidate = resolveFilePath(path.resolve(includeDir, includeName), extensions);
211
- if (candidate) return candidate;
212
- }
211
+ return resolveCIncludeFromDirectories(normalizedPath.replace(/^\.\//, ''), fromFile, config);
213
212
  }
214
213
  return null;
215
214
  }
216
215
 
216
+ /**
217
+ * Search the compiler include directories for a C/C++ include name.
218
+ * compile_commands.json is the authoritative build metadata when present;
219
+ * configured include paths follow, then the deterministic project roots.
220
+ * Unresolved system includes remain external.
221
+ */
222
+ function resolveCIncludeFromDirectories(includeName, fromFile, config) {
223
+ const { includeDirectoriesForFile } = require('./compilation-database');
224
+ const extensions = config.extensions || getExtensions(config.language);
225
+ const includeDirs = includeDirectoriesForFile(fromFile, config.root);
226
+ for (const configured of config.includePaths || []) {
227
+ if (typeof configured !== 'string' || !configured.trim()) continue;
228
+ includeDirs.push(path.isAbsolute(configured)
229
+ ? configured
230
+ : path.resolve(config.root || path.dirname(fromFile), configured));
231
+ }
232
+ // Header-only/source-distribution projects commonly omit a generated
233
+ // compile_commands.json but still use the conventional public
234
+ // `include/` root (`#include "fmt/format.h"`). These are project-owned
235
+ // files, not external packages. Try explicit compiler metadata first,
236
+ // then deterministic project roots; never search arbitrary parents.
237
+ if (config.root) {
238
+ includeDirs.push(config.root, path.join(config.root, 'include'));
239
+ }
240
+ // One import-graph build probes the same few directories for the same
241
+ // system headers from every file (`<stdio.h>`); the filesystem does not
242
+ // change within it, so a caller-owned memo answers repeats (fix #365).
243
+ const dirs = [...new Set(includeDirs)];
244
+ const memo = config.probeCache;
245
+ const memoKey = memo ? `${includeName}\0${extensions.join(',')}\0${dirs.join('\0')}` : null;
246
+ if (memo && memo.has(memoKey)) return memo.get(memoKey);
247
+ let found = null;
248
+ for (const includeDir of dirs) {
249
+ const candidate = resolveFilePath(path.resolve(includeDir, includeName), extensions);
250
+ if (candidate) { found = candidate; break; }
251
+ }
252
+ if (memo) memo.set(memoKey, found);
253
+ return found;
254
+ }
255
+
217
256
  // Cache for Go module paths
218
257
  const goModuleCache = new Map();
219
258
 
@@ -331,19 +370,25 @@ const cargoCache = new Map();
331
370
  // Workspace crate registry (fix #258): Cargo [package] name → crate source
332
371
  // root for EVERY crate in the project tree, so cross-crate workspace imports
333
372
  // (`use clap::Command` from clap_bench/) resolve like own-package imports.
334
- // One bounded scan per project root, cached.
373
+ // One bounded scan per project root. The scan runs at build time and its
374
+ // manifests are persisted in the index cache (fix #372): a cache-loaded index
375
+ // seeds the registry instead of walking the tree on the first query.
335
376
  const workspaceCrateCache = new Map();
336
377
  const _WORKSPACE_SCAN_PRUNE = new Set([
337
378
  'node_modules', '.git', 'target', 'vendor', 'dist', 'build', '.ucn-cache',
338
379
  ]);
380
+ const _WORKSPACE_SCAN_MAX_DEPTH = 6;
339
381
 
340
- function workspaceCrateRegistry(projectRoot) {
341
- if (workspaceCrateCache.has(projectRoot)) {
342
- return workspaceCrateCache.get(projectRoot);
343
- }
344
- const registry = new Map();
382
+ /**
383
+ * Directories holding a Cargo.toml, in the registry scan's order (depth-first,
384
+ * directory-entry order). Bounded by depth and the prune set.
385
+ * @param {string} projectRoot
386
+ * @returns {string[]} absolute directories
387
+ */
388
+ function scanWorkspaceManifestDirs(projectRoot) {
389
+ const dirs = [];
345
390
  const walk = (dir, depth) => {
346
- if (depth > 6) return;
391
+ if (depth > _WORKSPACE_SCAN_MAX_DEPTH) return;
347
392
  let entries;
348
393
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
349
394
  catch { return; }
@@ -352,18 +397,125 @@ function workspaceCrateRegistry(projectRoot) {
352
397
  if (_WORKSPACE_SCAN_PRUNE.has(e.name) || e.name.startsWith('.')) continue;
353
398
  walk(path.join(dir, e.name), depth + 1);
354
399
  } else if (e.name === 'Cargo.toml') {
355
- const info = findCargoRoot(dir);
356
- if (info && info.packageName && !registry.has(info.packageName)) {
357
- registry.set(info.packageName, info);
358
- }
400
+ dirs.push(dir);
359
401
  }
360
402
  }
361
403
  };
362
404
  walk(projectRoot, 0);
405
+ return dirs;
406
+ }
407
+
408
+ /**
409
+ * Order-free fingerprint of the Cargo.toml paths file discovery reports, so
410
+ * a staleness check (which walks the tree for new files anyway) sees a
411
+ * manifest appear or disappear without a second walk.
412
+ * @param {string[]} relativeManifestPaths
413
+ */
414
+ function manifestSetFingerprint(relativeManifestPaths) {
415
+ const sorted = [...relativeManifestPaths].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
416
+ return require('crypto').createHash('md5').update(sorted.join('\0')).digest('hex');
417
+ }
418
+
419
+ function _registryFromManifestDirs(dirs) {
420
+ const registry = new Map();
421
+ for (const dir of dirs) {
422
+ const info = findCargoRoot(dir);
423
+ if (info && info.packageName && !registry.has(info.packageName)) {
424
+ registry.set(info.packageName, info);
425
+ }
426
+ }
427
+ return registry;
428
+ }
429
+
430
+ function workspaceCrateRegistry(projectRoot) {
431
+ if (workspaceCrateCache.has(projectRoot)) {
432
+ return workspaceCrateCache.get(projectRoot);
433
+ }
434
+ const registry = _registryFromManifestDirs(scanWorkspaceManifestDirs(projectRoot));
363
435
  workspaceCrateCache.set(projectRoot, registry);
364
436
  return registry;
365
437
  }
366
438
 
439
+ /**
440
+ * Snapshot of the project's Cargo manifests for the index cache: every
441
+ * manifest the registry scan reaches, with its stat identity and the parsed
442
+ * crate facts findCargoRoot derives from it. Also (re)seeds the in-process
443
+ * caches from a fresh read.
444
+ * @param {string} projectRoot
445
+ * @returns {Array<{dir, mtime, size, packageName, srcDir, targetDirs}>} project-relative
446
+ */
447
+ function snapshotWorkspaceManifests(projectRoot) {
448
+ resetCargoCaches(projectRoot);
449
+ const dirs = scanWorkspaceManifestDirs(projectRoot);
450
+ const rel = p => path.relative(projectRoot, p);
451
+ const out = [];
452
+ for (const dir of dirs) {
453
+ let stat;
454
+ try { stat = fs.statSync(path.join(dir, 'Cargo.toml')); } catch { continue; }
455
+ const info = findCargoRoot(dir);
456
+ if (!info || info.root !== dir) continue;
457
+ out.push({
458
+ dir: rel(dir), mtime: stat.mtimeMs, size: stat.size,
459
+ packageName: info.packageName,
460
+ srcDir: rel(info.srcDir),
461
+ targetDirs: info.targetDirs.map(rel),
462
+ });
463
+ }
464
+ workspaceCrateCache.set(projectRoot, _registryFromManifestDirs(dirs));
465
+ return out;
466
+ }
467
+
468
+ /**
469
+ * Seed the in-process manifest caches from a persisted snapshot, so a
470
+ * cache-loaded index resolves workspace crates without reading the tree.
471
+ */
472
+ function seedWorkspaceManifests(projectRoot, manifests) {
473
+ if (!Array.isArray(manifests)) return;
474
+ const dirs = [];
475
+ for (const m of manifests) {
476
+ const dir = path.join(projectRoot, m.dir);
477
+ dirs.push(dir);
478
+ cargoCache.set(dir, {
479
+ root: dir,
480
+ srcDir: path.join(projectRoot, m.srcDir),
481
+ packageName: m.packageName,
482
+ targetDirs: m.targetDirs.map(t => path.join(projectRoot, t)),
483
+ });
484
+ }
485
+ workspaceCrateCache.set(projectRoot, _registryFromManifestDirs(dirs));
486
+ }
487
+
488
+ /**
489
+ * Whether a persisted manifest snapshot still describes the tree: every
490
+ * manifest keeps its size and mtime, and (when discovery results are given)
491
+ * discovery reports the same set of Cargo.toml paths as when it was taken.
492
+ * @param {string} projectRoot
493
+ * @param {Array} manifests - persisted snapshot
494
+ * @param {string[]|null} [discovered] - project-relative Cargo.toml paths discovery saw now
495
+ * @param {string|null} [seenFingerprint] - manifestSetFingerprint at snapshot time
496
+ */
497
+ function workspaceManifestsCurrent(projectRoot, manifests, discovered = null, seenFingerprint = null) {
498
+ if (!Array.isArray(manifests)) return false;
499
+ for (const m of manifests) {
500
+ let stat;
501
+ try { stat = fs.statSync(path.join(projectRoot, m.dir, 'Cargo.toml')); } catch { return false; }
502
+ if (stat.size !== m.size || stat.mtimeMs !== m.mtime) return false;
503
+ }
504
+ if (discovered && seenFingerprint &&
505
+ manifestSetFingerprint(discovered) !== seenFingerprint) return false;
506
+ return true;
507
+ }
508
+
509
+ /** Drop in-process manifest facts below a project root (a manifest changed). */
510
+ function resetCargoCaches(projectRoot) {
511
+ workspaceCrateCache.delete(projectRoot);
512
+ rustImportMemo.clear();
513
+ const prefix = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
514
+ for (const key of [...cargoCache.keys()]) {
515
+ if (key === projectRoot || key.startsWith(prefix)) cargoCache.delete(key);
516
+ }
517
+ }
518
+
367
519
  /**
368
520
  * Find the nearest Cargo.toml and return the crate's source root
369
521
  * @param {string} startDir - Directory to start searching from
@@ -451,13 +603,30 @@ function rustPathHasExactCase(base, file) {
451
603
  // Check directory entries, rather than realpath, so legitimate symlinked
452
604
  // modules keep their declared spelling and remain resolvable.
453
605
  let current = base;
454
- try {
455
- for (const part of path.relative(base, file).split(path.sep)) {
456
- if (!fs.readdirSync(current).includes(part)) return false;
457
- current = path.join(current, part);
606
+ for (const part of path.relative(base, file).split(path.sep)) {
607
+ const entries = rustDirEntries(current);
608
+ if (!entries || !entries.has(part)) return false;
609
+ current = path.join(current, part);
610
+ }
611
+ return true;
612
+ }
613
+
614
+ // Directory listings the case check reads, memoized with the resolution memo
615
+ // below (fix #375: every module probe re-read its directories; ~40% of a
616
+ // large workspace's import graph build).
617
+ const rustDirEntriesMemo = new Map();
618
+
619
+ function rustDirEntries(dir) {
620
+ let entries = rustDirEntriesMemo.get(dir);
621
+ if (entries === undefined) {
622
+ try {
623
+ entries = new Set(fs.readdirSync(dir));
624
+ } catch {
625
+ entries = null;
458
626
  }
459
- return true;
460
- } catch { return false; }
627
+ rustDirEntriesMemo.set(dir, entries);
628
+ }
629
+ return entries;
461
630
  }
462
631
 
463
632
  function resolveRustModulePath(dir, segments) {
@@ -486,7 +655,27 @@ function resolveRustModulePath(dir, segments) {
486
655
  * @param {string} projectRoot - Project root directory
487
656
  * @returns {string|null}
488
657
  */
658
+ // Resolution memo (fix #372): a query resolves the same use paths from the
659
+ // same files many times (receiver typing, module producers, glob walks), each
660
+ // probing the filesystem. Answers depend only on the tree, so they are
661
+ // memoized until the next build (resetRustResolveMemo).
662
+ const rustImportMemo = new Map();
663
+
489
664
  function resolveRustImport(importPath, fromFile, projectRoot) {
665
+ const key = `${projectRoot}\0${fromFile}\0${importPath}`;
666
+ if (rustImportMemo.has(key)) return rustImportMemo.get(key);
667
+ const resolved = resolveRustImportUncached(importPath, fromFile, projectRoot);
668
+ rustImportMemo.set(key, resolved);
669
+ return resolved;
670
+ }
671
+
672
+ /** Forget memoized Rust import resolutions (the tree may have changed). */
673
+ function resetRustResolveMemo() {
674
+ rustImportMemo.clear();
675
+ rustDirEntriesMemo.clear();
676
+ }
677
+
678
+ function resolveRustImportUncached(importPath, fromFile, projectRoot) {
490
679
  const fromDir = path.dirname(fromFile);
491
680
 
492
681
  // crate:: paths - resolve from the crate's src/ directory, or from a
@@ -626,6 +815,55 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
626
815
  return null;
627
816
  }
628
817
 
818
+ /**
819
+ * The file that holds a Rust module's own items (fix #368): `self` is the
820
+ * current file, `super` (repeatable) the parent module's file, `crate` the
821
+ * crate root, and any other path the module file resolveRustImport finds.
822
+ * Used to anchor glob imports (`use super::*`) and module-qualified calls
823
+ * (`crate::f()`), whose items live IN the module file, not in a child file.
824
+ * @returns {string|null}
825
+ */
826
+ function resolveRustModuleFile(modulePath, fromFile, projectRoot, inlineDepth = 0) {
827
+ let spec = String(modulePath || '');
828
+ if (!spec) return null;
829
+ if (spec === 'self') return fromFile;
830
+ // Inside `mod tests { use super::*; }` the first `super` hops out of the
831
+ // inline module, which still lives in this file.
832
+ let depth = inlineDepth;
833
+ while (depth > 0 && (spec === 'super' || spec.startsWith('super::'))) {
834
+ spec = spec === 'super' ? 'self' : spec.slice('super::'.length);
835
+ depth--;
836
+ }
837
+ if (spec === 'self') return fromFile;
838
+ if (depth > 0) return null; // crate/self paths from inside an inline module
839
+ if (spec.startsWith('self::')) return resolveRustImport(spec, fromFile, projectRoot);
840
+ const fromDir = path.dirname(fromFile);
841
+ if (spec === 'crate') {
842
+ const cargo = findCargoRoot(fromDir);
843
+ if (!cargo) return null;
844
+ const candidates = [cargo.srcDir, ...(cargo.targetDirs || [])]
845
+ .filter(d => fromDir === d || fromDir.startsWith(d + path.sep))
846
+ .sort((a, b) => b.length - a.length);
847
+ if (candidates.length === 0) candidates.push(cargo.srcDir);
848
+ for (const cand of candidates) {
849
+ for (const root of ['lib.rs', 'main.rs']) {
850
+ const file = path.join(cand, root);
851
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+ if (/^super(::super)*$/.test(spec)) {
857
+ const superCount = spec.split('::').length;
858
+ const basename = path.basename(fromFile);
859
+ const isMod = basename === 'mod.rs' || basename === 'lib.rs' || basename === 'main.rs';
860
+ let dir = fromDir;
861
+ for (let i = 0; i < (isMod ? superCount : superCount - 1); i++) dir = path.dirname(dir);
862
+ return rustModuleOwnFile(dir, fromFile);
863
+ }
864
+ return resolveRustImport(spec, fromFile, projectRoot);
865
+ }
866
+
629
867
  /**
630
868
  * Try to resolve a path with various extensions
631
869
  */
@@ -652,6 +890,8 @@ function _findPackageJson(fromDir, stopDir) {
652
890
  exports: pkg.exports,
653
891
  main: pkg.main,
654
892
  source: pkg.source,
893
+ module: pkg.module,
894
+ types: pkg.types,
655
895
  };
656
896
  }
657
897
  } catch { /* unreadable or invalid JSON */ }
@@ -694,6 +934,63 @@ function resolveSelfReference(importPath, fromDir, config) {
694
934
  const pkg = _findPackageJson(fromDir, config.root ? path.dirname(config.root) : null);
695
935
  if (!pkg || !pkg.name) return null;
696
936
  if (importPath !== pkg.name && !importPath.startsWith(pkg.name + '/')) return null;
937
+ return _resolvePackageSpecifier(pkg, importPath, config);
938
+ }
939
+
940
+ /**
941
+ * In-repository workspace packages (fix #397, TanStack-query-measured: a
942
+ * subclass in packages/solid-query extending `QueryClient` imported from
943
+ * '@tanstack/query-core' left every inherited call unresolved): a bare
944
+ * specifier naming a package whose package.json sits inside the project
945
+ * resolves to that package's source, the way the workspace links it.
946
+ * `packages` maps each package name declared exactly once in the project to
947
+ * its manifest facts (a name declared by two manifests stays unresolved).
948
+ */
949
+ function resolveWorkspacePackage(importPath, config) {
950
+ const packages = config.workspacePackages;
951
+ if (!packages || packages.size === 0) return null;
952
+ let name = importPath;
953
+ for (;;) {
954
+ let pkg = packages.get(name);
955
+ if (pkg?.manifestPending) {
956
+ // Restored from a cache: the manifest is read on first use.
957
+ const manifest = _findPackageJson(pkg.dir, pkg.dir);
958
+ pkg = manifest ? { ...manifest, workspace: true } : { ambiguous: true };
959
+ packages.set(name, pkg);
960
+ }
961
+ if (pkg) return pkg.ambiguous ? null : _resolvePackageSpecifier(pkg, importPath, config);
962
+ const slash = name.lastIndexOf('/');
963
+ if (slash <= 0) return null;
964
+ name = name.slice(0, slash);
965
+ }
966
+ }
967
+
968
+ /**
969
+ * The package manifests of a project's JS/TS directories: the nearest
970
+ * package.json of every directory holding a JS/TS source file, inside the
971
+ * project root. Returns Map name -> manifest facts ({ ambiguous: true } for a
972
+ * name two manifests declare).
973
+ * @param {string} root - project root
974
+ * @param {Iterable<string>} dirs - directories of the project's JS/TS files
975
+ */
976
+ function jsWorkspacePackages(root, dirs) {
977
+ const packages = new Map();
978
+ const seenDirs = new Set();
979
+ const seenManifests = new Set();
980
+ for (const dir of dirs) {
981
+ if (seenDirs.has(dir)) continue;
982
+ seenDirs.add(dir);
983
+ const pkg = _findPackageJson(dir, root);
984
+ if (!pkg || !pkg.name || seenManifests.has(pkg.dir)) continue;
985
+ seenManifests.add(pkg.dir);
986
+ if (pkg.dir !== root && !pkg.dir.startsWith(root + path.sep)) continue;
987
+ const existing = packages.get(pkg.name);
988
+ packages.set(pkg.name, existing ? { ambiguous: true } : { ...pkg, workspace: true });
989
+ }
990
+ return packages;
991
+ }
992
+
993
+ function _resolvePackageSpecifier(pkg, importPath, config) {
697
994
  const subpath = importPath === pkg.name ? '.' : './' + importPath.slice(pkg.name.length + 1);
698
995
  const extensions = config.extensions || getExtensions(config.language);
699
996
  const tryTargets = (entry, wildcard) => {
@@ -733,16 +1030,29 @@ function resolveSelfReference(importPath, fromDir, config) {
733
1030
  const source = resolveFilePath(path.resolve(pkg.dir, pkg.source), extensions);
734
1031
  if (source) return source;
735
1032
  }
1033
+ if (subpath === '.' && pkg.workspace) return _workspaceSourceEntry(pkg, extensions);
736
1034
  return null;
737
1035
  }
738
1036
  // No exports map: bare name -> main/index; subpath -> direct file
739
1037
  if (subpath === '.') {
740
1038
  return (pkg.main && resolveFilePath(path.resolve(pkg.dir, pkg.main), extensions)) ||
1039
+ (pkg.workspace && _workspaceSourceEntry(pkg, extensions)) ||
741
1040
  resolveFilePath(path.resolve(pkg.dir, 'index'), extensions);
742
1041
  }
743
1042
  return resolveFilePath(path.resolve(pkg.dir, subpath), extensions);
744
1043
  }
745
1044
 
1045
+ // A workspace package whose `main` names a build artifact absent from the
1046
+ // source checkout: its declared source/module/types entries, else src/index.
1047
+ function _workspaceSourceEntry(pkg, extensions) {
1048
+ for (const entry of [pkg.source, pkg.module, pkg.types]) {
1049
+ if (typeof entry !== 'string') continue;
1050
+ const hit = resolveFilePath(path.resolve(pkg.dir, entry), extensions);
1051
+ if (hit && !hit.endsWith('.d.ts')) return hit;
1052
+ }
1053
+ return resolveFilePath(path.resolve(pkg.dir, 'src', 'index'), extensions);
1054
+ }
1055
+
746
1056
  function resolveFilePath(basePath, extensions) {
747
1057
  // Check exact path
748
1058
  if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
@@ -779,13 +1089,61 @@ function resolveFilePath(basePath, extensions) {
779
1089
  return null;
780
1090
  }
781
1091
 
1092
+ function _probeCached(cache, key, probe) {
1093
+ if (!cache) return probe();
1094
+ if (cache.has(key)) return cache.get(key);
1095
+ const value = probe();
1096
+ cache.set(key, value);
1097
+ return value;
1098
+ }
1099
+
1100
+ function _isFile(p) {
1101
+ try { return fs.statSync(p).isFile(); } catch { return false; }
1102
+ }
1103
+
1104
+ function _isDir(p) {
1105
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
1106
+ }
1107
+
1108
+ /**
1109
+ * Resolve an absolute Python module against the nearest non-package ancestor
1110
+ * of the importing file (fix #366). Walks from the importing directory up to
1111
+ * (excluding) the project root, which the caller already tried.
1112
+ */
1113
+ function _resolvePythonNestedRoot(importPath, fromDir, config) {
1114
+ const root = path.resolve(config.root);
1115
+ const first = importPath.split('.')[0];
1116
+ if (!first) return null;
1117
+ const cache = config.probeCache;
1118
+ const modulePath = importPath.replace(/\./g, '/');
1119
+ let dir = path.resolve(fromDir);
1120
+ while (dir.length > root.length && dir.startsWith(root + path.sep)) {
1121
+ const isPackage = _probeCached(cache, `pyinit\0${dir}`,
1122
+ () => _isFile(path.join(dir, '__init__.py')));
1123
+ if (!isPackage) {
1124
+ const hasFirst = _probeCached(cache, `pyfirst\0${dir}\0${first}`,
1125
+ () => _isDir(path.join(dir, first)) || _isFile(path.join(dir, first + '.py')) ||
1126
+ _isFile(path.join(dir, first + '.pyi')));
1127
+ if (hasFirst) {
1128
+ const resolved = resolveFilePath(path.join(dir, modulePath), getExtensions('python'));
1129
+ if (resolved) return resolved;
1130
+ }
1131
+ }
1132
+ dir = path.dirname(dir);
1133
+ }
1134
+ return null;
1135
+ }
1136
+
782
1137
  /**
783
1138
  * Get file extensions for a language
784
1139
  */
785
1140
  function getExtensions(language) {
786
1141
  switch (language) {
787
1142
  case 'javascript':
788
- return ['.js', '.jsx', '.mjs', '.cjs'];
1143
+ // A JS file's extensionless specifier names a TS source when that
1144
+ // is the only file there (JS tests of a TS package run through a
1145
+ // TS-aware loader, fix #397); JS files win when both exist.
1146
+ return ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'];
789
1147
  case 'typescript':
790
1148
  case 'tsx':
791
1149
  return ['.ts', '.tsx', '.js', '.jsx'];
@@ -978,11 +1336,115 @@ function stripJsonComments(content) {
978
1336
  return result.replace(/,(\s*[}\]])/g, '$1');
979
1337
  }
980
1338
 
1339
+ /**
1340
+ * The inline `mod NAME { ... }` blocks of a Rust file (module symbols that
1341
+ * are not `mod NAME;` declarations), as { name, startLine, endLine }.
1342
+ */
1343
+ function rustInlineModules(fileEntry) {
1344
+ const declared = new Set();
1345
+ for (const detail of fileEntry.importDetails || []) {
1346
+ if (detail.type === 'mod' && detail.module) declared.add(`${detail.module}\0${detail.line}`);
1347
+ }
1348
+ const inline = [];
1349
+ for (const symbol of fileEntry.symbols || []) {
1350
+ if (symbol.type !== 'module' || declared.has(`${symbol.name}\0${symbol.startLine}`)) continue;
1351
+ inline.push({ name: symbol.name, startLine: symbol.startLine, endLine: symbol.endLine });
1352
+ }
1353
+ inline.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
1354
+ return inline;
1355
+ }
1356
+
1357
+ /** Names of the inline modules enclosing `line`, outermost first. */
1358
+ function rustInlineChainAt(inlineModules, line) {
1359
+ const chain = [];
1360
+ for (const mod of inlineModules) {
1361
+ if (mod.startLine <= line && mod.endLine >= line) chain.push(mod.name);
1362
+ }
1363
+ return chain;
1364
+ }
1365
+
1366
+ /**
1367
+ * Files a Rust `mod NAME;` declaration loads (fix #377), per the Rust
1368
+ * reference: in a mod-rs file (lib.rs, main.rs, mod.rs) the module directory
1369
+ * is the file's directory, in any other file `foo.rs` it is `foo/`; enclosing
1370
+ * inline modules add their names; `#[path = "p"]` is relative to the file's
1371
+ * directory outside inline modules and to the module directory inside them.
1372
+ * A non-mod-rs file can itself be a crate root (src/bin/x.rs, tests/x.rs,
1373
+ * build.rs), whose module directory is the file's directory: that layout is
1374
+ * the fallback when the first one names no indexed file. `exists(path)`
1375
+ * decides which candidate is loaded. Returns Map name -> [files] (several
1376
+ * files for cfg-selected alternatives of one name).
1377
+ */
1378
+ function rustModDeclarationFiles(filePath, fileEntry, exists, inlineModules = null) {
1379
+ const out = new Map();
1380
+ const details = (fileEntry.importDetails || []).filter(detail => detail.type === 'mod' && detail.module);
1381
+ if (details.length === 0) return out;
1382
+ const inline = inlineModules || rustInlineModules(fileEntry);
1383
+ const moduleSymbols = new Map();
1384
+ for (const symbol of fileEntry.symbols || []) {
1385
+ if (symbol.type === 'module') moduleSymbols.set(`${symbol.name}\0${symbol.startLine}`, symbol);
1386
+ }
1387
+ const dir = path.dirname(filePath);
1388
+ const base = path.basename(filePath);
1389
+ const modRs = base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs';
1390
+ const layouts = modRs ? [dir] : [path.join(dir, path.basename(filePath, '.rs')), dir];
1391
+ for (const detail of details) {
1392
+ const decl = moduleSymbols.get(`${detail.module}\0${detail.line}`);
1393
+ let pathAttr = null;
1394
+ for (const modifier of decl?.modifiers || []) {
1395
+ const match = /^path\s*=\s*"([^"]*)"$/.exec(String(modifier).trim());
1396
+ if (match) pathAttr = match[1];
1397
+ }
1398
+ const chain = rustInlineChainAt(inline, detail.line);
1399
+ let hit = null;
1400
+ for (const layout of layouts) {
1401
+ const moduleDir = path.join(layout, ...chain);
1402
+ const candidates = pathAttr != null
1403
+ ? [chain.length === 0 ? path.join(dir, pathAttr) : path.join(moduleDir, pathAttr)]
1404
+ : [path.join(moduleDir, detail.module + '.rs'), path.join(moduleDir, detail.module, 'mod.rs')];
1405
+ hit = candidates.find(candidate => candidate !== filePath && exists(candidate));
1406
+ if (hit) break;
1407
+ }
1408
+ if (!hit) continue;
1409
+ if (!out.has(detail.module)) out.set(detail.module, []);
1410
+ if (!out.get(detail.module).includes(hit)) out.get(detail.module).push(hit);
1411
+ }
1412
+ return out;
1413
+ }
1414
+
1415
+ /**
1416
+ * `moduleResolved` entries of a Rust file's `mod` declarations, resolved as
1417
+ * the import graph build resolves them (rustModDeclarationFiles against the
1418
+ * indexed files). `index` needs root and files (fix #375: macro expansion
1419
+ * workers derive module ancestry while the main thread builds the import
1420
+ * graph).
1421
+ */
1422
+ function rustModuleDeclarationsResolved(index, filePath, fileEntry) {
1423
+ const out = {};
1424
+ const declared = rustModDeclarationFiles(filePath, fileEntry, candidate => index.files.has(candidate));
1425
+ for (const [name, files] of declared) out[name] = path.relative(index.root, files[0]);
1426
+ return out;
1427
+ }
1428
+
981
1429
  module.exports = {
982
1430
  extractImports,
1431
+ jsWorkspacePackages,
1432
+ rustModuleDeclarationsResolved,
1433
+ rustModDeclarationFiles,
1434
+ rustInlineModules,
1435
+ rustInlineChainAt,
983
1436
  extractExports,
984
1437
  resolveImport,
985
1438
  resolveFilePath,
986
1439
  resolveRustImport,
987
- findGoModule
1440
+ resolveRustModuleFile,
1441
+ findGoModule,
1442
+ findCargoRoot,
1443
+ workspaceCrateRegistry,
1444
+ snapshotWorkspaceManifests,
1445
+ seedWorkspaceManifests,
1446
+ workspaceManifestsCurrent,
1447
+ manifestSetFingerprint,
1448
+ resetCargoCaches,
1449
+ resetRustResolveMemo,
988
1450
  };