ucn 4.2.3 → 5.0.2

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 (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -7,7 +7,59 @@
7
7
 
8
8
  const path = require('path');
9
9
  const { resolveImport } = require('./imports');
10
- const { langTraits } = require('../languages');
10
+ const { langTraits, getParser, safeParse } = require('../languages');
11
+
12
+ function _javaPackageTypes(index) {
13
+ const packages = new Map();
14
+ for (const [filePath, fileEntry] of index.files) {
15
+ if (fileEntry.language !== 'java') continue;
16
+ for (const symbol of fileEntry.symbols || []) {
17
+ if (!symbol.namespace || !['class', 'interface', 'record', 'enum'].includes(symbol.type)) continue;
18
+ let names = packages.get(symbol.namespace);
19
+ if (!names) { names = new Map(); packages.set(symbol.namespace, names); }
20
+ let files = names.get(symbol.name);
21
+ if (!files) { files = new Set(); names.set(symbol.name, files); }
22
+ files.add(filePath);
23
+ }
24
+ }
25
+ return packages;
26
+ }
27
+
28
+ /** Collect same-package Java type references from AST roles that denote types. */
29
+ function _javaSamePackageDependencies(index, filePath, fileEntry, packageTypes) {
30
+ const packageName = (fileEntry.symbols || []).find(symbol => symbol.namespace)?.namespace;
31
+ const candidates = packageTypes.get(packageName);
32
+ if (!packageName || !candidates || candidates.size === 0) return [];
33
+ let tree;
34
+ try {
35
+ tree = safeParse(getParser('java'), index._readFile(filePath));
36
+ } catch (_) {
37
+ return [];
38
+ }
39
+ const referenced = new Set();
40
+ const walk = node => {
41
+ if (node.type === 'type_identifier' && candidates.has(node.text)) {
42
+ referenced.add(node.text);
43
+ } else if (node.type === 'identifier' && candidates.has(node.text)) {
44
+ const parent = node.parent;
45
+ if (parent && ((parent.type === 'method_invocation' &&
46
+ parent.childForFieldName('object')?.id === node.id) ||
47
+ (parent.type === 'field_access' &&
48
+ parent.childForFieldName('object')?.id === node.id))) {
49
+ referenced.add(node.text);
50
+ }
51
+ }
52
+ for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
53
+ };
54
+ walk(tree.rootNode);
55
+ const out = new Set();
56
+ for (const name of referenced) {
57
+ for (const target of candidates.get(name) || []) {
58
+ if (target !== filePath) out.add(target);
59
+ }
60
+ }
61
+ return [...out];
62
+ }
11
63
 
12
64
  /**
13
65
  * Build directory→files index for O(1) same-package lookups.
@@ -89,6 +141,33 @@ function _resolveJavaPackageImport(index, importModule, javaFileIndex, opts = {}
89
141
  return opts.all ? [] : null;
90
142
  }
91
143
 
144
+ function _buildCSharpNamespaceIndex(index) {
145
+ const namespaces = new Map();
146
+ const add = (key, file) => {
147
+ if (!key) return;
148
+ if (!namespaces.has(key)) namespaces.set(key, []);
149
+ if (!namespaces.get(key).includes(file)) namespaces.get(key).push(file);
150
+ };
151
+ for (const [filePath, fileEntry] of index.files) {
152
+ if (fileEntry.language !== 'csharp') continue;
153
+ for (const symbol of fileEntry.symbols || []) {
154
+ if (!['class', 'interface', 'struct', 'record', 'enum'].includes(symbol.type)) {
155
+ continue;
156
+ }
157
+ add(symbol.namespace, filePath);
158
+ if (symbol.namespace) add(`${symbol.namespace}.${symbol.name}`, filePath);
159
+ }
160
+ }
161
+ return namespaces;
162
+ }
163
+
164
+ function _resolveCSharpUsing(index, importModule, namespaceIndex = null, opts = {}) {
165
+ const map = namespaceIndex || _buildCSharpNamespaceIndex(index);
166
+ const matches = map.get(importModule) || [];
167
+ if (opts.all) return [...matches];
168
+ return matches[0] || null;
169
+ }
170
+
92
171
  /**
93
172
  * Build import/export relationship graphs
94
173
  */
@@ -100,6 +179,9 @@ function buildImportGraph(index) {
100
179
  const dirToGoFiles = new Map();
101
180
  // Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
102
181
  const javaFileIndex = new Map();
182
+ const javaPackageTypes = _javaPackageTypes(index);
183
+ const csharpNamespaceIndex = _buildCSharpNamespaceIndex(index);
184
+ const csharpGlobalImports = new Set();
103
185
  for (const [fp, fe] of index.files) {
104
186
  if (langTraits(fe.language)?.packageScope === 'directory') {
105
187
  const dir = path.dirname(fp);
@@ -109,6 +191,10 @@ function buildImportGraph(index) {
109
191
  const name = path.basename(fp, '.java');
110
192
  if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
111
193
  javaFileIndex.get(name).push(fp);
194
+ } else if (fe.language === 'csharp') {
195
+ for (const moduleName of (fe.globalImports || [])) {
196
+ if (moduleName) csharpGlobalImports.add(moduleName);
197
+ }
112
198
  }
113
199
  }
114
200
 
@@ -123,7 +209,10 @@ function buildImportGraph(index) {
123
209
  // names is not evidence about THIS name's module).
124
210
  const moduleResolved = {};
125
211
 
126
- for (const importModule of fileEntry.imports) {
212
+ const effectiveImports = fileEntry.language === 'csharp'
213
+ ? [...(fileEntry.imports || []), ...csharpGlobalImports]
214
+ : (fileEntry.imports || []);
215
+ for (const importModule of effectiveImports) {
127
216
  // Skip null modules (e.g., dynamic include! macros in Rust)
128
217
  if (!importModule) continue;
129
218
 
@@ -134,6 +223,7 @@ function buildImportGraph(index) {
134
223
 
135
224
  let resolved = resolveImport(importModule, filePath, {
136
225
  aliases: index.config.aliases,
226
+ includePaths: index.config.includePaths,
137
227
  language: fileEntry.language,
138
228
  root: index.root
139
229
  });
@@ -156,18 +246,31 @@ function buildImportGraph(index) {
156
246
  }
157
247
  }
158
248
 
249
+ let csharpFiles = null;
250
+ if (!resolved && fileEntry.language === 'csharp') {
251
+ const all = _resolveCSharpUsing(
252
+ index, importModule, csharpNamespaceIndex, { all: true });
253
+ if (all.length > 0) {
254
+ resolved = all[0];
255
+ csharpFiles = all;
256
+ }
257
+ }
258
+
159
259
  if (resolved && index.files.has(resolved)) {
160
260
  moduleResolved[importModule] = path.relative(index.root, resolved);
161
261
  // For Go, a package import means all files in that directory are dependencies
162
262
  // (Go packages span multiple files in the same directory)
163
- const filesToLink = javaWildcardFiles ? [...javaWildcardFiles] : [resolved];
263
+ const filesToLink = javaWildcardFiles
264
+ ? [...javaWildcardFiles]
265
+ : csharpFiles ? [...csharpFiles] : [resolved];
164
266
  if (langTraits(fileEntry.language)?.packageScope === 'directory') {
165
267
  const pkgDir = path.dirname(resolved);
166
268
  const dirFiles = dirToGoFiles.get(pkgDir) || [];
167
- const importerIsTest = filePath.endsWith('_test.go');
168
269
  for (const fp of dirFiles) {
169
- if (fp !== resolved) {
170
- if (!importerIsTest && fp.endsWith('_test.go')) continue;
270
+ if (fp !== resolved && fp !== filePath) {
271
+ // Test files are compilation inputs, never part of
272
+ // an importable Go package surface.
273
+ if (fp.endsWith('_test.go')) continue;
171
274
  filesToLink.push(fp);
172
275
  }
173
276
  }
@@ -183,6 +286,19 @@ function buildImportGraph(index) {
183
286
  }
184
287
  }
185
288
 
289
+ // Java same-package visibility needs no import declaration. Add only
290
+ // AST-proven type/static-qualifier references, not a package clique.
291
+ if (fileEntry.language === 'java') {
292
+ for (const linkedFile of _javaSamePackageDependencies(
293
+ index, filePath, fileEntry, javaPackageTypes)) {
294
+ importedFiles.add(linkedFile);
295
+ if (!index.exportGraph.has(linkedFile)) {
296
+ index.exportGraph.set(linkedFile, new Set());
297
+ }
298
+ index.exportGraph.get(linkedFile).add(filePath);
299
+ }
300
+ }
301
+
186
302
  // From-import submodules (fix #224): `from . import jobs` binds
187
303
  // jobs.py as a plain NAME — the parser can't know (a from-import name
188
304
  // may be a symbol), the resolver can. Resolve the composed dotted
@@ -260,6 +376,38 @@ function buildInheritanceGraph(index) {
260
376
  const alias = fileEntry.importAliases.find(a => a.local === parent);
261
377
  if (alias && classNames.has(alias.original)) return alias.original;
262
378
  }
379
+ // Nominal languages commonly spell a project base with a
380
+ // namespace/package qualifier (`detail::buffer<T>`,
381
+ // `demo.Base`). The symbol table is keyed by the terminal
382
+ // type name, so retaining the qualifier disconnects the
383
+ // inheritance graph and can turn an inherited method call
384
+ // into a false receiver mismatch. Strip it only when an
385
+ // indexed type has the matching compiler owner; suffix
386
+ // matching accommodates parsers which retain either the
387
+ // full namespace or only its innermost component.
388
+ if (langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
389
+ (parent.includes('::') || parent.includes('.'))) {
390
+ const separator = parent.includes('::') ? '::' : '.';
391
+ const pieces = parent.split(separator).filter(Boolean);
392
+ const terminal = pieces.pop();
393
+ const qualifier = pieces.join(separator);
394
+ const typeKinds = new Set([
395
+ 'class', 'interface', 'struct', 'trait', 'record',
396
+ ]);
397
+ const matches = (index.symbols.get(terminal) || [])
398
+ .filter(definition => {
399
+ if (!typeKinds.has(definition.type)) return false;
400
+ const owner = String(definition.namespace || '');
401
+ return owner === qualifier ||
402
+ owner.endsWith(`${separator}${qualifier}`) ||
403
+ qualifier.endsWith(`${separator}${owner}`);
404
+ });
405
+ if (matches.length > 0 &&
406
+ new Set(matches.map(definition =>
407
+ definition.namespace || '')).size === 1) {
408
+ return terminal;
409
+ }
410
+ }
263
411
  // Qualified structural parent: `class CustomCommand(
264
412
  // click.Command)`. The symbol table owns bare class names,
265
413
  // so keeping `click.Command` makes the subclass invisible
@@ -347,4 +495,11 @@ function splitParentList(clause) {
347
495
  .filter(Boolean);
348
496
  }
349
497
 
350
- module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, splitParentList, _resolveJavaPackageImport };
498
+ module.exports = {
499
+ buildDirIndex,
500
+ buildImportGraph,
501
+ buildInheritanceGraph,
502
+ splitParentList,
503
+ _resolveJavaPackageImport,
504
+ _resolveCSharpUsing,
505
+ };
package/core/graph.js CHANGED
@@ -79,12 +79,16 @@ function imports(index, filePath) {
79
79
  if (!resolvedPath && fileEntry.language === 'java' && !imp.module.startsWith('.')) {
80
80
  resolvedPath = index._resolveJavaPackageImport(imp.module);
81
81
  }
82
+ if (!resolvedPath && fileEntry.language === 'csharp') {
83
+ resolvedPath = index._resolveCSharpUsing(imp.module);
84
+ }
82
85
 
83
86
  return {
84
87
  module: imp.module,
85
88
  names: imp.names,
86
89
  type: imp.type,
87
90
  resolved: resolvedPath ? path.relative(index.root, resolvedPath) : null,
91
+ indexed: !!(resolvedPath && index.files.has(resolvedPath)),
88
92
  isExternal: !resolvedPath,
89
93
  // A string-literal dynamic import (import('./x'), importlib.import_module("x"))
90
94
  // is still mechanically dynamic even when the path resolves —
@@ -107,6 +111,35 @@ function imports(index, filePath) {
107
111
  */
108
112
  function symbolIsExported(symbol, fileEntry, exportedNames) {
109
113
  const modifiers = symbol.modifiers || [];
114
+ // Python's language-level public surface is convention based when the
115
+ // module does not declare __all__: top-level non-underscore names are
116
+ // public. Keep explicit __all__ authoritative when present.
117
+ if (fileEntry.language === 'python' &&
118
+ !(fileEntry.moduleAssignedNames || []).includes('__all__') &&
119
+ !symbol.className && !symbol.isMethod) {
120
+ return !!symbol.name && !symbol.name.startsWith('_');
121
+ }
122
+ if ((fileEntry.language === 'csharp' || fileEntry.language === 'java') &&
123
+ (symbol.className || symbol.enclosingType)) {
124
+ const typeKinds = new Set([
125
+ 'class', 'struct', 'interface', 'record', 'enum', 'type',
126
+ ]);
127
+ const typeIsPublic = (name, namespace, seen = new Set()) => {
128
+ const identity = `${namespace || ''}\0${name}`;
129
+ if (seen.has(identity)) return false;
130
+ seen.add(identity);
131
+ return (fileEntry.symbols || []).some(candidate => {
132
+ if (!typeKinds.has(candidate.type) || candidate.name !== name ||
133
+ (candidate.namespace || null) !== (namespace || null) ||
134
+ !(candidate.modifiers || []).includes('public')) return false;
135
+ return !candidate.enclosingType || typeIsPublic(
136
+ candidate.enclosingType, candidate.namespace,
137
+ new Set(seen));
138
+ });
139
+ };
140
+ if (!typeIsPublic(symbol.className || symbol.enclosingType,
141
+ symbol.namespace)) return false;
142
+ }
110
143
  if (modifiers.includes('export') || modifiers.includes('public')) return true;
111
144
  if (modifiers.some(m => typeof m === 'string' && /^pub\b/.test(m))) return true;
112
145
  if (langTraits(fileEntry.language)?.exportVisibility === 'capitalization' &&
@@ -516,6 +549,8 @@ function fileExports(index, filePath, _visited) {
516
549
  */
517
550
  function api(index, filePath, options = {}) {
518
551
  const results = [];
552
+ let scopedFiles = 0;
553
+ let pythonImplicitFiles = 0;
519
554
 
520
555
  let fileIterator;
521
556
  if (filePath) {
@@ -540,95 +575,33 @@ function api(index, filePath, options = {}) {
540
575
  fileIterator = index.files.entries();
541
576
  }
542
577
 
543
- for (const [, fileEntry] of fileIterator) {
578
+ for (const [absPath, fileEntry] of fileIterator) {
544
579
  if (!fileEntry) continue;
580
+ if (options.in &&
581
+ !index.matchesFilters(fileEntry.relativePath, { in: options.in })) {
582
+ continue;
583
+ }
545
584
 
546
585
  // Skip test files by default (test classes aren't part of public API)
547
586
  if (!options.includeTests && isTestFile(fileEntry.relativePath, fileEntry.language)) {
548
587
  continue;
549
588
  }
550
-
551
- const exportedNames = new Set(fileEntry.exports);
552
-
553
- for (const symbol of fileEntry.symbols) {
554
- if (symbol.type === 'impl') continue;
555
- if (symbolIsExported(symbol, fileEntry, exportedNames)) {
556
- results.push({
557
- name: symbol.name,
558
- type: symbol.type,
559
- file: fileEntry.relativePath,
560
- startLine: symbol.startLine,
561
- endLine: symbol.endLine,
562
- ...(symbol.className && { className: symbol.className }),
563
- params: symbol.params,
564
- returnType: symbol.returnType,
565
- signature: formatExportSignature(index, symbol)
566
- });
567
- }
568
- }
569
-
570
- // Add variable exports (export const/let/var) not matched to symbols
571
- if (fileEntry.exportDetails) {
572
- const matchedNames = new Set(results.filter(r => r.file === fileEntry.relativePath).map(r => r.name));
573
- for (const exp of fileEntry.exportDetails) {
574
- if (exp.isVariable && !matchedNames.has(exp.name)) {
575
- const sig = `${exp.declKind} ${exp.name}${exp.typeAnnotation ? ': ' + exp.typeAnnotation : ''}`;
576
- results.push({
577
- name: exp.name,
578
- type: 'variable',
579
- file: fileEntry.relativePath,
580
- startLine: exp.line,
581
- endLine: exp.line,
582
- params: undefined,
583
- returnType: exp.typeAnnotation || null,
584
- signature: sig
585
- });
586
- matchedNames.add(exp.name);
587
- }
588
- }
589
- // The fix #245 fileExports discipline, api side (fix #251 — the
590
- // two commands diverged on the same file): consumers import the
591
- // ALIAS, and clause-exported names with no indexed symbol
592
- // (class/function expressions) are still API surface.
593
- for (const exp of fileEntry.exportDetails) {
594
- if (!exp || !exp.name || exp.module) continue;
595
- if (exp.alias && exp.alias !== exp.name) {
596
- const entry = results.find(r =>
597
- r.file === fileEntry.relativePath && r.name === exp.name && !r.sourceName);
598
- if (entry) {
599
- entry.sourceName = exp.name;
600
- entry.name = exp.alias;
601
- if (entry.signature) {
602
- entry.signature = entry.signature.replace(exp.name, exp.alias);
603
- }
604
- matchedNames.add(exp.alias);
605
- continue;
606
- }
607
- }
608
- const shown = exp.alias || exp.name;
609
- if (!matchedNames.has(shown) && !matchedNames.has(exp.name) &&
610
- exportedNames.has(exp.name)) {
611
- results.push({
612
- name: shown,
613
- ...(exp.alias && exp.alias !== exp.name && { sourceName: exp.name }),
614
- type: 'export',
615
- file: fileEntry.relativePath,
616
- startLine: exp.line || 1,
617
- endLine: exp.line || 1,
618
- params: undefined,
619
- returnType: null,
620
- signature: shown,
621
- });
622
- matchedNames.add(shown);
623
- }
624
- }
589
+ scopedFiles++;
590
+ if (fileEntry.language === 'python' && fileEntry.exports.length === 0) {
591
+ pythonImplicitFiles++;
625
592
  }
593
+ const exports = fileExports(index, absPath);
594
+ if (Array.isArray(exports)) results.push(...exports);
626
595
  }
627
596
 
628
597
  // Rule 11: (file, line) ordering regardless of parse order — file mode
629
598
  // used to emit symbols in extraction order (fix #251).
630
599
  results.sort((a, b) => codeUnitCompare(a.file, b.file) ||
631
600
  (a.startLine - b.startLine) || codeUnitCompare(a.name, b.name));
601
+ Object.defineProperty(results, 'apiInfo', {
602
+ value: { scopedFiles, pythonImplicitFiles },
603
+ enumerable: false, writable: true, configurable: true,
604
+ });
632
605
  return results;
633
606
  }
634
607
 
@@ -852,4 +825,7 @@ function circularDeps(index, options = {}) {
852
825
  }
853
826
  }
854
827
 
855
- module.exports = { imports, exporters, fileExports, api, graph, circularDeps };
828
+ module.exports = {
829
+ imports, exporters, fileExports, api, graph, circularDeps,
830
+ symbolIsExported,
831
+ };
package/core/imports.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const { getParser, getLanguageModule } = require('../languages');
10
+ const { getParser, getLanguageAdapter } = require('../languages');
11
11
 
12
12
  /**
13
13
  * Extract imports from file content using AST
@@ -20,7 +20,7 @@ function extractImports(content, language) {
20
20
  // Use JS language module for TS/TSX (same import syntax), but the actual language's parser
21
21
  const moduleLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
22
22
 
23
- const langModule = getLanguageModule(moduleLang);
23
+ const langModule = getLanguageAdapter(moduleLang);
24
24
  if (langModule && typeof langModule.findImportsInCode === 'function') {
25
25
  try {
26
26
  const parser = getParser(language);
@@ -45,7 +45,7 @@ function extractExports(content, language) {
45
45
  // Use JS language module for TS/TSX (same export syntax), but the actual language's parser
46
46
  const moduleLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
47
47
 
48
- const langModule = getLanguageModule(moduleLang);
48
+ const langModule = getLanguageAdapter(moduleLang);
49
49
  if (langModule && typeof langModule.findExportsInCode === 'function') {
50
50
  try {
51
51
  const parser = getParser(language);
@@ -180,8 +180,38 @@ function resolveImport(importPath, fromFile, config = {}) {
180
180
  }
181
181
 
182
182
  // Relative imports
183
+ const extensions = config.extensions || getExtensions(config.language);
183
184
  const resolved = path.resolve(fromDir, normalizedPath);
184
- return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
185
+ const direct = resolveFilePath(resolved, extensions);
186
+ if (direct) return direct;
187
+
188
+ // 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.
191
+ 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
+ }
213
+ }
214
+ return null;
185
215
  }
186
216
 
187
217
  // Cache for Go module paths
@@ -484,8 +514,17 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
484
514
  const cargo = findCargoRoot(fromDir);
485
515
  if (cargo && cargo.packageName && firstSeg === cargo.packageName) {
486
516
  const topDir = path.relative(cargo.root, fromFile).split(path.sep)[0];
517
+ const sourceRelative = path.relative(cargo.srcDir, fromFile);
518
+ const sourceHead = sourceRelative.split(path.sep)[0];
519
+ // A package's binaries are separate crates from its library even
520
+ // though both live below src/. `use package_name::Type` in
521
+ // src/main.rs or src/bin/** therefore names the lib target, not a
522
+ // child module of the binary. Treat these like examples/tests so
523
+ // the import graph can pin re-exported library types exactly.
524
+ const separateBinary = sourceRelative === 'main.rs' ||
525
+ sourceHead === 'bin';
487
526
  const externalTarget = topDir === 'tests' || topDir === 'benches' || topDir === 'examples' ||
488
- !fromFile.startsWith(cargo.srcDir + path.sep);
527
+ separateBinary || !fromFile.startsWith(cargo.srcDir + path.sep);
489
528
  if (externalTarget) {
490
529
  const restSegs = importPath.split('::').slice(1);
491
530
  const resolved = restSegs.length > 0 ? resolveRustModulePath(cargo.srcDir, restSegs) : null;
@@ -593,7 +632,13 @@ function _findPackageJson(fromDir, stopDir) {
593
632
  try {
594
633
  if (fs.existsSync(candidate)) {
595
634
  const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
596
- info = { dir: current, name: pkg.name, exports: pkg.exports, main: pkg.main };
635
+ info = {
636
+ dir: current,
637
+ name: pkg.name,
638
+ exports: pkg.exports,
639
+ main: pkg.main,
640
+ source: pkg.source,
641
+ };
597
642
  }
598
643
  } catch { /* unreadable or invalid JSON */ }
599
644
  _pkgCache.set(current, info);
@@ -666,6 +711,14 @@ function resolveSelfReference(importPath, fromDir, config) {
666
711
  if (hit) return hit;
667
712
  }
668
713
  }
714
+ // Monorepo packages commonly export build artifacts that do not
715
+ // exist in a source checkout while declaring the development entry
716
+ // explicitly (`"source": "src/index.ts"`). For a package's own bare
717
+ // import, that source field is the authoritative local module.
718
+ if (subpath === '.' && typeof pkg.source === 'string') {
719
+ const source = resolveFilePath(path.resolve(pkg.dir, pkg.source), extensions);
720
+ if (source) return source;
721
+ }
669
722
  return null;
670
723
  }
671
724
  // No exports map: bare name -> main/index; subpath -> direct file
@@ -730,6 +783,12 @@ function getExtensions(language) {
730
783
  return ['.java'];
731
784
  case 'rust':
732
785
  return ['.rs'];
786
+ case 'c':
787
+ return ['.c', '.h'];
788
+ case 'cpp':
789
+ return ['.cc', '.cpp', '.cxx', '.c++', '.hpp', '.hh', '.hxx', '.h++', '.h'];
790
+ case 'csharp':
791
+ return ['.cs', '.csx'];
733
792
  default:
734
793
  return ['.js', '.ts'];
735
794
  }