weavatrix 0.2.13 → 0.2.15

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 (145) hide show
  1. package/README.md +99 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.15.md +37 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +14 -71
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/conventions.js +16 -0
  20. package/src/analysis/dependency/scoped-dependencies.js +45 -0
  21. package/src/analysis/dependency/source-references.js +21 -0
  22. package/src/analysis/duplicates.tokenize.js +12 -2
  23. package/src/analysis/endpoints/common.js +62 -0
  24. package/src/analysis/endpoints/extract.js +107 -0
  25. package/src/analysis/endpoints/inventory.js +84 -0
  26. package/src/analysis/endpoints/mounts.js +129 -0
  27. package/src/analysis/endpoints-java.js +80 -3
  28. package/src/analysis/endpoints.js +3 -448
  29. package/src/analysis/git-history/analytics.js +177 -0
  30. package/src/analysis/git-history/collector.js +109 -0
  31. package/src/analysis/git-history/options.js +68 -0
  32. package/src/analysis/git-history.js +128 -577
  33. package/src/analysis/health-capabilities.js +82 -0
  34. package/src/analysis/http-contracts/analysis.js +169 -0
  35. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  36. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  37. package/src/analysis/http-contracts/graph-context.js +143 -0
  38. package/src/analysis/http-contracts/matching.js +61 -0
  39. package/src/analysis/http-contracts/shared.js +86 -0
  40. package/src/analysis/http-contracts.js +6 -822
  41. package/src/analysis/internal-audit/dependency-health.js +111 -0
  42. package/src/analysis/internal-audit/python-manifests.js +65 -0
  43. package/src/analysis/internal-audit/repo-files.js +55 -0
  44. package/src/analysis/internal-audit/supply-chain.js +132 -0
  45. package/src/analysis/internal-audit.collect.js +5 -106
  46. package/src/analysis/internal-audit.reach.js +20 -0
  47. package/src/analysis/internal-audit.run.js +114 -200
  48. package/src/analysis/jvm-dependency-evidence.js +69 -0
  49. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  50. package/src/analysis/source-correctness.js +225 -0
  51. package/src/analysis/structure/dependency-graph.js +156 -0
  52. package/src/analysis/structure/findings.js +142 -0
  53. package/src/analysis/task-retrieval.js +3 -14
  54. package/src/graph/builder/go-receiver-resolution.js +112 -0
  55. package/src/graph/builder/js/queries.js +48 -0
  56. package/src/graph/builder/lang-go.js +89 -4
  57. package/src/graph/builder/lang-js.js +4 -44
  58. package/src/graph/builder/pass2-resolution.js +68 -0
  59. package/src/graph/freshness-probe.js +2 -1
  60. package/src/graph/incremental-refresh.js +3 -2
  61. package/src/graph/internal-builder.build.js +22 -183
  62. package/src/graph/internal-builder.pass2.js +161 -0
  63. package/src/graph/internal-builder.resolvers.js +2 -105
  64. package/src/graph/resolvers/rust.js +117 -0
  65. package/src/mcp/actions/advisories.mjs +18 -0
  66. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  67. package/src/mcp/actions/graph-sync.mjs +195 -0
  68. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  69. package/src/mcp/architecture-bootstrap.mjs +168 -0
  70. package/src/mcp/architecture-starter.mjs +234 -0
  71. package/src/mcp/catalog.mjs +20 -6
  72. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  73. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  74. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  75. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  76. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  77. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  78. package/src/mcp/git-output.mjs +10 -0
  79. package/src/mcp/graph/context-core.mjs +111 -0
  80. package/src/mcp/graph/context-seeds.mjs +197 -0
  81. package/src/mcp/graph/context-state.mjs +86 -0
  82. package/src/mcp/graph/reverse-reach.mjs +42 -0
  83. package/src/mcp/graph/tools-core.mjs +143 -0
  84. package/src/mcp/graph/tools-query.mjs +223 -0
  85. package/src/mcp/graph-context.mjs +4 -496
  86. package/src/mcp/health/audit-format.mjs +172 -0
  87. package/src/mcp/health/audit.mjs +171 -0
  88. package/src/mcp/health/dead-code.mjs +110 -0
  89. package/src/mcp/health/duplicates.mjs +83 -0
  90. package/src/mcp/health/endpoints.mjs +42 -0
  91. package/src/mcp/health/structure.mjs +219 -0
  92. package/src/mcp/runtime-version.mjs +32 -0
  93. package/src/mcp/server/auto-refresh.mjs +66 -0
  94. package/src/mcp/server/runtime-config.mjs +43 -0
  95. package/src/mcp/server/shutdown.mjs +40 -0
  96. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  97. package/src/mcp/sync/evidence-common.mjs +88 -0
  98. package/src/mcp/sync/evidence-duplicates.mjs +97 -0
  99. package/src/mcp/sync/evidence-health.mjs +56 -0
  100. package/src/mcp/sync/evidence-packages.mjs +95 -0
  101. package/src/mcp/sync/payload-common.mjs +97 -0
  102. package/src/mcp/sync/payload-v2.mjs +53 -0
  103. package/src/mcp/sync/payload-v3.mjs +79 -0
  104. package/src/mcp/sync-evidence.mjs +7 -402
  105. package/src/mcp/sync-payload.mjs +10 -244
  106. package/src/mcp/tools-actions.mjs +18 -414
  107. package/src/mcp/tools-architecture.mjs +18 -70
  108. package/src/mcp/tools-context.mjs +56 -15
  109. package/src/mcp/tools-endpoints.mjs +4 -1
  110. package/src/mcp/tools-graph.mjs +17 -331
  111. package/src/mcp/tools-health.mjs +24 -705
  112. package/src/mcp/tools-impact-change.mjs +2 -38
  113. package/src/mcp/tools-impact.mjs +2 -43
  114. package/src/mcp-server.mjs +35 -146
  115. package/src/path-classification.js +14 -0
  116. package/src/precision/lsp-client/constants.js +12 -0
  117. package/src/precision/lsp-client/environment.js +20 -0
  118. package/src/precision/lsp-client/errors.js +15 -0
  119. package/src/precision/lsp-client/lifecycle.js +127 -0
  120. package/src/precision/lsp-client/message-parser.js +81 -0
  121. package/src/precision/lsp-client/protocol.js +212 -0
  122. package/src/precision/lsp-client/registry.js +58 -0
  123. package/src/precision/lsp-client/repo-uri.js +60 -0
  124. package/src/precision/lsp-client/stdio-client.js +151 -0
  125. package/src/precision/lsp-client.js +10 -682
  126. package/src/precision/lsp-overlay/build.js +238 -0
  127. package/src/precision/lsp-overlay/contract.js +61 -0
  128. package/src/precision/lsp-overlay/reference-results.js +108 -0
  129. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  130. package/src/precision/lsp-overlay/source-session.js +134 -0
  131. package/src/precision/lsp-overlay/store.js +150 -0
  132. package/src/precision/lsp-overlay/target-index.js +147 -0
  133. package/src/precision/lsp-overlay/target-query.js +55 -0
  134. package/src/precision/lsp-overlay.js +11 -868
  135. package/src/precision/typescript-lsp-provider.js +12 -682
  136. package/src/precision/typescript-provider/client.js +69 -0
  137. package/src/precision/typescript-provider/discovery.js +124 -0
  138. package/src/precision/typescript-provider/project-host.js +249 -0
  139. package/src/precision/typescript-provider/project-safety.js +161 -0
  140. package/src/precision/typescript-provider/reference-usage.js +53 -0
  141. package/src/security/malware-heuristics.sweep.js +1 -1
  142. package/src/security/registry-sig.classify.js +2 -1
  143. package/src/security/registry-sig.rules.js +3 -3
  144. package/src/version.js +7 -0
  145. package/docs/releases/v0.2.13.md +0 -48
@@ -8,11 +8,17 @@ import { specToPkg } from "./builder/spec-pkg.js";
8
8
  import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
9
9
  import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
10
10
  import { buildResolvers } from "./internal-builder.resolvers.js";
11
- import { addJavaReferences } from "./internal-builder.java.js";
12
11
  import { assignDeterministicCommunities } from "./community.js";
13
- import { resolveJsBarrels } from "./internal-builder.barrels.js";
14
12
  import { snapshotRepository } from "./incremental-refresh.js";
15
13
  import { EDGE_PROVENANCE_V, stampEdgeProvenance } from "./edge-provenance.js";
14
+ import {runInternalGraphPass2} from './internal-builder.pass2.js'
15
+
16
+ function physicalLineCount(text) {
17
+ if (!text.length) return 0;
18
+ let lines = 1;
19
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) lines++;
20
+ return text.endsWith("\n") ? lines - 1 : lines;
21
+ }
16
22
 
17
23
  // Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
18
24
  export async function buildInternalGraph(repoDir, opts = {}) {
@@ -78,10 +84,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
78
84
  let _parsed = 0;
79
85
  for (const abs of files) {
80
86
  const fileRel = rel(abs); const ext = extname(abs);
81
- addNode({ id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" });
82
- if (isDataFile(fileRel) || isDocFile(fileRel)) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; } // config/infra/docs file-only node
87
+ const fileNode = { id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" };
88
+ if (isDataFile(fileRel) || isDocFile(fileRel)) {
89
+ addNode(fileNode); perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue;
90
+ } // config/infra/docs file-only node
91
+ let code; try { code = readFileSync(abs, "utf8"); } catch { addNode(fileNode); continue; }
92
+ addNode({ ...fileNode, physical_loc: physicalLineCount(code) });
83
93
  const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
84
- let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
85
94
  // giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
86
95
  if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; }
87
96
  if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
@@ -144,7 +153,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
144
153
  ...(extra && extra.symbolSpace ? { symbol_space: extra.symbolSpace } : {}),
145
154
  ...(extra && extra.memberOf ? { member_of: extra.memberOf } : {}),
146
155
  ...(extra && extra.visibility ? { visibility: extra.visibility } : {}),
147
- ...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {})
156
+ ...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {}),
157
+ ...(extra?.receiverType ? { receiver_type: extra.receiverType } : {}),
158
+ ...(extra?.returnType ? { return_type: extra.returnType } : {}),
159
+ ...(extra?.fieldTypes && Object.keys(extra.fieldTypes).length ? { field_types: extra.fieldTypes } : {})
148
160
  });
149
161
  links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
150
162
  syms.push({
@@ -216,184 +228,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
216
228
  // ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
217
229
  // C# gets the same treatment: one folder ≈ one namespace by convention, and `using` names namespaces,
218
230
  // not files, so the folder map is the only reliable cross-file resolver) ----
219
- const goDirSymbols = new Map();
220
- const sharesDirScope = (fr) => fr.endsWith(".go") || fr.endsWith(".cs") || fr.endsWith(".rs");
221
- for (const [fr, m] of symByFileName) {
222
- if (!sharesDirScope(fr)) continue;
223
- const d = fr.includes("/") ? fr.slice(0, fr.lastIndexOf("/")) : "";
224
- let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
225
- for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
226
- }
227
- const inferredSymbolSpace = (node) => {
228
- const explicit = String(node?.symbol_space || "");
229
- if (["value", "type", "both"].includes(explicit)) return explicit;
230
- const kind = String(node?.symbol_kind || "").toLowerCase();
231
- if (["interface", "type"].includes(kind)) return "type";
232
- if (["class", "enum"].includes(kind)) return "both";
233
- return "value";
234
- };
235
- const resolveNamedSymbol = (file, name, space = "value") => {
236
- const ids = symIdsByFileName.get(file)?.get(name) || [];
237
- const accepts = (id) => {
238
- const symbolSpace = inferredSymbolSpace(nodeById.get(id));
239
- return symbolSpace === "both" || symbolSpace === space;
240
- };
241
- const exact = ids.find((id) => inferredSymbolSpace(nodeById.get(id)) === space);
242
- return exact || ids.find(accepts) || null;
243
- };
244
- const { resolveNamespaceMember, reExportOccurrences } = resolveJsBarrels({
245
- jsExports, importedLocals, links,
246
- resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? "type" : "value"),
231
+ const {reExportOccurrences} = runInternalGraphPass2({
232
+ files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
233
+ symIdsByFileName, importedLocals, jsExports,
247
234
  });
248
- // Exact source ranges can overlap (a named function nested inside another function). Attribute a call
249
- // to the innermost matching symbol, not whichever outer declaration happened to be added first.
250
- const enclosing = (fileRel, line) => {
251
- let best = null;
252
- for (const s of perFileSymbols.get(fileRel) || []) {
253
- if (line < s.start || line > s.end) continue;
254
- if (!best || s.start > best.start || (s.start === best.start && s.end < best.end)) best = s;
255
- }
256
- return best;
257
- };
258
- const resolveCall = (name, fileRel) => {
259
- const local = resolveNamedSymbol(fileRel, name, "value"); if (local) return local;
260
- if (sharesDirScope(fileRel)) { const d = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : ""; const dm = goDirSymbols.get(d); if (dm && dm.has(name)) return dm.get(name); }
261
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
262
- if (imp && imp.targetFile) {
263
- const targetFile = imp.originFile || imp.targetFile;
264
- const importedName = imp.originName || imp.imported;
265
- return resolveNamedSymbol(targetFile, importedName, "value");
266
- }
267
- return null;
268
- };
269
- const javaTypeKinds = new Set(["class", "interface", "enum", "record", "annotation"]);
270
- const resolveJavaType = (name, fileRel) => {
271
- const imp = importedLocals.get(fileRel)?.get(name);
272
- if (imp?.targetFile) {
273
- const symbols = symByFileName.get(imp.targetFile);
274
- const target = symbols?.get(imp.imported) || symbols?.get(name);
275
- if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target;
276
- }
277
- const target = symByFileName.get(fileRel)?.get(name);
278
- return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null;
279
- };
280
- for (const abs of files) {
281
- const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
282
- const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
283
- let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
284
- const parser = new Parser(); parser.setLanguage(langs[grammar]);
285
- let tree; try { tree = parser.parse(code); } catch { continue; }
286
- if (!lang.customCalls) for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
287
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
288
- const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
289
- links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
290
- }
291
- if (typeof lang.pass2 === "function") {
292
- try {
293
- lang.pass2({
294
- grammar, tree, fileRel, code, caps, field, enclosing, links, nodeById,
295
- perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
296
- });
297
- } catch (e) { /* one language-specific resolver never sinks the graph */ void e; }
298
- }
299
- // qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
300
- // SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
301
- if (lang.selectorCall) for (const cap of caps(grammar, lang.selectorCall, tree.rootNode)) {
302
- const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
303
- if (!operand || operand.type !== "identifier" || !fld) continue;
304
- const caller = enclosing(fileRel, sel.startPosition.row + 1); if (!caller) continue;
305
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text);
306
- const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
307
- const dm = goDirSymbols.get(imp && imp.targetDir ? imp.targetDir : dir);
308
- const target = dm && dm.get(fld.text);
309
- if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
310
- }
311
- for (const heritageSpec of lang.heritage || []) {
312
- const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
313
- const relation = typeof heritageSpec === "string" ? "inherits" : (heritageSpec.relation || "inherits");
314
- for (const cap of caps(grammar, query, tree.rootNode)) {
315
- const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
316
- const target = FAMILY[grammar] === "java" ? resolveJavaType(cap.node.text, fileRel) : resolveCall(cap.node.text, fileRel);
317
- if (target && target !== cls.id) links.push({ source: cls.id, target, relation, confidence: "INFERRED" });
318
- }
319
- }
320
- // JSX is a real symbol use even though it is not a call_expression. Resolve imported components to their
321
- // declaration so component fan-in and unused-export checks do not claim `<SettingsView />` is unreferenced.
322
- if (FAMILY[grammar] === "js") {
323
- // Namespace calls (`ui.run()`) need the member name before an export-star facade can be resolved.
324
- for (const cap of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
325
- const object = field(cap.node, "object"), property = field(cap.node, "property");
326
- if (!object || object.type !== "identifier" || !property) continue;
327
- const imp = importedLocals.get(fileRel)?.get(object.text);
328
- if (!imp || !["*", "default"].includes(imp.imported) || imp.typeOnly) continue;
329
- const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
330
- if (origin.status !== "resolved") continue;
331
- const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, "value");
332
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
333
- if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
334
- }
335
- for (const cap of caps(grammar, `[
336
- (jsx_opening_element name: (_) @jsx)
337
- (jsx_self_closing_element name: (_) @jsx)
338
- ]`, tree.rootNode)) {
339
- const jsxName = cap.node.text;
340
- const parts = jsxName.split(".");
341
- const localName = parts[0];
342
- if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue; // undotted lowercase tags are platform/intrinsic elements
343
- const imp = importedLocals.get(fileRel)?.get(localName);
344
- if (!imp || !imp.targetFile || imp.typeOnly) continue;
345
- let targetFile = imp.originFile || imp.targetFile;
346
- let importedName = imp.originName || imp.imported;
347
- if (imp.imported === "*" && parts.length > 1) {
348
- const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
349
- if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
350
- }
351
- const target = resolveNamedSymbol(targetFile, importedName, "value") || resolveNamedSymbol(targetFile, localName, "value");
352
- if (!target) continue;
353
- const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
354
- links.push({ source: owner?.id || fileRel, target, relation: "references", confidence: "INFERRED", usage: "jsx", line: cap.node.startPosition.row + 1 });
355
- }
356
- // Type identifiers are a separate namespace in TypeScript. Resolve them to type/both-space
357
- // declarations without creating runtime calls or keeping a same-named value symbol alive.
358
- if (grammar !== "javascript") for (const cap of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
359
- const name = cap.node.text;
360
- const imp = importedLocals.get(fileRel)?.get(name);
361
- const targetFile = imp?.originFile || imp?.targetFile || fileRel;
362
- const targetName = imp?.originName || imp?.imported || name;
363
- const target = resolveNamedSymbol(targetFile, targetName, "type");
364
- if (!target || String(target).startsWith(`${fileRel}#${name}@${cap.node.startPosition.row + 1}`)) continue;
365
- const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
366
- const source = owner?.id || fileRel;
367
- if (source === target) continue;
368
- links.push({source, target, relation: "references", confidence: "EXTRACTED", provenance: "RESOLVED", typeOnly: true, line: cap.node.startPosition.row + 1, usage: "type"});
369
- }
370
- }
371
- // Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
372
- // another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
373
- // not falsely DEAD. (Same-file usage is already covered by graph-builder-analysis localRefs.) Go-only for now.
374
- if (FAMILY[grammar] === "go") {
375
- const refSeen = new Set();
376
- const emitRef = (src, target) => { if (!target || target === src) return; const k = src + ">" + target; if (refSeen.has(k)) return; refSeen.add(k); links.push({ source: src, target, relation: "references", confidence: "INFERRED" }); };
377
- const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
378
- const dm = goDirSymbols.get(dir);
379
- for (const cap of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) { // type_identifier → struct/type usage
380
- const target = dm && dm.get(cap.node.text); if (!target || target.slice(0, target.indexOf("#")) === fileRel) continue;
381
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
382
- }
383
- for (const cap of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
384
- const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
385
- if (!operand || operand.type !== "identifier" || !fld) continue;
386
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text); if (!imp || !imp.targetDir) continue;
387
- const tdm = goDirSymbols.get(imp.targetDir); const target = tdm && tdm.get(fld.text); if (!target) continue;
388
- const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
389
- }
390
- }
391
- if (FAMILY[grammar] === "java") {
392
- addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links });
393
- }
394
- tree.delete();
395
- }
396
-
397
235
  // HTML class/id usage → the CSS file(s) defining that selector: file-level reference edges (deduped per pair).
398
236
  const htmlRefSeen = new Set();
399
237
  for (const u of htmlUsages) {
@@ -422,6 +260,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
422
260
  edgeTypesV: 2,
423
261
  edgeProvenanceV: EDGE_PROVENANCE_V,
424
262
  complexityV: 2,
263
+ physicalFileLocV: 1,
425
264
  repoBoundaryV: 1,
426
265
  barrelResolutionV: 1,
427
266
  reExportOccurrencesV: 1,
@@ -0,0 +1,161 @@
1
+ import {readFileSync} from 'node:fs'
2
+ import {extname} from 'node:path'
3
+ import {Parser, EXT_LANG, FAMILY, LANGS} from './internal-builder.langs.js'
4
+ import {addJavaReferences} from './internal-builder.java.js'
5
+ import {resolveJsBarrels} from './internal-builder.barrels.js'
6
+ import {createPass2Resolution} from './builder/pass2-resolution.js'
7
+ import {createGoReceiverResolution} from './builder/go-receiver-resolution.js'
8
+
9
+ export function runInternalGraphPass2({
10
+ files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
11
+ symIdsByFileName, importedLocals, jsExports,
12
+ }) {
13
+ const resolution = createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName})
14
+ const {dirSymbols, resolveNamedSymbol, resolveCall, resolveJavaType} = resolution
15
+ const {resolveNamespaceMember, reExportOccurrences} = resolveJsBarrels({
16
+ jsExports, importedLocals, links,
17
+ resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? 'type' : 'value'),
18
+ })
19
+ const enclosing = (file, line) => {
20
+ let best = null
21
+ for (const symbol of perFileSymbols.get(file) || []) {
22
+ if (line < symbol.start || line > symbol.end) continue
23
+ if (!best || symbol.start > best.start || (symbol.start === best.start && symbol.end < best.end)) best = symbol
24
+ }
25
+ return best
26
+ }
27
+ const goReceivers = createGoReceiverResolution({resolution, nodeById, importedLocals})
28
+ for (const absolute of files) {
29
+ const file = rel(absolute), grammar = EXT_LANG[extname(absolute)]
30
+ if (!grammar) continue
31
+ const lang = LANGS[FAMILY[grammar]]
32
+ if (!lang || lang.isWeb || !langs[grammar]) continue
33
+ let code
34
+ try { code = readFileSync(absolute, 'utf8') } catch { continue }
35
+ const parser = new Parser()
36
+ parser.setLanguage(langs[grammar])
37
+ let tree
38
+ try { tree = parser.parse(code) } catch { continue }
39
+ if (!lang.customCalls) for (const capture of caps(grammar, lang.calls, tree.rootNode)) {
40
+ const caller = enclosing(file, capture.node.startPosition.row + 1)
41
+ if (!caller) continue
42
+ const target = resolveCall(capture.node.text, file)
43
+ if (target && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
44
+ }
45
+ if (typeof lang.pass2 === 'function') try {
46
+ lang.pass2({
47
+ grammar, tree, fileRel: file, code, caps, field, enclosing, links, nodeById,
48
+ perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
49
+ })
50
+ } catch { /* one language-specific resolver never sinks the graph */ }
51
+ if (lang.selectorCall) for (const capture of caps(grammar, lang.selectorCall, tree.rootNode)) {
52
+ const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
53
+ if (!operand || !member) continue
54
+ const caller = enclosing(file, selector.startPosition.row + 1)
55
+ if (!caller) continue
56
+ const bindings = goReceivers.receiverBindings(selector, file)
57
+ const imported = operand.type === 'identifier' && !bindings.has(operand.text)
58
+ ? importedLocals.get(file)?.get(operand.text) : null
59
+ let target = imported?.targetDir ? dirSymbols.get(imported.targetDir)?.get(member.text) : null
60
+ let resolved = !!target
61
+ if (!target) {
62
+ let receiver = null
63
+ if (operand.type === 'identifier') receiver = bindings.get(operand.text) || null
64
+ else if (operand.type === 'selector_expression') {
65
+ const base = field(operand, 'operand')
66
+ const baseType = base?.type === 'identifier' ? bindings.get(base.text) : null
67
+ receiver = goReceivers.fieldType(baseType, field(operand, 'field')?.text)
68
+ }
69
+ target = goReceivers.exactMethod(receiver, member.text)
70
+ resolved = !!target
71
+ if (!target && !receiver) target = goReceivers.uniqueMethod(goReceivers.dirOf(file), member.text)
72
+ }
73
+ if (target && target !== caller.id) links.push({
74
+ source: caller.id, target, relation: 'calls', confidence: 'INFERRED',
75
+ ...(resolved ? {provenance: 'RESOLVED'} : {}), line: selector.startPosition.row + 1,
76
+ })
77
+ }
78
+ for (const heritageSpec of lang.heritage || []) {
79
+ const query = typeof heritageSpec === 'string' ? heritageSpec : heritageSpec.query
80
+ const relation = typeof heritageSpec === 'string' ? 'inherits' : (heritageSpec.relation || 'inherits')
81
+ for (const capture of caps(grammar, query, tree.rootNode)) {
82
+ const owner = enclosing(file, capture.node.startPosition.row + 1)
83
+ if (!owner) continue
84
+ const target = FAMILY[grammar] === 'java' ? resolveJavaType(capture.node.text, file) : resolveCall(capture.node.text, file)
85
+ if (target && target !== owner.id) links.push({source: owner.id, target, relation, confidence: 'INFERRED'})
86
+ }
87
+ }
88
+ if (FAMILY[grammar] === 'js') addJavaScriptReferences({
89
+ grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember,
90
+ resolveNamedSymbol, enclosing, links,
91
+ })
92
+ if (FAMILY[grammar] === 'go') addGoReferences({
93
+ grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links,
94
+ })
95
+ if (FAMILY[grammar] === 'java') addJavaReferences({grammar, tree, fileRel: file, caps, resolveJavaType, enclosing, links})
96
+ tree.delete()
97
+ }
98
+ return {reExportOccurrences}
99
+ }
100
+
101
+ function addJavaScriptReferences({grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember, resolveNamedSymbol, enclosing, links}) {
102
+ for (const capture of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
103
+ const object = field(capture.node, 'object'), property = field(capture.node, 'property')
104
+ if (!object || object.type !== 'identifier' || !property) continue
105
+ const imported = importedLocals.get(file)?.get(object.text)
106
+ if (!imported || !['*', 'default'].includes(imported.imported) || imported.typeOnly) continue
107
+ const origin = resolveNamespaceMember(file, imported, property.text, 'call')
108
+ if (origin.status !== 'resolved') continue
109
+ const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, 'value')
110
+ const caller = enclosing(file, capture.node.startPosition.row + 1)
111
+ if (target && caller && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
112
+ }
113
+ for (const capture of caps(grammar, `[(jsx_opening_element name: (_) @jsx) (jsx_self_closing_element name: (_) @jsx)]`, tree.rootNode)) {
114
+ const parts = capture.node.text.split('.'), localName = parts[0]
115
+ if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue
116
+ const imported = importedLocals.get(file)?.get(localName)
117
+ if (!imported?.targetFile || imported.typeOnly) continue
118
+ let targetFile = imported.originFile || imported.targetFile
119
+ let importedName = imported.originName || imported.imported
120
+ if (imported.imported === '*' && parts.length > 1) {
121
+ const origin = resolveNamespaceMember(file, imported, parts.at(-1), 'jsx')
122
+ if (origin.status === 'resolved') { targetFile = origin.origin.file; importedName = origin.origin.name }
123
+ }
124
+ const target = resolveNamedSymbol(targetFile, importedName, 'value') || resolveNamedSymbol(targetFile, localName, 'value')
125
+ if (!target) continue
126
+ const owner = enclosing(file, capture.node.startPosition.row + 1)
127
+ links.push({source: owner?.id || file, target, relation: 'references', confidence: 'INFERRED', usage: 'jsx', line: capture.node.startPosition.row + 1})
128
+ }
129
+ if (grammar === 'javascript') return
130
+ for (const capture of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
131
+ const name = capture.node.text, imported = importedLocals.get(file)?.get(name)
132
+ const targetFile = imported?.originFile || imported?.targetFile || file
133
+ const targetName = imported?.originName || imported?.imported || name
134
+ const target = resolveNamedSymbol(targetFile, targetName, 'type')
135
+ if (!target || String(target).startsWith(`${file}#${name}@${capture.node.startPosition.row + 1}`)) continue
136
+ const source = enclosing(file, capture.node.startPosition.row + 1)?.id || file
137
+ if (source !== target) links.push({source, target, relation: 'references', confidence: 'EXTRACTED', provenance: 'RESOLVED', typeOnly: true, line: capture.node.startPosition.row + 1, usage: 'type'})
138
+ }
139
+ }
140
+
141
+ function addGoReferences({grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links}) {
142
+ const seen = new Set()
143
+ const emit = (source, target) => {
144
+ const key = `${source}>${target}`
145
+ if (!target || target === source || seen.has(key)) return
146
+ seen.add(key); links.push({source, target, relation: 'references', confidence: 'INFERRED'})
147
+ }
148
+ const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '', symbols = dirSymbols.get(dir)
149
+ for (const capture of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) {
150
+ const target = symbols?.get(capture.node.text)
151
+ if (!target || target.slice(0, target.indexOf('#')) === file) continue
152
+ emit(enclosing(file, capture.node.startPosition.row + 1)?.id || file, target)
153
+ }
154
+ for (const capture of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
155
+ const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
156
+ if (!operand || operand.type !== 'identifier' || !member) continue
157
+ const imported = importedLocals.get(file)?.get(operand.text)
158
+ const target = imported?.targetDir && dirSymbols.get(imported.targetDir)?.get(member.text)
159
+ if (target) emit(enclosing(file, selector.startPosition.row + 1)?.id || file, target)
160
+ }
161
+ }
@@ -6,6 +6,7 @@ import { readFileSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { parseGoMod } from "../analysis/manifests.js";
8
8
  import { createRepoBoundary } from "../repo-path.js";
9
+ import { createRustResolvers } from "./resolvers/rust.js";
9
10
 
10
11
  export function buildResolvers(repoDir, fileSet) {
11
12
  const boundary = createRepoBoundary(repoDir);
@@ -48,111 +49,7 @@ export function buildResolvers(repoDir, fileSet) {
48
49
  return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
49
50
  };
50
51
 
51
- // Rust modules are files, but their paths are logical rather than simple source-relative imports:
52
- // `foo.rs` owns children below `foo/`, while lib.rs/main.rs/mod.rs own siblings. Keep the resolver
53
- // filesystem-only and crate-local: Cargo/external dependencies belong to dependency analysis, not to
54
- // the internal module graph.
55
- const cleanRustRel = (p) => String(p || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "").replace(/^\.$/, "");
56
- const rustDir = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? "" : p.slice(0, i); };
57
- const rustBase = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? p : p.slice(i + 1); };
58
- const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((x) => x != null && x !== "")));
59
- const rustFiles = new Set([...fileSet].filter((fr) => fr.endsWith(".rs")));
60
- const rustRoots = new Map();
61
- for (const fr of rustFiles) {
62
- const base = rustBase(fr);
63
- if (base !== "lib.rs" && base !== "main.rs") continue;
64
- const dir = rustDir(fr);
65
- let root = rustRoots.get(dir);
66
- if (!root) rustRoots.set(dir, (root = { base: dir, lib: null, main: null }));
67
- root[base === "lib.rs" ? "lib" : "main"] = fr;
68
- }
69
- const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length);
70
- const rustContext = (fromRel) => {
71
- fromRel = cleanRustRel(fromRel);
72
- const base = rustBase(fromRel);
73
- const dir = rustDir(fromRel);
74
- if (base === "lib.rs" || base === "main.rs") return { base: dir, rootFile: fromRel };
75
-
76
- // Cargo treats direct files in these conventional folders as independent crate roots. This only
77
- // affects paths originating in the root file itself; nested module ownership still comes from mod/use.
78
- if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return { base: dir, rootFile: fromRel };
79
- for (const root of rustRootList) {
80
- if (!root.base || fromRel.startsWith(root.base + "/")) return { base: root.base, rootFile: root.lib || root.main };
81
- }
82
- return { base: dir, rootFile: fromRel };
83
- };
84
- const rustModuleBase = (fromRel) => {
85
- const ctx = rustContext(fromRel);
86
- if (ctx.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel);
87
- const name = rustBase(fromRel);
88
- if (name === "lib.rs" || name === "main.rs" || name === "mod.rs") return rustDir(fromRel);
89
- return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ""));
90
- };
91
- const rustInlineBase = (fromRel, inlineModules = []) => {
92
- let base = rustModuleBase(fromRel);
93
- for (let i = 0; i < inlineModules.length; i++) {
94
- const mod = inlineModules[i] || {};
95
- if (mod.path) {
96
- // Rust Reference: a path attribute on the first inline module is relative to the source file's
97
- // directory; nested attributes are relative to their containing module's search directory.
98
- const parent = i === 0 ? rustDir(fromRel) : base;
99
- base = rustJoin(parent, mod.path);
100
- } else base = rustJoin(base, mod.name);
101
- }
102
- return base;
103
- };
104
- const rustModuleFile = (moduleBase, ctx) => {
105
- moduleBase = cleanRustRel(moduleBase);
106
- if (moduleBase === cleanRustRel(ctx.base) && ctx.rootFile && rustFiles.has(ctx.rootFile)) return ctx.rootFile;
107
- const flat = moduleBase + ".rs";
108
- if (rustFiles.has(flat)) return flat;
109
- const legacy = rustJoin(moduleBase, "mod.rs");
110
- return rustFiles.has(legacy) ? legacy : null;
111
- };
112
- const resolveRustMod = (fromRel, name, { inlineModules = [], explicitPath = "" } = {}) => {
113
- fromRel = cleanRustRel(fromRel);
114
- if (explicitPath) {
115
- const parent = inlineModules.length ? rustInlineBase(fromRel, inlineModules) : rustDir(fromRel);
116
- const target = rustJoin(parent, explicitPath);
117
- return rustFiles.has(target) ? target : null;
118
- }
119
- const targetBase = rustJoin(rustInlineBase(fromRel, inlineModules), String(name || "").replace(/^r#/, ""));
120
- return rustModuleFile(targetBase, rustContext(fromRel));
121
- };
122
- const resolveRustPath = (fromRel, rawSegments, { inlineModules = [], unqualified = true } = {}) => {
123
- fromRel = cleanRustRel(fromRel);
124
- const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || "").split("::"))
125
- .map((s) => String(s).trim().replace(/^r#/, "")).filter(Boolean);
126
- if (!segments.length) return null;
127
- const ctx = rustContext(fromRel);
128
- const current = rustInlineBase(fromRel, inlineModules);
129
- let rest = [...segments];
130
- const starts = [];
131
- let anchored = false;
132
- if (rest[0] === "crate") { anchored = true; rest.shift(); starts.push(ctx.base); }
133
- else if (rest[0] === "self") { anchored = true; rest.shift(); starts.push(current); }
134
- else if (rest[0] === "super") {
135
- anchored = true;
136
- let base = current;
137
- while (rest[0] === "super") { rest.shift(); base = rustDir(base); }
138
- if (ctx.base && base !== ctx.base && !base.startsWith(ctx.base + "/")) return null;
139
- starts.push(base);
140
- } else if (unqualified) {
141
- // Rust 2018 resolves a bare use path from the crate root/external prelude. Prefer an internal root
142
- // module, then allow a lexically-local module for qualified expressions in nested modules.
143
- starts.push(ctx.base);
144
- if (current !== ctx.base) starts.push(current);
145
- } else return null;
146
-
147
- for (const start of starts) {
148
- const min = anchored ? 0 : 1; // never reinterpret an unresolved external `serde::X` as the crate root
149
- for (let used = rest.length; used >= min; used--) {
150
- const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), ctx);
151
- if (target) return { targetFile: target, consumed: segments.length - rest.length + used, remaining: rest.slice(used), anchored };
152
- }
153
- }
154
- return null;
155
- };
52
+ const { resolveRustMod, resolveRustPath } = createRustResolvers(fileSet);
156
53
 
157
54
  // Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
158
55
  // Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
@@ -0,0 +1,117 @@
1
+ import {join} from 'node:path'
2
+
3
+ const cleanRustRel = (path) => String(path || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/').replace(/\/$/, '').replace(/^\.$/, '')
4
+ const rustDir = (path) => {
5
+ const clean = cleanRustRel(path)
6
+ const index = clean.lastIndexOf('/')
7
+ return index < 0 ? '' : clean.slice(0, index)
8
+ }
9
+ const rustBase = (path) => {
10
+ const clean = cleanRustRel(path)
11
+ const index = clean.lastIndexOf('/')
12
+ return index < 0 ? clean : clean.slice(index + 1)
13
+ }
14
+ const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((part) => part != null && part !== '')))
15
+
16
+ export function createRustResolvers(fileSet) {
17
+ const rustFiles = new Set([...fileSet].filter((file) => file.endsWith('.rs')))
18
+ const rustRoots = new Map()
19
+ for (const file of rustFiles) {
20
+ const base = rustBase(file)
21
+ if (base !== 'lib.rs' && base !== 'main.rs') continue
22
+ const dir = rustDir(file)
23
+ let root = rustRoots.get(dir)
24
+ if (!root) rustRoots.set(dir, (root = {base: dir, lib: null, main: null}))
25
+ root[base === 'lib.rs' ? 'lib' : 'main'] = file
26
+ }
27
+ const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length)
28
+
29
+ const rustContext = (fromRel) => {
30
+ const clean = cleanRustRel(fromRel)
31
+ const base = rustBase(clean)
32
+ const dir = rustDir(clean)
33
+ if (base === 'lib.rs' || base === 'main.rs') return {base: dir, rootFile: clean}
34
+ if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return {base: dir, rootFile: clean}
35
+ for (const root of rustRootList) {
36
+ if (!root.base || clean.startsWith(root.base + '/')) return {base: root.base, rootFile: root.lib || root.main}
37
+ }
38
+ return {base: dir, rootFile: clean}
39
+ }
40
+ const rustModuleBase = (fromRel) => {
41
+ const context = rustContext(fromRel)
42
+ if (context.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel)
43
+ const name = rustBase(fromRel)
44
+ if (name === 'lib.rs' || name === 'main.rs' || name === 'mod.rs') return rustDir(fromRel)
45
+ return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ''))
46
+ }
47
+ const rustInlineBase = (fromRel, inlineModules = []) => {
48
+ let base = rustModuleBase(fromRel)
49
+ for (let index = 0; index < inlineModules.length; index++) {
50
+ const module = inlineModules[index] || {}
51
+ if (module.path) {
52
+ const parent = index === 0 ? rustDir(fromRel) : base
53
+ base = rustJoin(parent, module.path)
54
+ } else base = rustJoin(base, module.name)
55
+ }
56
+ return base
57
+ }
58
+ const rustModuleFile = (moduleBase, context) => {
59
+ const clean = cleanRustRel(moduleBase)
60
+ if (clean === cleanRustRel(context.base) && context.rootFile && rustFiles.has(context.rootFile)) return context.rootFile
61
+ const flat = clean + '.rs'
62
+ if (rustFiles.has(flat)) return flat
63
+ const legacy = rustJoin(clean, 'mod.rs')
64
+ return rustFiles.has(legacy) ? legacy : null
65
+ }
66
+
67
+ const resolveRustMod = (fromRel, name, {inlineModules = [], explicitPath = ''} = {}) => {
68
+ const clean = cleanRustRel(fromRel)
69
+ if (explicitPath) {
70
+ const parent = inlineModules.length ? rustInlineBase(clean, inlineModules) : rustDir(clean)
71
+ const target = rustJoin(parent, explicitPath)
72
+ return rustFiles.has(target) ? target : null
73
+ }
74
+ const targetBase = rustJoin(rustInlineBase(clean, inlineModules), String(name || '').replace(/^r#/, ''))
75
+ return rustModuleFile(targetBase, rustContext(clean))
76
+ }
77
+
78
+ const resolveRustPath = (fromRel, rawSegments, {inlineModules = [], unqualified = true} = {}) => {
79
+ const clean = cleanRustRel(fromRel)
80
+ const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || '').split('::'))
81
+ .map((segment) => String(segment).trim().replace(/^r#/, '')).filter(Boolean)
82
+ if (!segments.length) return null
83
+ const context = rustContext(clean)
84
+ const current = rustInlineBase(clean, inlineModules)
85
+ const rest = [...segments]
86
+ const starts = []
87
+ let anchored = false
88
+ if (rest[0] === 'crate') { anchored = true; rest.shift(); starts.push(context.base) }
89
+ else if (rest[0] === 'self') { anchored = true; rest.shift(); starts.push(current) }
90
+ else if (rest[0] === 'super') {
91
+ anchored = true
92
+ let base = current
93
+ while (rest[0] === 'super') { rest.shift(); base = rustDir(base) }
94
+ if (context.base && base !== context.base && !base.startsWith(context.base + '/')) return null
95
+ starts.push(base)
96
+ } else if (unqualified) {
97
+ starts.push(context.base)
98
+ if (current !== context.base) starts.push(current)
99
+ } else return null
100
+
101
+ for (const start of starts) {
102
+ const minimum = anchored ? 0 : 1
103
+ for (let used = rest.length; used >= minimum; used--) {
104
+ const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), context)
105
+ if (target) return {
106
+ targetFile: target,
107
+ consumed: segments.length - rest.length + used,
108
+ remaining: rest.slice(used),
109
+ anchored,
110
+ }
111
+ }
112
+ }
113
+ return null
114
+ }
115
+
116
+ return {resolveRustMod, resolveRustPath}
117
+ }