@wrongstack/tools 0.299.0 → 0.300.0

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.
@@ -31,7 +31,10 @@ function detectLang(file) {
31
31
  function isIndexablePath(file) {
32
32
  return detectLang(file) !== null;
33
33
  }
34
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
34
+ function languageFamily(lang) {
35
+ return LANG_FAMILY[lang] ?? "other";
36
+ }
37
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
35
38
  var init_languages = __esm({
36
39
  "src/codebase-index/languages.ts"() {
37
40
  "use strict";
@@ -122,6 +125,52 @@ var init_languages = __esm({
122
125
  procfile: "other",
123
126
  justfile: "other"
124
127
  };
128
+ LANG_FAMILY = {
129
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
130
+ // imports from — and is imported by — plain .ts files.
131
+ ts: "js",
132
+ tsx: "js",
133
+ js: "js",
134
+ jsx: "js",
135
+ vue: "js",
136
+ svelte: "js",
137
+ go: "go",
138
+ py: "py",
139
+ rs: "rs",
140
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
141
+ java: "jvm",
142
+ kotlin: "jvm",
143
+ scala: "jvm",
144
+ csharp: "dotnet",
145
+ // A .h header is consumed by both C and C++ translation units.
146
+ c: "c",
147
+ cpp: "c",
148
+ ruby: "ruby",
149
+ php: "php",
150
+ swift: "swift",
151
+ dart: "dart",
152
+ elixir: "elixir",
153
+ haskell: "haskell",
154
+ zig: "zig",
155
+ lua: "lua",
156
+ r: "r",
157
+ shell: "shell",
158
+ sql: "sql",
159
+ json: "data",
160
+ yaml: "data",
161
+ toml: "data",
162
+ html: "web",
163
+ css: "web",
164
+ proto: "proto",
165
+ graphql: "graphql",
166
+ md: "other",
167
+ other: "other"
168
+ };
169
+ LANG_FAMILY_ENTRIES = Object.freeze(
170
+ Object.entries(LANG_FAMILY).map(
171
+ ([lang, family]) => Object.freeze([lang, family])
172
+ )
173
+ );
125
174
  }
126
175
  });
127
176
 
@@ -286,7 +335,7 @@ function getTypeName(name) {
286
335
  function deduplicateRefs(refs) {
287
336
  const seen = /* @__PURE__ */ new Set();
288
337
  return refs.filter((r) => {
289
- const key = `${r.toName}:${r.callType}:${r.line}`;
338
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
290
339
  if (seen.has(key)) return false;
291
340
  seen.add(key);
292
341
  return true;
@@ -296,10 +345,16 @@ function getImportSpecifierName(spec) {
296
345
  return spec.propertyName?.text ?? spec.name.text;
297
346
  }
298
347
  function emitImportSpecifierRefs(node, refs, lineNum) {
348
+ const module = moduleSpecifierOf(node.moduleSpecifier);
299
349
  const clause = node.importClause;
300
- if (!clause) return;
350
+ if (!clause) {
351
+ if (module) {
352
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
353
+ }
354
+ return;
355
+ }
301
356
  if (clause.name) {
302
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
357
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
303
358
  }
304
359
  const bindings = clause.namedBindings;
305
360
  if (!bindings) return;
@@ -309,26 +364,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
309
364
  fromId: 0,
310
365
  toName: getImportSpecifierName(element),
311
366
  callType: "import",
312
- line: lineNum
367
+ line: lineNum,
368
+ module
313
369
  });
314
370
  }
315
371
  } else if (ts.isNamespaceImport(bindings)) {
316
- refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
372
+ refs.push({
373
+ fromId: 0,
374
+ toName: bindings.name.text,
375
+ callType: "import",
376
+ line: lineNum,
377
+ module
378
+ });
317
379
  }
318
380
  }
381
+ function moduleSpecifierOf(node) {
382
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
383
+ }
319
384
  function emitExportSpecifierRefs(node, refs, lineNum) {
385
+ const module = moduleSpecifierOf(node.moduleSpecifier);
320
386
  const clause = node.exportClause;
321
387
  if (clause && ts.isNamespaceExport(clause)) {
322
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
388
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
323
389
  return;
324
390
  }
325
391
  if (clause && ts.isNamedExports(clause)) {
326
392
  for (const element of clause.elements) {
327
393
  const originalName = element.propertyName?.text ?? element.name.text;
328
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
394
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
329
395
  }
330
396
  return;
331
397
  }
398
+ if (module) {
399
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
400
+ }
332
401
  }
333
402
  var ts, tsLoad, kindMapCache;
334
403
  var init_ts_parser = __esm({
@@ -341,21 +410,21 @@ var init_ts_parser = __esm({
341
410
  });
342
411
 
343
412
  // src/_win32-resolve.ts
344
- import * as fs2 from "node:fs";
345
- import * as path3 from "node:path";
413
+ import * as fs3 from "node:fs";
414
+ import * as path5 from "node:path";
346
415
  function resolveWin32Command(cmd) {
347
416
  if (process.platform !== "win32") return cmd;
348
- if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
417
+ if (cmd.includes("/") || cmd.includes("\\") || path5.extname(cmd.replace(/\//g, "\\"))) {
349
418
  return cmd;
350
419
  }
351
420
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
352
- const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
421
+ const pathDirs = (process.env["PATH"] ?? "").split(path5.delimiter);
353
422
  for (const dir of pathDirs) {
354
- const base = path3.join(dir, cmd);
423
+ const base = path5.join(dir, cmd);
355
424
  for (const ext of pathext) {
356
425
  const full = `${base}${ext}`;
357
426
  try {
358
- fs2.accessSync(full, fs2.constants.X_OK);
427
+ fs3.accessSync(full, fs3.constants.X_OK);
359
428
  return full;
360
429
  } catch {
361
430
  }
@@ -369,6 +438,82 @@ var init_win32_resolve = __esm({
369
438
  }
370
439
  });
371
440
 
441
+ // src/codebase-index/parser-output.ts
442
+ function coerceSymbols(value) {
443
+ if (!Array.isArray(value)) return [];
444
+ return value.flatMap((entry) => {
445
+ const candidate = entry;
446
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
447
+ return [
448
+ {
449
+ name: candidate.name,
450
+ kind: candidate.kind,
451
+ line: typeof candidate.line === "number" ? candidate.line : 1,
452
+ col: typeof candidate.col === "number" ? candidate.col : 0,
453
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
454
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
455
+ }
456
+ ];
457
+ });
458
+ }
459
+ function coerceRefs(value, lang) {
460
+ if (!Array.isArray(value)) return [];
461
+ return value.flatMap((entry) => {
462
+ const candidate = entry;
463
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
464
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
465
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
466
+ return [
467
+ {
468
+ fromId: 0,
469
+ toName: candidate.toName,
470
+ callType: candidate.callType,
471
+ line: typeof candidate.line === "number" ? candidate.line : 1,
472
+ lang,
473
+ module
474
+ }
475
+ ];
476
+ });
477
+ }
478
+ function parseParserOutput(stdout, lang) {
479
+ const trimmed = stdout.trim();
480
+ if (!trimmed) return { symbols: [], refs: [] };
481
+ let parsed2;
482
+ try {
483
+ parsed2 = JSON.parse(trimmed);
484
+ } catch {
485
+ return { symbols: [], refs: [] };
486
+ }
487
+ if (Array.isArray(parsed2)) return { symbols: coerceSymbols(parsed2), refs: [] };
488
+ const record = parsed2;
489
+ return {
490
+ symbols: coerceSymbols(record.symbols),
491
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
492
+ };
493
+ }
494
+ function dedupeRefs(refs) {
495
+ const seen = /* @__PURE__ */ new Set();
496
+ return refs.filter((ref) => {
497
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
498
+ if (seen.has(key)) return false;
499
+ seen.add(key);
500
+ return true;
501
+ });
502
+ }
503
+ var CALL_TYPES;
504
+ var init_parser_output = __esm({
505
+ "src/codebase-index/parser-output.ts"() {
506
+ "use strict";
507
+ CALL_TYPES = /* @__PURE__ */ new Set([
508
+ "call",
509
+ "type_ref",
510
+ "inherit",
511
+ "implement",
512
+ "import"
513
+ ]);
514
+ }
515
+ });
516
+
372
517
  // src/codebase-index/spawn-gate.ts
373
518
  function withSpawnGate(fn) {
374
519
  const run = chain.then(fn, fn);
@@ -394,8 +539,8 @@ __export(go_parser_exports, {
394
539
  });
395
540
  import { spawn } from "node:child_process";
396
541
  import * as os from "node:os";
397
- import * as path4 from "node:path";
398
- import * as fs3 from "node:fs/promises";
542
+ import * as path6 from "node:path";
543
+ import * as fs4 from "node:fs/promises";
399
544
  async function parseSymbols2(opts) {
400
545
  const { file, content, lang } = opts;
401
546
  try {
@@ -403,7 +548,8 @@ async function parseSymbols2(opts) {
403
548
  if (parsed2.symbols.length > 0) {
404
549
  return parsed2;
405
550
  }
406
- return fallbackParse(file, content, lang);
551
+ const fallback = fallbackParse(file, content, lang);
552
+ return parsed2.refs?.length ? { ...fallback, refs: parsed2.refs } : fallback;
407
553
  } catch {
408
554
  return fallbackParse(file, content, lang);
409
555
  }
@@ -467,9 +613,9 @@ async function syncGoParse(filePath, content, lang) {
467
613
  try {
468
614
  let scriptPath = _cachedGoScriptPath;
469
615
  if (!scriptPath) {
470
- const tmpDir = await fs3.mkdtemp(path4.join(os.tmpdir(), "ws-go-parse-"));
471
- scriptPath = path4.join(tmpDir, "parse.go");
472
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
616
+ const tmpDir = await fs4.mkdtemp(path6.join(os.tmpdir(), "ws-go-parse-"));
617
+ scriptPath = path6.join(tmpDir, "parse.go");
618
+ await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
473
619
  _cachedGoScriptPath = scriptPath;
474
620
  }
475
621
  const goBinary = resolveWin32Command("go");
@@ -511,8 +657,8 @@ async function syncGoParse(filePath, content, lang) {
511
657
  if (code !== 0 || !stdout.trim()) {
512
658
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
513
659
  }
514
- const raw = JSON.parse(stdout.trim());
515
- const symbols = raw.map((s) => ({
660
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
661
+ const symbols = rawSymbols.map((s) => ({
516
662
  id: 0,
517
663
  lang,
518
664
  kind: s.kind,
@@ -525,7 +671,7 @@ async function syncGoParse(filePath, content, lang) {
525
671
  scope: s.scope ?? "",
526
672
  text: `${s.name} ${s.signature ?? ""}`.trim()
527
673
  }));
528
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
674
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
529
675
  } catch {
530
676
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
531
677
  }
@@ -535,6 +681,7 @@ var init_go_parser = __esm({
535
681
  "src/codebase-index/go-parser.ts"() {
536
682
  "use strict";
537
683
  init_win32_resolve();
684
+ init_parser_output();
538
685
  init_spawn_gate();
539
686
  init_languages();
540
687
  GO_PARSE_SCRIPT = `
@@ -548,6 +695,7 @@ import (
548
695
  "go/token"
549
696
  "io"
550
697
  "os"
698
+ "strconv"
551
699
  "strings"
552
700
  )
553
701
 
@@ -560,16 +708,34 @@ type Sym struct {
560
708
  Scope string \`json:"scope"\`
561
709
  }
562
710
 
711
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
712
+ // yields both. Module is the import path for CallType "import", else empty.
713
+ type Ref struct {
714
+ ToName string \`json:"toName"\`
715
+ CallType string \`json:"callType"\`
716
+ Line int \`json:"line"\`
717
+ Module string \`json:"module"\`
718
+ }
719
+
720
+ type Result struct {
721
+ Symbols []Sym \`json:"symbols"\`
722
+ Refs []Ref \`json:"refs"\`
723
+ }
724
+
725
+ func emptyResult() string {
726
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
727
+ }
728
+
563
729
  func main() {
564
730
  src, err := io.ReadAll(os.Stdin)
565
731
  if err != nil {
566
- fmt.Print("[]")
732
+ fmt.Print(emptyResult())
567
733
  return
568
734
  }
569
735
  fset := token.NewFileSet()
570
736
  node, err := parser.ParseFile(fset, "src.go", src, 0)
571
737
  if err != nil {
572
- fmt.Print("[]")
738
+ fmt.Print(emptyResult())
573
739
  return
574
740
  }
575
741
 
@@ -633,9 +799,43 @@ func main() {
633
799
  }
634
800
  }
635
801
 
636
- data, err := json.Marshal(syms)
802
+ refs := []Ref{}
803
+ ast.Inspect(node, func(n ast.Node) bool {
804
+ switch expr := n.(type) {
805
+ case *ast.CallExpr:
806
+ line := fset.Position(expr.Pos()).Line
807
+ switch fun := expr.Fun.(type) {
808
+ case *ast.Ident:
809
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
810
+ case *ast.SelectorExpr:
811
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
812
+ // declared symbol name, so it resolves the same way the TypeScript
813
+ // and Python extractors' call refs do.
814
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
815
+ }
816
+ case *ast.ImportSpec:
817
+ if expr.Path != nil {
818
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
819
+ line := fset.Position(expr.Pos()).Line
820
+ // A Go import names a package, not a symbol; the package's
821
+ // last path segment is the name it is referenced by.
822
+ name := importPath
823
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
824
+ name = importPath[idx+1:]
825
+ }
826
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
827
+ }
828
+ }
829
+ }
830
+ return true
831
+ })
832
+
833
+ if syms == nil {
834
+ syms = []Sym{}
835
+ }
836
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
637
837
  if err != nil {
638
- fmt.Print("[]")
838
+ fmt.Print(emptyResult())
639
839
  return
640
840
  }
641
841
  fmt.Print(string(data))
@@ -987,9 +1187,13 @@ var init_generic_parser = __esm({
987
1187
  ],
988
1188
  elixir: [
989
1189
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
990
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
1190
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
1191
+ // against this symbol, and a `Foo`-only capture never matches it.
1192
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
991
1193
  ],
992
1194
  haskell: [
1195
+ // Target of `import Data.List`.
1196
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
993
1197
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
994
1198
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
995
1199
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -1080,9 +1284,9 @@ __export(py_parser_exports, {
1080
1284
  parseSymbols: () => parseSymbols4
1081
1285
  });
1082
1286
  import { spawn as spawn2 } from "node:child_process";
1083
- import * as fs4 from "node:fs/promises";
1287
+ import * as fs5 from "node:fs/promises";
1084
1288
  import * as os2 from "node:os";
1085
- import * as path5 from "node:path";
1289
+ import * as path7 from "node:path";
1086
1290
  async function parseSymbols4(opts) {
1087
1291
  const { file, content, lang } = opts;
1088
1292
  try {
@@ -1160,10 +1364,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1160
1364
  async function syncPyParse(filePath, content, lang) {
1161
1365
  try {
1162
1366
  if (!_cachedScriptPath) {
1163
- const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
1164
- await fs4.mkdir(tmpDir, { recursive: true });
1165
- _cachedScriptPath = path5.join(tmpDir, "parse.py");
1166
- await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1367
+ const tmpDir = path7.join(os2.tmpdir(), "ws-py-parse");
1368
+ await fs5.mkdir(tmpDir, { recursive: true });
1369
+ _cachedScriptPath = path7.join(tmpDir, "parse.py");
1370
+ await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1167
1371
  }
1168
1372
  cachedPyBinary ??= resolvePython();
1169
1373
  const pyBinary = await cachedPyBinary;
@@ -1177,7 +1381,7 @@ async function syncPyParse(filePath, content, lang) {
1177
1381
  if (code !== 0 || !stdout.trim()) {
1178
1382
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1179
1383
  }
1180
- const raw = JSON.parse(stdout.trim());
1384
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
1181
1385
  const symbols = raw.map((s) => ({
1182
1386
  id: 0,
1183
1387
  lang,
@@ -1191,7 +1395,7 @@ async function syncPyParse(filePath, content, lang) {
1191
1395
  scope: s.scope ?? "",
1192
1396
  text: `${s.name} ${s.signature ?? ""}`.trim()
1193
1397
  }));
1194
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1398
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
1195
1399
  } catch {
1196
1400
  return null;
1197
1401
  }
@@ -1202,6 +1406,7 @@ var init_py_parser = __esm({
1202
1406
  "use strict";
1203
1407
  init_win32_resolve();
1204
1408
  init_generic_parser();
1409
+ init_parser_output();
1205
1410
  init_spawn_gate();
1206
1411
  init_languages();
1207
1412
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -1263,7 +1468,18 @@ class Sym:
1263
1468
  def is_private(name):
1264
1469
  return name.startswith("__") and not name.endswith("__")
1265
1470
 
1471
+ def leaf_name(node):
1472
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
1473
+ # TypeScript and Go extractors record call refs, so resolution behaves the
1474
+ # same across languages.
1475
+ if isinstance(node, ast.Attribute):
1476
+ return node.attr
1477
+ if isinstance(node, ast.Name):
1478
+ return node.id
1479
+ return get_name(node).split(".")[-1]
1480
+
1266
1481
  syms = []
1482
+ refs = []
1267
1483
  errors = []
1268
1484
 
1269
1485
  try:
@@ -1271,7 +1487,7 @@ try:
1271
1487
  tree = ast.parse(source, filename=sys.argv[1])
1272
1488
  except Exception as e:
1273
1489
  errors.append(str(e))
1274
- print("[]")
1490
+ print(json.dumps({"symbols": [], "refs": []}))
1275
1491
  sys.exit(0)
1276
1492
 
1277
1493
  # Module-level scope
@@ -1405,7 +1621,42 @@ class ModuleVisitor(ast.NodeVisitor):
1405
1621
  visitor = ModuleVisitor()
1406
1622
  visitor.visit(tree)
1407
1623
 
1408
- print(json.dumps([s.to_dict() for s in syms]))
1624
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
1625
+ # into function bodies (it would index locals as symbols), but that is exactly
1626
+ # where the calls are.
1627
+ for node in ast.walk(tree):
1628
+ if isinstance(node, ast.Call):
1629
+ name = leaf_name(node.func)
1630
+ if name:
1631
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
1632
+ elif isinstance(node, ast.Import):
1633
+ for alias in node.names:
1634
+ refs.append({
1635
+ "toName": alias.name.split(".")[-1],
1636
+ "callType": "import",
1637
+ "line": node.lineno,
1638
+ "module": alias.name,
1639
+ })
1640
+ elif isinstance(node, ast.ImportFrom):
1641
+ # PEP 328: node.level is the number of leading dots. Preserving them is
1642
+ # what lets the resolver walk up from the importing file's package \u2014
1643
+ # dropping them made \`from .foo import X\` indistinguishable from an
1644
+ # absolute \`foo\`.
1645
+ module = ("." * (node.level or 0)) + (node.module or "")
1646
+ for alias in node.names:
1647
+ refs.append({
1648
+ "toName": alias.name,
1649
+ "callType": "import",
1650
+ "line": node.lineno,
1651
+ "module": module,
1652
+ })
1653
+ elif isinstance(node, ast.ClassDef):
1654
+ for base in node.bases:
1655
+ name = leaf_name(base)
1656
+ if name:
1657
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1658
+
1659
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1409
1660
  `;
1410
1661
  _cachedScriptPath = null;
1411
1662
  }
@@ -1418,107 +1669,10 @@ __export(rs_parser_exports, {
1418
1669
  parseSymbols: () => parseSymbols5
1419
1670
  });
1420
1671
  import { expectDefined } from "@wrongstack/core/utils";
1421
- import { execFile, spawn as spawn3 } from "node:child_process";
1422
- import * as fs5 from "node:fs/promises";
1423
- import * as path6 from "node:path";
1424
1672
  async function parseSymbols5(opts) {
1425
1673
  const { file, content, lang } = opts;
1426
- const nativeAvailable = await checkNativeParser();
1427
- if (nativeAvailable) {
1428
- const result = await withSpawnGate(() => tryNativeParse(file, content));
1429
- if (result) return result;
1430
- }
1431
1674
  return regexParse({ file, content, lang });
1432
1675
  }
1433
- function probe(command, args) {
1434
- return new Promise((resolve4, reject) => {
1435
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
1436
- if (error) reject(error);
1437
- else resolve4();
1438
- });
1439
- });
1440
- }
1441
- function checkNativeParser() {
1442
- nativeParserAvailability ??= (async () => {
1443
- try {
1444
- await probe("rustc", ["--version"]);
1445
- const toolsDir = path6.join(process.cwd(), "tools");
1446
- await probe(
1447
- "cargo",
1448
- [
1449
- "metadata",
1450
- "--no-deps",
1451
- "--format-version",
1452
- "1",
1453
- "--manifest-path",
1454
- path6.join(toolsDir, "Cargo.toml")
1455
- ]
1456
- );
1457
- return true;
1458
- } catch {
1459
- return false;
1460
- }
1461
- })();
1462
- return nativeParserAvailability;
1463
- }
1464
- async function tryNativeParse(file, content) {
1465
- try {
1466
- const toolsDir = path6.join(process.cwd(), "tools");
1467
- const crateDir = path6.join(toolsDir, "syn-parser");
1468
- const tmpFile = path6.join(crateDir, "src", "input.rs");
1469
- await fs5.writeFile(tmpFile, content, "utf8");
1470
- const cargoBinary = resolveWin32Command("cargo");
1471
- const result = await new Promise(
1472
- (resolve4, reject) => {
1473
- let settled = false;
1474
- const proc = spawn3(
1475
- cargoBinary,
1476
- ["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
1477
- {
1478
- cwd: process.cwd(),
1479
- stdio: ["pipe", "pipe", "pipe"],
1480
- windowsHide: true
1481
- }
1482
- );
1483
- proc.on("error", (err) => {
1484
- if (settled) return;
1485
- settled = true;
1486
- reject(err);
1487
- });
1488
- let stdout2 = "";
1489
- proc.stdout?.on("data", (chunk) => {
1490
- stdout2 += chunk.toString();
1491
- });
1492
- proc.stderr?.resume();
1493
- const timer = setTimeout(() => {
1494
- if (settled) return;
1495
- settled = true;
1496
- proc.kill("SIGKILL");
1497
- reject(new Error("timeout"));
1498
- }, 15e3);
1499
- timer.unref?.();
1500
- proc.on("close", (c) => {
1501
- if (settled) return;
1502
- settled = true;
1503
- clearTimeout(timer);
1504
- resolve4({ code: c, stdout: stdout2 });
1505
- });
1506
- }
1507
- );
1508
- const { code, stdout } = result;
1509
- if (code === 0 && stdout.trim()) {
1510
- const symbols = JSON.parse(stdout.trim());
1511
- return {
1512
- file,
1513
- lang: "rs",
1514
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
1515
- mtimeMs: Date.now()
1516
- };
1517
- }
1518
- } catch {
1519
- }
1520
- return null;
1521
- }
1522
1676
  function regexParse(opts) {
1523
1677
  const { file, content, lang } = opts;
1524
1678
  const symbols = [];
@@ -1574,12 +1728,10 @@ function regexParse(opts) {
1574
1728
  });
1575
1729
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
1576
1730
  }
1577
- var nativeParserAvailability, RS_PATTERNS;
1731
+ var RS_PATTERNS;
1578
1732
  var init_rs_parser = __esm({
1579
1733
  "src/codebase-index/rs-parser.ts"() {
1580
1734
  "use strict";
1581
- init_win32_resolve();
1582
- init_spawn_gate();
1583
1735
  init_languages();
1584
1736
  RS_PATTERNS = [
1585
1737
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -1602,7 +1754,7 @@ __export(json_parser_exports, {
1602
1754
  parseSymbols: () => parseSymbols6
1603
1755
  });
1604
1756
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1605
- import * as path7 from "node:path";
1757
+ import * as path8 from "node:path";
1606
1758
  function parseSymbols6(opts) {
1607
1759
  const { file, content, lang } = opts;
1608
1760
  try {
@@ -1614,7 +1766,7 @@ function parseSymbols6(opts) {
1614
1766
  function regexParse2(opts) {
1615
1767
  const { file, content, lang } = opts;
1616
1768
  const symbols = [];
1617
- const basename6 = path7.basename(file).toLowerCase();
1769
+ const basename6 = path8.basename(file).toLowerCase();
1618
1770
  const isPackageJson = basename6 === "package.json";
1619
1771
  const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
1620
1772
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1640,11 +1792,11 @@ function regexParse2(opts) {
1640
1792
  const line = lineFromOffset(offset);
1641
1793
  symbols.push(
1642
1794
  makeSymbol({
1643
- name: path7.basename(file),
1795
+ name: path8.basename(file),
1644
1796
  kind: "object",
1645
1797
  line,
1646
1798
  col: 0,
1647
- signature: `"${path7.basename(file)}" = { ... }`,
1799
+ signature: `"${path8.basename(file)}" = { ... }`,
1648
1800
  file,
1649
1801
  lang
1650
1802
  })
@@ -1985,7 +2137,7 @@ import {
1985
2137
 
1986
2138
  // src/codebase-index/indexer.ts
1987
2139
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
1988
- import { execFile as execFile2 } from "node:child_process";
2140
+ import { execFile } from "node:child_process";
1989
2141
  import * as fs8 from "node:fs/promises";
1990
2142
  import { availableParallelism } from "node:os";
1991
2143
  import * as path11 from "node:path";
@@ -2053,8 +2205,738 @@ async function loadGitignoreMatcher(projectRoot2) {
2053
2205
  // src/codebase-index/indexer.ts
2054
2206
  init_languages();
2055
2207
 
2208
+ // src/codebase-index/module-resolver.ts
2209
+ init_languages();
2210
+ import * as path4 from "node:path";
2211
+
2212
+ // src/codebase-index/module-roots.ts
2213
+ init_languages();
2214
+ import * as fs2 from "node:fs/promises";
2215
+ import * as path3 from "node:path";
2216
+ function toPortablePath(file) {
2217
+ return file.replace(/\\/g, "/");
2218
+ }
2219
+ async function readTextIfPresent(file) {
2220
+ try {
2221
+ return await fs2.readFile(file, "utf8");
2222
+ } catch {
2223
+ return void 0;
2224
+ }
2225
+ }
2226
+ function parsePackageJsonName(source) {
2227
+ try {
2228
+ const parsed2 = JSON.parse(source);
2229
+ return typeof parsed2.name === "string" && parsed2.name ? parsed2.name : void 0;
2230
+ } catch {
2231
+ return void 0;
2232
+ }
2233
+ }
2234
+ function parseGoModulePath(source) {
2235
+ for (const rawLine of source.split(/\r?\n/)) {
2236
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
2237
+ const match = /^module\s+(\S+)/.exec(line);
2238
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
2239
+ }
2240
+ return void 0;
2241
+ }
2242
+ function parseTomlTableName(source, tables) {
2243
+ let current = "";
2244
+ for (const rawLine of source.split(/\r?\n/)) {
2245
+ const line = rawLine.replace(/#.*$/, "").trim();
2246
+ if (line.startsWith("[[")) {
2247
+ current = "\0";
2248
+ continue;
2249
+ }
2250
+ const table = /^\[([^\]]+)\]$/.exec(line);
2251
+ if (table?.[1]) {
2252
+ current = table[1].trim();
2253
+ continue;
2254
+ }
2255
+ if (!tables.includes(current)) continue;
2256
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
2257
+ if (match?.[1]) return match[1];
2258
+ }
2259
+ return void 0;
2260
+ }
2261
+ function parsePomArtifactId(source) {
2262
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
2263
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
2264
+ }
2265
+ var LANGS_BY_KIND = {
2266
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
2267
+ cargo: ["rs"],
2268
+ go: ["go"],
2269
+ python: ["py"],
2270
+ maven: ["java", "kotlin", "scala"],
2271
+ gradle: ["java", "kotlin", "scala"],
2272
+ dotnet: ["csharp"]
2273
+ };
2274
+ function ancestorsOf(dir, stopAt) {
2275
+ const out = [];
2276
+ let current = dir;
2277
+ for (; ; ) {
2278
+ out.push(current);
2279
+ if (current === stopAt || current.length <= stopAt.length) break;
2280
+ const parent = path3.posix.dirname(current);
2281
+ if (parent === current) break;
2282
+ current = parent;
2283
+ }
2284
+ return out;
2285
+ }
2286
+ var MARKER_PROBES = [
2287
+ {
2288
+ kind: "npm",
2289
+ file: "package.json",
2290
+ build: (dir, source) => {
2291
+ const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
2292
+ return { name, importPath: name, sourceRoots: [dir] };
2293
+ }
2294
+ },
2295
+ {
2296
+ kind: "cargo",
2297
+ file: "Cargo.toml",
2298
+ build: (dir, source) => {
2299
+ const name = parseTomlTableName(source, ["package"]);
2300
+ if (!name) return void 0;
2301
+ return {
2302
+ name: `crate:${name}`,
2303
+ // Rust paths use underscores where crate names often use dashes.
2304
+ importPath: name.replace(/-/g, "_"),
2305
+ sourceRoots: [path3.posix.join(dir, "src")]
2306
+ };
2307
+ }
2308
+ },
2309
+ {
2310
+ kind: "go",
2311
+ file: "go.mod",
2312
+ build: (dir, source) => {
2313
+ const modulePath = parseGoModulePath(source);
2314
+ if (!modulePath) return void 0;
2315
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
2316
+ }
2317
+ },
2318
+ {
2319
+ kind: "python",
2320
+ file: "pyproject.toml",
2321
+ build: (dir, source) => {
2322
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
2323
+ return {
2324
+ name: `py:${name}`,
2325
+ importPath: void 0,
2326
+ // `src/` layout is the packaging-guide default; the root itself covers
2327
+ // the flat layout. Both are probed, missing ones simply never match.
2328
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2329
+ };
2330
+ }
2331
+ },
2332
+ {
2333
+ kind: "python",
2334
+ file: "setup.py",
2335
+ build: (dir) => ({
2336
+ name: `py:${path3.posix.basename(dir)}`,
2337
+ importPath: void 0,
2338
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2339
+ })
2340
+ },
2341
+ {
2342
+ kind: "maven",
2343
+ file: "pom.xml",
2344
+ build: (dir, source) => {
2345
+ const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
2346
+ return {
2347
+ name: `mvn:${artifactId}`,
2348
+ importPath: void 0,
2349
+ sourceRoots: [
2350
+ path3.posix.join(dir, "src/main/java"),
2351
+ path3.posix.join(dir, "src/main/kotlin"),
2352
+ path3.posix.join(dir, "src/main/scala"),
2353
+ path3.posix.join(dir, "src/test/java")
2354
+ ]
2355
+ };
2356
+ }
2357
+ },
2358
+ {
2359
+ kind: "gradle",
2360
+ file: "build.gradle",
2361
+ build: (dir) => buildGradleRoot(dir)
2362
+ },
2363
+ {
2364
+ kind: "gradle",
2365
+ file: "build.gradle.kts",
2366
+ build: (dir) => buildGradleRoot(dir)
2367
+ }
2368
+ ];
2369
+ function buildGradleRoot(dir) {
2370
+ return {
2371
+ name: `gradle:${path3.posix.basename(dir)}`,
2372
+ importPath: void 0,
2373
+ sourceRoots: [
2374
+ path3.posix.join(dir, "src/main/java"),
2375
+ path3.posix.join(dir, "src/main/kotlin"),
2376
+ path3.posix.join(dir, "src/main/scala")
2377
+ ]
2378
+ };
2379
+ }
2380
+ async function probeDotnetRoot(dir) {
2381
+ let entries;
2382
+ try {
2383
+ entries = await fs2.readdir(dir);
2384
+ } catch {
2385
+ return void 0;
2386
+ }
2387
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
2388
+ if (!project) return void 0;
2389
+ const name = project.slice(0, -".csproj".length);
2390
+ return {
2391
+ dir,
2392
+ kind: "dotnet",
2393
+ name: `csproj:${name}`,
2394
+ importPath: void 0,
2395
+ sourceRoots: [dir]
2396
+ };
2397
+ }
2398
+ async function detectModuleRoots(projectRoot2, files) {
2399
+ const root = toPortablePath(projectRoot2).replace(/\/+$/, "");
2400
+ const langsByDir = /* @__PURE__ */ new Map();
2401
+ for (const file of files) {
2402
+ const portable = toPortablePath(file);
2403
+ const lang = detectLang(portable);
2404
+ if (!lang) continue;
2405
+ const dir = path3.posix.dirname(portable);
2406
+ let langs = langsByDir.get(dir);
2407
+ if (!langs) {
2408
+ langs = /* @__PURE__ */ new Set();
2409
+ langsByDir.set(dir, langs);
2410
+ }
2411
+ langs.add(lang);
2412
+ }
2413
+ const candidates = /* @__PURE__ */ new Map();
2414
+ for (const [dir, langs] of langsByDir) {
2415
+ for (const ancestor of ancestorsOf(dir, root)) {
2416
+ let merged = candidates.get(ancestor);
2417
+ if (!merged) {
2418
+ merged = /* @__PURE__ */ new Set();
2419
+ candidates.set(ancestor, merged);
2420
+ }
2421
+ for (const lang of langs) merged.add(lang);
2422
+ }
2423
+ }
2424
+ const roots = [];
2425
+ await Promise.all(
2426
+ [...candidates].map(async ([dir, langs]) => {
2427
+ for (const probe of MARKER_PROBES) {
2428
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
2429
+ const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
2430
+ if (source === void 0) continue;
2431
+ const built = probe.build(dir, source);
2432
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
2433
+ }
2434
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
2435
+ const dotnet = await probeDotnetRoot(dir);
2436
+ if (dotnet) roots.push(dotnet);
2437
+ }
2438
+ })
2439
+ );
2440
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
2441
+ return { projectRoot: root, roots };
2442
+ }
2443
+ function findOwningRoot(structure, file, kinds) {
2444
+ const portable = toPortablePath(file);
2445
+ for (const root of structure.roots) {
2446
+ if (kinds && !kinds.includes(root.kind)) continue;
2447
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
2448
+ }
2449
+ return void 0;
2450
+ }
2451
+ function derivePackageFromLayout(filePath) {
2452
+ const portable = toPortablePath(filePath);
2453
+ const packagesIdx = portable.indexOf("/packages/");
2454
+ if (packagesIdx !== -1) {
2455
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
2456
+ if (segment) return `@wrongstack/${segment}`;
2457
+ }
2458
+ const appsIdx = portable.indexOf("/apps/");
2459
+ if (appsIdx !== -1) {
2460
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
2461
+ if (segment) return `app:${segment}`;
2462
+ }
2463
+ return void 0;
2464
+ }
2465
+ function pythonPackageLabel(structure, file, initDirs) {
2466
+ const portable = toPortablePath(file);
2467
+ const dir = path3.posix.dirname(portable);
2468
+ if (!initDirs.has(dir)) return void 0;
2469
+ const segments = [];
2470
+ let current = dir;
2471
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
2472
+ segments.unshift(path3.posix.basename(current));
2473
+ current = path3.posix.dirname(current);
2474
+ }
2475
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
2476
+ }
2477
+ function assignPackageLabels(structure, files) {
2478
+ const initDirs = /* @__PURE__ */ new Set();
2479
+ for (const file of files) {
2480
+ const portable = toPortablePath(file);
2481
+ if (path3.posix.basename(portable) === "__init__.py") {
2482
+ initDirs.add(path3.posix.dirname(portable));
2483
+ }
2484
+ }
2485
+ const labels = /* @__PURE__ */ new Map();
2486
+ for (const file of files) {
2487
+ const portable = toPortablePath(file);
2488
+ const lang = detectLang(portable);
2489
+ if (lang === "go") {
2490
+ const owner2 = findOwningRoot(structure, portable, ["go"]);
2491
+ const dir = path3.posix.dirname(portable);
2492
+ if (owner2?.importPath) {
2493
+ const relative3 = path3.posix.relative(owner2.dir, dir);
2494
+ labels.set(file, relative3 ? `${owner2.importPath}/${relative3}` : owner2.importPath);
2495
+ } else {
2496
+ labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
2497
+ }
2498
+ continue;
2499
+ }
2500
+ if (lang === "py") {
2501
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
2502
+ if (dotted) {
2503
+ labels.set(file, dotted);
2504
+ continue;
2505
+ }
2506
+ }
2507
+ const owner = findOwningRoot(structure, portable);
2508
+ const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
2509
+ labels.set(file, label);
2510
+ }
2511
+ return labels;
2512
+ }
2513
+
2514
+ // src/codebase-index/module-resolver.ts
2515
+ var EXTENSIONS = {
2516
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
2517
+ py: [".py", ".pyi"],
2518
+ rs: [".rs"],
2519
+ jvm: [".java", ".kt", ".scala"],
2520
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
2521
+ ruby: [".rb"],
2522
+ go: [".go"]
2523
+ };
2524
+ var DIRECTORY_ENTRIES = {
2525
+ js: ["index"],
2526
+ py: ["__init__"],
2527
+ rs: ["mod"],
2528
+ ruby: ["index"]
2529
+ };
2530
+ function normalizeNamespace(value) {
2531
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
2532
+ }
2533
+ var ModuleResolver = class {
2534
+ structure;
2535
+ /** Lowercased portable path → the path as indexed (case is preserved). */
2536
+ byPath;
2537
+ /** Lowercased portable directory → files directly inside it, as indexed. */
2538
+ byDir;
2539
+ /** Normalized namespace → the file declaring it (first by path, stable). */
2540
+ byNamespace;
2541
+ constructor(structure, files, namespaces = []) {
2542
+ this.structure = structure;
2543
+ this.byPath = /* @__PURE__ */ new Map();
2544
+ this.byDir = /* @__PURE__ */ new Map();
2545
+ this.byNamespace = /* @__PURE__ */ new Map();
2546
+ const dirsByKey = /* @__PURE__ */ new Map();
2547
+ for (const file of files) {
2548
+ const portable = toPortablePath(file);
2549
+ const pathKey = portable.toLowerCase();
2550
+ const priorPath = this.byPath.get(pathKey);
2551
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
2552
+ else this.byPath.set(pathKey, file);
2553
+ const dir = path4.posix.dirname(portable);
2554
+ const dirKey = dir.toLowerCase();
2555
+ const knownDir = dirsByKey.get(dirKey);
2556
+ if (knownDir === void 0) {
2557
+ dirsByKey.set(dirKey, dir);
2558
+ this.byDir.set(dirKey, [file]);
2559
+ } else if (knownDir === dir) {
2560
+ this.byDir.get(dirKey)?.push(file);
2561
+ } else {
2562
+ dirsByKey.delete(dirKey);
2563
+ this.byDir.delete(dirKey);
2564
+ }
2565
+ }
2566
+ for (const { name, file } of namespaces) {
2567
+ const lang = detectLang(file);
2568
+ if (!lang) continue;
2569
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
2570
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
2571
+ this.byNamespace.set(key, file);
2572
+ }
2573
+ }
2574
+ }
2575
+ /**
2576
+ * Resolve `specifier` as written in `fromFile`.
2577
+ * Returns the indexed target path, or `undefined` when it is external or
2578
+ * cannot be located.
2579
+ */
2580
+ resolve(fromFile, lang, specifier) {
2581
+ const spec = specifier.trim().replace(/\\/g, "/");
2582
+ if (!spec) return void 0;
2583
+ const from = toPortablePath(fromFile);
2584
+ switch (languageFamily(lang)) {
2585
+ case "js":
2586
+ return this.resolveJs(from, spec);
2587
+ case "go":
2588
+ return this.resolveGo(spec);
2589
+ case "py":
2590
+ return this.resolvePython(from, spec);
2591
+ case "rs":
2592
+ return this.resolveRust(from, spec);
2593
+ case "jvm":
2594
+ return this.resolveJvm(spec);
2595
+ case "c":
2596
+ return this.resolveInclude(from, spec);
2597
+ case "ruby":
2598
+ return this.resolveRuby(from, spec);
2599
+ case "dotnet":
2600
+ case "php":
2601
+ case "elixir":
2602
+ case "haskell":
2603
+ return this.resolveNamespace(lang, spec);
2604
+ default:
2605
+ return void 0;
2606
+ }
2607
+ }
2608
+ /**
2609
+ * Resolve a namespace specifier to the file declaring it.
2610
+ *
2611
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
2612
+ * names a namespace outright, while PHP's `use App\Models\User` names a
2613
+ * *class* inside `App\Models`, so the prefix is what was declared.
2614
+ */
2615
+ resolveNamespace(lang, spec) {
2616
+ const family = languageFamily(lang);
2617
+ const normalized = normalizeNamespace(spec);
2618
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
2619
+ if (exact) return exact;
2620
+ const segments = normalized.split(".").filter(Boolean);
2621
+ if (segments.length < 2) return void 0;
2622
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
2623
+ }
2624
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
2625
+ lookup(candidate) {
2626
+ return this.byPath.get(path4.posix.normalize(candidate).toLowerCase());
2627
+ }
2628
+ /**
2629
+ * Try `base` verbatim, then `base` + each extension, then each directory
2630
+ * entry point inside `base`.
2631
+ */
2632
+ lookupWithExtensions(base, family) {
2633
+ const direct = this.lookup(base);
2634
+ if (direct) return direct;
2635
+ const extensions = EXTENSIONS[family] ?? [];
2636
+ const suffix = path4.posix.extname(base);
2637
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
2638
+ for (const ext of extensions) {
2639
+ const hit = this.lookup(`${stem}${ext}`);
2640
+ if (hit) return hit;
2641
+ }
2642
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
2643
+ for (const ext of extensions) {
2644
+ const hit = this.lookup(path4.posix.join(base, `${entry}${ext}`));
2645
+ if (hit) return hit;
2646
+ }
2647
+ }
2648
+ return void 0;
2649
+ }
2650
+ /**
2651
+ * A representative indexed file inside `dir`, for ecosystems whose import
2652
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
2653
+ *
2654
+ * The choice is deterministic — a file named after the directory, else the
2655
+ * first by name — so the same import always produces the same edge. Package
2656
+ * grouping is unaffected either way: every file in the directory carries the
2657
+ * same package label, so the package-level edge is exact regardless of which
2658
+ * member represents it.
2659
+ */
2660
+ representativeIn(dir, family) {
2661
+ const members = this.byDir.get(path4.posix.normalize(dir).toLowerCase());
2662
+ if (!members?.length) return void 0;
2663
+ const extensions = EXTENSIONS[family] ?? [];
2664
+ const eligible = members.filter((file) => extensions.includes(path4.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
2665
+ if (eligible.length === 0) return void 0;
2666
+ const base = path4.posix.basename(path4.posix.normalize(dir)).toLowerCase();
2667
+ const named = eligible.find(
2668
+ (file) => path4.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
2669
+ );
2670
+ return named ?? eligible[0];
2671
+ }
2672
+ // ─── Per-family resolution ──────────────────────────────────────────────────
2673
+ /** Relative specifiers, then workspace package names and their subpaths. */
2674
+ resolveJs(fromFile, spec) {
2675
+ if (spec.startsWith(".")) {
2676
+ const absolute = path4.posix.join(path4.posix.dirname(fromFile), spec);
2677
+ return this.lookupWithExtensions(absolute, "js");
2678
+ }
2679
+ const owner = this.structure.roots.find(
2680
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2681
+ );
2682
+ if (!owner?.importPath) return void 0;
2683
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2684
+ if (!subpath) {
2685
+ return this.lookupWithExtensions(path4.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "index"), "js");
2686
+ }
2687
+ return this.lookupWithExtensions(path4.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "src", subpath), "js");
2688
+ }
2689
+ /** Go import paths are absolute module paths; a package is a directory. */
2690
+ resolveGo(spec) {
2691
+ const owner = this.structure.roots.find(
2692
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2693
+ );
2694
+ if (!owner?.importPath) return void 0;
2695
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2696
+ return this.representativeIn(path4.posix.join(owner.dir, subpath), "go");
2697
+ }
2698
+ /**
2699
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
2700
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
2701
+ */
2702
+ resolvePython(fromFile, spec) {
2703
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
2704
+ if (leadingDots > 0) {
2705
+ let base = path4.posix.dirname(fromFile);
2706
+ for (let i = 1; i < leadingDots; i++) base = path4.posix.dirname(base);
2707
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
2708
+ return this.lookupWithExtensions(path4.posix.join(base, ...rest), "py");
2709
+ }
2710
+ const segments = spec.split(".").filter(Boolean);
2711
+ if (segments.length === 0) return void 0;
2712
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
2713
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2714
+ const hit = this.lookupWithExtensions(path4.posix.join(base, ...segments), "py");
2715
+ if (hit) return hit;
2716
+ if (segments.length > 1) {
2717
+ const parent = this.lookupWithExtensions(
2718
+ path4.posix.join(base, ...segments.slice(0, -1)),
2719
+ "py"
2720
+ );
2721
+ if (parent) return parent;
2722
+ }
2723
+ }
2724
+ return void 0;
2725
+ }
2726
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
2727
+ resolveRust(fromFile, spec) {
2728
+ const segments = spec.split("::").filter(Boolean);
2729
+ if (segments.length === 0) return void 0;
2730
+ const head = segments[0];
2731
+ if (head === "self" || head === "super") {
2732
+ let base = path4.posix.dirname(fromFile);
2733
+ for (const segment of segments) {
2734
+ if (segment === "super") base = path4.posix.dirname(base);
2735
+ else if (segment !== "self") break;
2736
+ }
2737
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
2738
+ return this.lookupWithExtensions(path4.posix.join(base, ...rest2), "rs");
2739
+ }
2740
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
2741
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
2742
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
2743
+ );
2744
+ if (!crate) {
2745
+ return this.lookupWithExtensions(
2746
+ path4.posix.join(path4.posix.dirname(fromFile), ...segments),
2747
+ "rs"
2748
+ );
2749
+ }
2750
+ const rest = segments.slice(1);
2751
+ for (const base of crate.sourceRoots) {
2752
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path4.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
2753
+ const exact = this.lookupWithExtensions(path4.posix.join(base, ...rest), "rs");
2754
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path4.posix.join(base, "lib"), "rs");
2755
+ if (hit) return hit;
2756
+ }
2757
+ return void 0;
2758
+ }
2759
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
2760
+ resolveJvm(spec) {
2761
+ const segments = spec.split(".").filter(Boolean);
2762
+ if (segments.length === 0) return void 0;
2763
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
2764
+ const wildcard = segments[segments.length - 1] === "*";
2765
+ const parts = wildcard ? segments.slice(0, -1) : segments;
2766
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2767
+ const target = path4.posix.join(base, ...parts);
2768
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
2769
+ if (hit) return hit;
2770
+ }
2771
+ return void 0;
2772
+ }
2773
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
2774
+ resolveInclude(fromFile, spec) {
2775
+ const relative3 = this.lookupWithExtensions(
2776
+ path4.posix.join(path4.posix.dirname(fromFile), spec),
2777
+ "c"
2778
+ );
2779
+ if (relative3) return relative3;
2780
+ for (const base of [
2781
+ path4.posix.join(this.structure.projectRoot, "include"),
2782
+ this.structure.projectRoot
2783
+ ]) {
2784
+ const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "c");
2785
+ if (hit) return hit;
2786
+ }
2787
+ return void 0;
2788
+ }
2789
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
2790
+ resolveRuby(fromFile, spec) {
2791
+ const relative3 = this.lookupWithExtensions(
2792
+ path4.posix.join(path4.posix.dirname(fromFile), spec),
2793
+ "ruby"
2794
+ );
2795
+ if (relative3) return relative3;
2796
+ for (const base of [
2797
+ path4.posix.join(this.structure.projectRoot, "lib"),
2798
+ this.structure.projectRoot
2799
+ ]) {
2800
+ const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "ruby");
2801
+ if (hit) return hit;
2802
+ }
2803
+ return void 0;
2804
+ }
2805
+ };
2806
+
2807
+ // src/codebase-index/import-extractor.ts
2808
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
2809
+ var IMPORT_MAX_PER_FILE = 400;
2810
+ var DOTTED_IMPORT = [
2811
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
2812
+ ];
2813
+ var LANG_IMPORTS = {
2814
+ // Go and Python have real AST extractors; these patterns are the fallback for
2815
+ // machines with no Go toolchain or Python interpreter installed, where the
2816
+ // parser degrades to regex symbols and would otherwise contribute no edges.
2817
+ go: [
2818
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
2819
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
2820
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
2821
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
2822
+ ],
2823
+ py: [
2824
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
2825
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
2826
+ ],
2827
+ rs: [
2828
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
2829
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
2830
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
2831
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
2832
+ ],
2833
+ java: DOTTED_IMPORT,
2834
+ kotlin: DOTTED_IMPORT,
2835
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
2836
+ csharp: [
2837
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
2838
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
2839
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
2840
+ ],
2841
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
2842
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2843
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2844
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
2845
+ php: [
2846
+ // `use A\B\C` imports the class C, which is what the index has a symbol
2847
+ // for — the namespace symbol only covers the `A\B` prefix.
2848
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
2849
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
2850
+ ],
2851
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
2852
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
2853
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
2854
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
2855
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
2856
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
2857
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
2858
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
2859
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
2860
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
2861
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
2862
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
2863
+ html: [
2864
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
2865
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
2866
+ ],
2867
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
2868
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
2869
+ };
2870
+ function lastSegment(specifier) {
2871
+ const pathLike = /[/\\]|::/.test(specifier);
2872
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
2873
+ let last = segments[segments.length - 1] ?? specifier;
2874
+ if (last === "*" || last === "_") {
2875
+ last = segments[segments.length - 2] ?? specifier;
2876
+ }
2877
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
2878
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
2879
+ return dotted[dotted.length - 1] ?? last;
2880
+ }
2881
+ function newlineOffsets(content) {
2882
+ const offsets = [];
2883
+ for (let i = 0; i < content.length; i++) {
2884
+ if (content.charCodeAt(i) === 10) offsets.push(i);
2885
+ }
2886
+ return offsets;
2887
+ }
2888
+ function lineAt(offsets, index) {
2889
+ let low = 0;
2890
+ let high = offsets.length;
2891
+ while (low < high) {
2892
+ const mid = low + high >>> 1;
2893
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
2894
+ else high = mid;
2895
+ }
2896
+ return low + 1;
2897
+ }
2898
+ function hasImportPatterns(lang) {
2899
+ return LANG_IMPORTS[lang] !== void 0;
2900
+ }
2901
+ function extractImports(opts) {
2902
+ const patterns = LANG_IMPORTS[opts.lang];
2903
+ if (!patterns || !opts.content) return [];
2904
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
2905
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
2906
+ const refs = [];
2907
+ const seen = /* @__PURE__ */ new Set();
2908
+ const offsets = newlineOffsets(content);
2909
+ for (const pattern of patterns) {
2910
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
2911
+ for (const match of content.matchAll(re)) {
2912
+ if (refs.length >= limit) return refs;
2913
+ const specifier = match[1]?.trim();
2914
+ if (!specifier) continue;
2915
+ const module = specifier;
2916
+ const toName = pattern.name === "full" ? module : lastSegment(module);
2917
+ if (!toName) continue;
2918
+ const key = `${module}\0${toName}`;
2919
+ if (seen.has(key)) continue;
2920
+ seen.add(key);
2921
+ refs.push({
2922
+ fromId: 0,
2923
+ toName,
2924
+ callType: "import",
2925
+ line: lineAt(offsets, match.index ?? 0),
2926
+ lang: opts.lang,
2927
+ module
2928
+ });
2929
+ }
2930
+ }
2931
+ return refs;
2932
+ }
2933
+
2056
2934
  // src/codebase-index/parser-dispatch.ts
2057
2935
  async function parseFileContent(file, content, lang) {
2936
+ const parsed2 = await dispatch(file, content, lang);
2937
+ return withRelations(parsed2, content, lang);
2938
+ }
2939
+ async function dispatch(file, content, lang) {
2058
2940
  switch (lang) {
2059
2941
  case "ts":
2060
2942
  case "tsx":
@@ -2089,6 +2971,13 @@ async function parseFileContent(file, content, lang) {
2089
2971
  }
2090
2972
  }
2091
2973
  }
2974
+ function withRelations(parsed2, content, lang) {
2975
+ let refs = parsed2.refs ?? [];
2976
+ if (refs.length === 0 && hasImportPatterns(lang)) {
2977
+ refs = extractImports({ content, lang });
2978
+ }
2979
+ return { ...parsed2, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
2980
+ }
2092
2981
 
2093
2982
  // src/codebase-index/writer.ts
2094
2983
  import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
@@ -2184,6 +3073,9 @@ var Bm25Index = class {
2184
3073
  }
2185
3074
  };
2186
3075
 
3076
+ // src/codebase-index/writer.ts
3077
+ init_languages();
3078
+
2187
3079
  // src/codebase-index/lsp-kind.ts
2188
3080
  function lspKindToInternalKind(k) {
2189
3081
  switch (k) {
@@ -2218,7 +3110,7 @@ function lspKindToInternalKind(k) {
2218
3110
  }
2219
3111
 
2220
3112
  // src/codebase-index/schema.ts
2221
- var SCHEMA_VERSION = 3;
3113
+ var SCHEMA_VERSION = 4;
2222
3114
 
2223
3115
  // src/codebase-index/sqlite-runtime.ts
2224
3116
  import { createRequire } from "node:module";
@@ -2365,7 +3257,7 @@ function runSqliteWithRetry(fn) {
2365
3257
 
2366
3258
  // src/codebase-index/writer-admin.ts
2367
3259
  import * as fs6 from "node:fs";
2368
- import * as path8 from "node:path";
3260
+ import * as path9 from "node:path";
2369
3261
  var DB_FILE = "index.db";
2370
3262
  function getAllIndexableWithStatement(stmt) {
2371
3263
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -2424,7 +3316,7 @@ function getAllFileMetasWithStatement(stmt) {
2424
3316
  }
2425
3317
  function getIndexDbSizeBytes(indexDir2) {
2426
3318
  try {
2427
- return fs6.statSync(path8.join(indexDir2, DB_FILE)).size;
3319
+ return fs6.statSync(path9.join(indexDir2, DB_FILE)).size;
2428
3320
  } catch {
2429
3321
  return 0;
2430
3322
  }
@@ -2475,49 +3367,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
2475
3367
  }
2476
3368
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2477
3369
  if (refs.length === 0) return;
2478
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
3370
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
2479
3371
  for (let i = 0; i < refs.length; i += chunkSize) {
2480
3372
  const chunk = refs.slice(i, i + chunkSize);
2481
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
3373
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
2482
3374
  const insert = stmt(
2483
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
3375
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
3376
+ VALUES ${placeholders}`
2484
3377
  );
2485
3378
  const binds = [];
2486
3379
  for (const ref of chunk) {
2487
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
3380
+ binds.push(
3381
+ ref.fromId,
3382
+ ref.toName,
3383
+ ref.toId ?? null,
3384
+ ref.callType,
3385
+ ref.line,
3386
+ ref.lang ?? "",
3387
+ ref.module ?? null,
3388
+ ref.toFile ?? null
3389
+ );
2488
3390
  }
2489
3391
  insert.run(...binds);
2490
3392
  }
2491
3393
  }
2492
3394
 
3395
+ // src/codebase-index/writer-graph-reader.ts
3396
+ init_languages();
3397
+
2493
3398
  // src/codebase-index/writer-graph-helpers.ts
2494
- import * as path9 from "node:path";
2495
- function derivePackage(filePath) {
2496
- const f = filePath.replace(/\\/g, "/");
2497
- const pkgsIdx = f.indexOf("/packages/");
2498
- if (pkgsIdx !== -1) {
2499
- const rest = f.slice(pkgsIdx + "/packages/".length);
2500
- const seg = rest.split("/")[0];
2501
- return seg ? `@wrongstack/${seg}` : void 0;
2502
- }
2503
- const appsIdx = f.indexOf("/apps/");
2504
- if (appsIdx !== -1) {
2505
- const rest = f.slice(appsIdx + "/apps/".length);
2506
- const seg = rest.split("/")[0];
2507
- return seg ? `app:${seg}` : void 0;
2508
- }
2509
- return void 0;
2510
- }
2511
- function packageFromImport(moduleName) {
2512
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
2513
- const parts = moduleName.split("/");
2514
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
3399
+ function createPackageLabeller(stored) {
3400
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
2515
3401
  }
2516
- function buildPackageGraphNodes(fileCounts, files) {
3402
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
2517
3403
  const pkgNodes = /* @__PURE__ */ new Map();
2518
3404
  const fileToPkg = /* @__PURE__ */ new Map();
2519
3405
  for (const { file, n } of fileCounts) {
2520
- const pkg = derivePackage(file) ?? "(root)";
3406
+ const pkg = packageOf(file);
2521
3407
  fileToPkg.set(file, pkg);
2522
3408
  const node = pkgNodes.get(pkg);
2523
3409
  if (node) {
@@ -2534,7 +3420,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2534
3420
  }
2535
3421
  }
2536
3422
  for (const { file } of files) {
2537
- const pkg = derivePackage(file) ?? "(root)";
3423
+ const pkg = packageOf(file);
2538
3424
  fileToPkg.set(file, pkg);
2539
3425
  const node = pkgNodes.get(pkg);
2540
3426
  if (node) {
@@ -2552,7 +3438,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2552
3438
  }
2553
3439
  return { pkgNodes, fileToPkg };
2554
3440
  }
2555
- function buildFileGraphNodeState(pkgSyms, localFiles) {
3441
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
2556
3442
  const fileNodes = /* @__PURE__ */ new Map();
2557
3443
  const symToFile = /* @__PURE__ */ new Map();
2558
3444
  const fileStats = /* @__PURE__ */ new Map();
@@ -2571,7 +3457,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2571
3457
  id: `file:${file}`,
2572
3458
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
2573
3459
  kind: "file",
2574
- package: derivePackage(file) ?? "(root)",
3460
+ package: packageOf(file),
2575
3461
  file,
2576
3462
  symbolCount: stats?.count ?? 0,
2577
3463
  lang: stats?.lang,
@@ -2583,7 +3469,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2583
3469
  }
2584
3470
  return { fileNodes, symToFile, fileStats, ensureFileNode };
2585
3471
  }
2586
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
3472
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
2587
3473
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
2588
3474
  const aExternal = a.file === fileFilter ? 0 : 1;
2589
3475
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -2595,7 +3481,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2595
3481
  symbolId: s.id,
2596
3482
  symbolKind: s.kind,
2597
3483
  file: s.file,
2598
- package: derivePackage(s.file) ?? "(root)",
3484
+ package: packageOf(s.file),
2599
3485
  lang: s.lang,
2600
3486
  line: s.line,
2601
3487
  signature: s.signature,
@@ -2603,29 +3489,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2603
3489
  external: s.file !== fileFilter
2604
3490
  }));
2605
3491
  }
2606
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
2607
- if (!moduleName.startsWith(".")) return void 0;
2608
- const normalizedFrom = fromFile.replace(/\\/g, "/");
2609
- const absolute = path9.posix.normalize(
2610
- path9.posix.join(path9.posix.dirname(normalizedFrom), moduleName)
2611
- );
2612
- const extension = path9.posix.extname(absolute);
2613
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
2614
- const candidates = [
2615
- absolute,
2616
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
2617
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(absolute, `index${ext}`)),
2618
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(base, `index${ext}`))
2619
- ];
2620
- const indexedByPortablePath = new Map(
2621
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
2622
- );
2623
- for (const candidate of candidates) {
2624
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
2625
- if (indexed) return indexed;
2626
- }
2627
- return void 0;
2628
- }
2629
3492
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
2630
3493
  const key = `${source}\0${target}`;
2631
3494
  let edge = edgeMap.get(key);
@@ -2666,7 +3529,12 @@ function mapWriterRefRow(row) {
2666
3529
  toName: row.to_name,
2667
3530
  toId: row.to_id ?? void 0,
2668
3531
  callType: row.call_type,
2669
- line: row.line
3532
+ line: row.line,
3533
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
3534
+ // queries select; `undefined` keeps those rows valid Refs.
3535
+ lang: row.lang || void 0,
3536
+ module: row.module ?? void 0,
3537
+ toFile: row.to_file ?? void 0
2670
3538
  };
2671
3539
  }
2672
3540
 
@@ -2814,7 +3682,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
2814
3682
  function getPackageGraphWithStatement(stmt) {
2815
3683
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
2816
3684
  const files = stmt("SELECT DISTINCT file FROM files").all();
2817
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
3685
+ const packageOf = readPackageLabeller(stmt);
3686
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
2818
3687
  const refRows = stmt(
2819
3688
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
2820
3689
  FROM refs r
@@ -2825,32 +3694,42 @@ function getPackageGraphWithStatement(stmt) {
2825
3694
  ).all();
2826
3695
  const edgeMap = /* @__PURE__ */ new Map();
2827
3696
  for (const r of refRows) {
2828
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2829
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
3697
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3698
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
2830
3699
  if (fromPkg === toPkg) continue;
2831
3700
  const n = Number(r.n) || 0;
2832
3701
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
2833
3702
  }
2834
3703
  const importRows = stmt(
2835
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
3704
+ `SELECT s.file AS from_file,
3705
+ COALESCE(r.to_file, st.file) AS to_file,
3706
+ COUNT(*) AS n
2836
3707
  FROM refs r
2837
3708
  JOIN symbols s ON s.id = r.from_id
3709
+ LEFT JOIN symbols st ON st.id = r.to_id
2838
3710
  WHERE r.call_type = 'import'
2839
- GROUP BY r.to_name, s.file`
3711
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
3712
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
2840
3713
  ).all();
2841
3714
  for (const r of importRows) {
2842
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2843
- const toPkg = packageFromImport(r.to_name);
2844
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
3715
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3716
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
3717
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
2845
3718
  const n = Number(r.n) || 0;
2846
3719
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
2847
3720
  }
2848
3721
  const edges = materializeWeightedEdges(edgeMap, "pkg");
2849
3722
  return { nodes: [...pkgNodes.values()], edges };
2850
3723
  }
3724
+ function readPackageLabeller(stmt) {
3725
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
3726
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
3727
+ }
2851
3728
  function getFileGraphWithStatement(stmt, packageFilter) {
2852
3729
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
2853
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
3730
+ const packageOf = readPackageLabeller(stmt);
3731
+ const langOf = (file) => detectLang(file) ?? "other";
3732
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
2854
3733
  const localFiles = new Set(pkgFilePaths);
2855
3734
  if (localFiles.size === 0) return { nodes: [], edges: [] };
2856
3735
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -2859,9 +3738,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2859
3738
  ).all(...pkgFilePaths);
2860
3739
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
2861
3740
  pkgSyms,
2862
- localFiles
3741
+ localFiles,
3742
+ packageOf
2863
3743
  );
2864
- const indexedFiles = new Set(allFiles.map((f) => f.file));
2865
3744
  const refRows = stmt(
2866
3745
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
2867
3746
  FROM refs r
@@ -2884,7 +3763,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2884
3763
  for (const x of extras) {
2885
3764
  symToFile.set(x.id, x.file);
2886
3765
  if (!fileStats.has(x.file)) {
2887
- fileStats.set(x.file, { count: 0, lang: "ts" });
3766
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
2888
3767
  }
2889
3768
  }
2890
3769
  }
@@ -2901,17 +3780,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2901
3780
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
2902
3781
  }
2903
3782
  const importRows = stmt(
2904
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
3783
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
2905
3784
  FROM refs r
3785
+ LEFT JOIN symbols st ON st.id = r.to_id
2906
3786
  WHERE r.call_type = 'import'
3787
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
2907
3788
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
2908
- GROUP BY r.from_id, r.to_name`
3789
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
2909
3790
  ).all(...pkgFilePaths);
2910
3791
  for (const r of importRows) {
2911
3792
  const fromFile = symToFile.get(r.from_id);
2912
3793
  if (!fromFile || !localFiles.has(fromFile)) continue;
2913
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
3794
+ const toFile = r.to_file;
2914
3795
  if (!toFile || fromFile === toFile) continue;
3796
+ if (!fileStats.has(toFile)) {
3797
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
3798
+ }
2915
3799
  ensureFileNode(fromFile);
2916
3800
  ensureFileNode(toFile);
2917
3801
  const n = Number(r.n) || 0;
@@ -2961,7 +3845,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
2961
3845
  ).all(...missingIds);
2962
3846
  for (const s of extras) symById.set(s.id, s);
2963
3847
  }
2964
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
3848
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
2965
3849
  return { nodes, edges };
2966
3850
  }
2967
3851
 
@@ -2983,7 +3867,7 @@ function assignRefsToSymbols(refs, symbols) {
2983
3867
  }
2984
3868
  if (!owner && ref.callType === "import") owner = ordered[0];
2985
3869
  if (!owner || owner.id <= 0) continue;
2986
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3870
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
2987
3871
  if (seen.has(key)) continue;
2988
3872
  seen.add(key);
2989
3873
  assigned.push({ ...ref, fromId: owner.id });
@@ -3025,7 +3909,11 @@ var CORE_TABLES_SQL = `
3025
3909
  lang TEXT NOT NULL,
3026
3910
  mtime_ms INTEGER NOT NULL,
3027
3911
  symbol_count INTEGER NOT NULL DEFAULT 0,
3028
- last_indexed INTEGER NOT NULL
3912
+ last_indexed INTEGER NOT NULL,
3913
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
3914
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
3915
+ -- re-derived per query because the evidence lives on disk, not in the DB.
3916
+ package TEXT NOT NULL DEFAULT ''
3029
3917
  );
3030
3918
  CREATE TABLE IF NOT EXISTS symbols (
3031
3919
  id INTEGER PRIMARY KEY,
@@ -3042,6 +3930,9 @@ var CORE_TABLES_SQL = `
3042
3930
  file_fk TEXT NOT NULL
3043
3931
  );
3044
3932
  `;
3933
+ var FILE_INDEX_SQL = [
3934
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
3935
+ ];
3045
3936
  var SYMBOL_INDEX_SQL = [
3046
3937
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
3047
3938
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -3058,15 +3949,32 @@ var REFS_TABLE_SQL = `
3058
3949
  to_name TEXT NOT NULL,
3059
3950
  to_id INTEGER,
3060
3951
  call_type TEXT NOT NULL,
3061
- line INTEGER NOT NULL
3952
+ line INTEGER NOT NULL,
3953
+ lang TEXT NOT NULL DEFAULT '',
3954
+ module TEXT,
3955
+ to_file TEXT
3062
3956
  );
3063
3957
  `;
3064
3958
  var REFS_INDEX_SQL = [
3065
3959
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
3066
3960
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
3067
3961
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
3068
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
3962
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
3963
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
3964
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
3965
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
3966
+ // The post-index module resolution pass groups unresolved import refs by
3967
+ // (module, lang); graph readers then read to_file back.
3968
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
3969
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
3069
3970
  ];
3971
+ var LANG_FAMILY_TABLE_SQL = `
3972
+ CREATE TABLE IF NOT EXISTS lang_family (
3973
+ lang TEXT PRIMARY KEY,
3974
+ family TEXT NOT NULL
3975
+ );
3976
+ `;
3977
+ var LANG_FAMILY_WILDCARD = "*";
3070
3978
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
3071
3979
 
3072
3980
  // src/codebase-index/writer-search-helpers.ts
@@ -3287,6 +4195,60 @@ var IndexStore = class _IndexStore {
3287
4195
  runWithRetry(fn) {
3288
4196
  return runSqliteWithRetry(fn);
3289
4197
  }
4198
+ /**
4199
+ * Mirror the in-process language→family map into SQLite.
4200
+ *
4201
+ * Rewritten on every open rather than only on schema bumps: the mapping is
4202
+ * static lookup data, so a code-side change (a new language, a language
4203
+ * moving families) must take effect without forcing a full reindex.
4204
+ */
4205
+ seedLangFamilies() {
4206
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
4207
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
4208
+ insert.run("", LANG_FAMILY_WILDCARD);
4209
+ }
4210
+ /**
4211
+ * Add any column the current schema expects but the on-disk table lacks.
4212
+ *
4213
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
4214
+ * and the version check above only rebuilds on a version *mismatch*. That
4215
+ * leaves a real gap: several wstack processes share this database, and while
4216
+ * a version upgrade is rolling out one of them may still be running the
4217
+ * previous build. That older process sees the newer version number, drops the
4218
+ * tables, and recreates them from *its* DDL — without the newer columns —
4219
+ * while the metadata row still reads the new version. Every later query for
4220
+ * one of those columns then fails with `no such column`, and no amount of
4221
+ * reindexing fixes it, because the version numbers already agree.
4222
+ *
4223
+ * Repairing column-by-column makes the schema self-healing from any of those
4224
+ * states. Table and column names are compile-time literals from this module,
4225
+ * never user input.
4226
+ */
4227
+ repairMissingColumns() {
4228
+ const expected = [
4229
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
4230
+ {
4231
+ table: "refs",
4232
+ columns: [
4233
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
4234
+ ["module", "TEXT"],
4235
+ ["to_file", "TEXT"]
4236
+ ]
4237
+ }
4238
+ ];
4239
+ for (const { table, columns } of expected) {
4240
+ const present = new Set(
4241
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
4242
+ (row) => typeof row.name === "string" ? [row.name] : []
4243
+ )
4244
+ );
4245
+ if (present.size === 0) continue;
4246
+ for (const [name, type] of columns) {
4247
+ if (present.has(name)) continue;
4248
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
4249
+ }
4250
+ }
4251
+ }
3290
4252
  initSchema() {
3291
4253
  this.db.exec(METADATA_TABLE_SQL);
3292
4254
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -3309,9 +4271,13 @@ var IndexStore = class _IndexStore {
3309
4271
  );
3310
4272
  }
3311
4273
  this.db.exec(CORE_TABLES_SQL);
3312
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3313
4274
  this.db.exec(REFS_TABLE_SQL);
4275
+ this.repairMissingColumns();
4276
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
4277
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3314
4278
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
4279
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
4280
+ this.seedLangFamilies();
3315
4281
  try {
3316
4282
  this.db.exec(SYMBOLS_FTS_SQL);
3317
4283
  this.ftsAvailable = true;
@@ -3346,6 +4312,18 @@ var IndexStore = class _IndexStore {
3346
4312
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
3347
4313
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
3348
4314
  static MAX_SQL_VARS = 900;
4315
+ /**
4316
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
4317
+ * `sym` belong to the same language family — or the ref carries no language,
4318
+ * in which case the wildcard bind matches everything.
4319
+ *
4320
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
4321
+ */
4322
+ static FAMILY_MATCH_SQL = `(
4323
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
4324
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
4325
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
4326
+ )`;
3349
4327
  /**
3350
4328
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
3351
4329
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -3409,9 +4387,12 @@ var IndexStore = class _IndexStore {
3409
4387
  const placeholders = chunk.map(() => "?").join(",");
3410
4388
  const result = this.stmt(
3411
4389
  `UPDATE refs
3412
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
4390
+ SET to_id = (
4391
+ SELECT MIN(sym.id) FROM symbols sym
4392
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
4393
+ )
3413
4394
  WHERE to_name IN (${placeholders})`
3414
- ).run(...chunk);
4395
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
3415
4396
  changes += result.changes ?? 0;
3416
4397
  }
3417
4398
  return changes;
@@ -3538,6 +4519,115 @@ var IndexStore = class _IndexStore {
3538
4519
  getAllFileMetas() {
3539
4520
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
3540
4521
  }
4522
+ // ─── Project structure & module resolution ──────────────────────────────────
4523
+ /** Store the Code Atlas grouping label for each indexed file. */
4524
+ setFilePackages(entries) {
4525
+ if (entries.size === 0) return;
4526
+ this.runWithRetry(() => {
4527
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
4528
+ for (const [file, label] of entries) update.run(label, file);
4529
+ });
4530
+ }
4531
+ /**
4532
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
4533
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
4534
+ * Ordered so the resolver's choice among duplicate declarations is stable.
4535
+ */
4536
+ getNamespaceDeclarations() {
4537
+ return this.stmt(
4538
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
4539
+ ).all();
4540
+ }
4541
+ /** `file → package` for every indexed file that has a label. */
4542
+ getFilePackages() {
4543
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
4544
+ return new Map(rows.map((row) => [row.file, row.package]));
4545
+ }
4546
+ /**
4547
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
4548
+ *
4549
+ * Distinct rather than per-ref because resolution depends only on these three
4550
+ * values: a file importing the same module twenty times resolves it once.
4551
+ */
4552
+ getUnresolvedImports(onlyFiles) {
4553
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
4554
+ FROM refs r
4555
+ JOIN symbols s ON s.id = r.from_id
4556
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
4557
+ if (!onlyFiles?.length) {
4558
+ return this.stmt(base).all();
4559
+ }
4560
+ const out = [];
4561
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
4562
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
4563
+ const placeholders = chunk.map(() => "?").join(",");
4564
+ out.push(
4565
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
4566
+ );
4567
+ }
4568
+ return out;
4569
+ }
4570
+ /**
4571
+ * Write resolved import targets back onto `refs.to_file`.
4572
+ *
4573
+ * Applied through a temp table and a single UPDATE: one statement per
4574
+ * resolution would mean thousands of round-trips on a first index.
4575
+ */
4576
+ applyImportResolutions(resolutions) {
4577
+ if (resolutions.length === 0) return 0;
4578
+ return this.runWithRetry(() => {
4579
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4580
+ this.db.exec(
4581
+ `CREATE TEMP TABLE import_resolution (
4582
+ from_file TEXT NOT NULL,
4583
+ lang TEXT NOT NULL,
4584
+ module TEXT NOT NULL,
4585
+ to_file TEXT NOT NULL
4586
+ )`
4587
+ );
4588
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
4589
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
4590
+ const chunk = resolutions.slice(i, i + chunkSize);
4591
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
4592
+ const binds = [];
4593
+ for (const entry of chunk) {
4594
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
4595
+ }
4596
+ this.stmt(
4597
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
4598
+ VALUES ${placeholders}`
4599
+ ).run(...binds);
4600
+ }
4601
+ this.db.exec(
4602
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
4603
+ ON import_resolution(module, lang, from_file)`
4604
+ );
4605
+ const result = this.stmt(
4606
+ `UPDATE refs
4607
+ SET to_file = (
4608
+ SELECT ir.to_file
4609
+ FROM temp.import_resolution ir
4610
+ JOIN symbols s ON s.id = refs.from_id
4611
+ WHERE ir.module = refs.module
4612
+ AND ir.lang = refs.lang
4613
+ AND ir.from_file = s.file
4614
+ LIMIT 1
4615
+ )
4616
+ WHERE refs.call_type = 'import'
4617
+ AND refs.module IS NOT NULL
4618
+ AND EXISTS (
4619
+ SELECT 1
4620
+ FROM temp.import_resolution ir
4621
+ JOIN symbols s ON s.id = refs.from_id
4622
+ WHERE ir.module = refs.module
4623
+ AND ir.lang = refs.lang
4624
+ AND ir.from_file = s.file
4625
+ )`
4626
+ ).run();
4627
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4628
+ return result.changes ?? 0;
4629
+ });
4630
+ }
3541
4631
  // ─── Search ──────────────────────────────────────────────────────────────────
3542
4632
  search(query, filter, opts) {
3543
4633
  const built = this.buildSearchWhere(query, filter);
@@ -3924,9 +5014,12 @@ var IndexStore = class _IndexStore {
3924
5014
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
3925
5015
  * Call this after all symbols have been inserted to fill in cross-references.
3926
5016
  *
3927
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
3928
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
3929
- * that found a targetmatching the previous per-row loop's return value.
5017
+ * A match additionally requires the referencing ref and the target symbol to
5018
+ * be in the same {@link LangFamily}. Without that guard a name match is a
5019
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
5020
+ * `Config` are declared in most languages at once, and each collision draws a
5021
+ * Code Atlas edge between files that never reference each other. Refs stored
5022
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
3930
5023
  */
3931
5024
  resolveRefs() {
3932
5025
  return this.runWithRetry(() => {
@@ -3935,20 +5028,35 @@ var IndexStore = class _IndexStore {
3935
5028
  `UPDATE refs
3936
5029
  SET to_id = s.id
3937
5030
  FROM (
3938
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
3939
- ) AS s
5031
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5032
+ FROM symbols sym
5033
+ JOIN lang_family lf ON lf.lang = sym.lang
5034
+ GROUP BY sym.name, lf.family
5035
+ UNION ALL
5036
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5037
+ FROM symbols sym
5038
+ GROUP BY sym.name
5039
+ ) AS s,
5040
+ lang_family AS rf
3940
5041
  WHERE refs.to_id IS NULL
3941
5042
  AND refs.to_name IS NOT NULL
3942
- AND refs.to_name = s.name`
5043
+ AND rf.lang = refs.lang
5044
+ AND s.name = refs.to_name
5045
+ AND s.family = rf.family`
3943
5046
  ).run();
3944
5047
  return result.changes ?? 0;
3945
5048
  } catch {
3946
5049
  const result = this.stmt(
3947
5050
  `UPDATE refs SET to_id = (
3948
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
5051
+ SELECT sym.id FROM symbols sym
5052
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5053
+ ORDER BY sym.id LIMIT 1
3949
5054
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
3950
- AND to_name IN (SELECT name FROM symbols)`
3951
- ).run();
5055
+ AND EXISTS (
5056
+ SELECT 1 FROM symbols sym
5057
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5058
+ )`
5059
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
3952
5060
  return result.changes ?? 0;
3953
5061
  }
3954
5062
  });
@@ -4166,7 +5274,7 @@ function normalizeComparablePath(value) {
4166
5274
  }
4167
5275
  function gitOutput(projectRoot2, args) {
4168
5276
  return new Promise((resolve4, reject) => {
4169
- execFile2(
5277
+ execFile(
4170
5278
  "git",
4171
5279
  ["-C", projectRoot2, ...args],
4172
5280
  {
@@ -4297,13 +5405,40 @@ function assignRefsToSymbols2(refs, symbols) {
4297
5405
  }
4298
5406
  if (!owner && ref.callType === "import") owner = ordered[0];
4299
5407
  if (!owner || owner.id <= 0) continue;
4300
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
5408
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4301
5409
  if (seen.has(key)) continue;
4302
5410
  seen.add(key);
4303
5411
  assigned.push({ ...ref, fromId: owner.id });
4304
5412
  }
4305
5413
  return assigned;
4306
5414
  }
5415
+ async function resolveProjectRelations(store, projectRoot2, opts) {
5416
+ if (opts.signal?.aborted) return;
5417
+ try {
5418
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
5419
+ if (indexedFiles.length === 0) return;
5420
+ const structure = await detectModuleRoots(projectRoot2, indexedFiles);
5421
+ if (opts.signal?.aborted) return;
5422
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
5423
+ const resolver = new ModuleResolver(
5424
+ structure,
5425
+ indexedFiles,
5426
+ store.getNamespaceDeclarations()
5427
+ );
5428
+ const pending = store.getUnresolvedImports(opts.onlyFiles);
5429
+ const resolutions = [];
5430
+ for (const entry of pending) {
5431
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
5432
+ if (toFile && toFile !== entry.fromFile) {
5433
+ resolutions.push({ ...entry, toFile });
5434
+ }
5435
+ }
5436
+ if (opts.signal?.aborted) return;
5437
+ store.applyImportResolutions(resolutions);
5438
+ } catch (err) {
5439
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
5440
+ }
5441
+ }
4307
5442
  async function runIndexerWithStore(store, opts) {
4308
5443
  const { projectRoot: projectRoot2, langs, ignore = [], signal } = opts;
4309
5444
  const relationGraphVersion = "2";
@@ -4548,6 +5683,14 @@ async function runIndexerWithStore(store, opts) {
4548
5683
  }
4549
5684
  }
4550
5685
  if (needsFullRefResolution) store.resolveRefs();
5686
+ await resolveProjectRelations(store, projectRoot2, {
5687
+ // A watcher run re-resolves only what it touched; a full run (or a contract
5688
+ // bump) re-resolves everything, because a newly indexed file can be the
5689
+ // target of imports written long before it.
5690
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
5691
+ errors,
5692
+ signal
5693
+ });
4551
5694
  store.setMetadata("ref_resolution_version", refResolutionVersion);
4552
5695
  store.setMetadata("relation_graph_version", relationGraphVersion);
4553
5696
  if (!opts.files || filesIndexed >= 50) store.optimize();