@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.
@@ -14,23 +14,26 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // src/codebase-index/languages.ts
17
- import * as path6 from "node:path";
17
+ import * as path from "node:path";
18
18
  function detectLang(file) {
19
- const base = path6.basename(file);
19
+ const base = path.basename(file);
20
20
  const lowerBase = base.toLowerCase();
21
21
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
22
22
  return "ts";
23
23
  }
24
24
  const special = SPECIAL_FILENAMES[lowerBase];
25
25
  if (special) return special;
26
- const ext = path6.extname(base).toLowerCase();
26
+ const ext = path.extname(base).toLowerCase();
27
27
  if (!ext) return null;
28
28
  return EXT_TO_LANG[ext] ?? null;
29
29
  }
30
30
  function isIndexablePath(file) {
31
31
  return detectLang(file) !== null;
32
32
  }
33
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
33
+ function languageFamily(lang) {
34
+ return LANG_FAMILY[lang] ?? "other";
35
+ }
36
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
34
37
  var init_languages = __esm({
35
38
  "src/codebase-index/languages.ts"() {
36
39
  "use strict";
@@ -121,6 +124,52 @@ var init_languages = __esm({
121
124
  procfile: "other",
122
125
  justfile: "other"
123
126
  };
127
+ LANG_FAMILY = {
128
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
129
+ // imports from — and is imported by — plain .ts files.
130
+ ts: "js",
131
+ tsx: "js",
132
+ js: "js",
133
+ jsx: "js",
134
+ vue: "js",
135
+ svelte: "js",
136
+ go: "go",
137
+ py: "py",
138
+ rs: "rs",
139
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
140
+ java: "jvm",
141
+ kotlin: "jvm",
142
+ scala: "jvm",
143
+ csharp: "dotnet",
144
+ // A .h header is consumed by both C and C++ translation units.
145
+ c: "c",
146
+ cpp: "c",
147
+ ruby: "ruby",
148
+ php: "php",
149
+ swift: "swift",
150
+ dart: "dart",
151
+ elixir: "elixir",
152
+ haskell: "haskell",
153
+ zig: "zig",
154
+ lua: "lua",
155
+ r: "r",
156
+ shell: "shell",
157
+ sql: "sql",
158
+ json: "data",
159
+ yaml: "data",
160
+ toml: "data",
161
+ html: "web",
162
+ css: "web",
163
+ proto: "proto",
164
+ graphql: "graphql",
165
+ md: "other",
166
+ other: "other"
167
+ };
168
+ LANG_FAMILY_ENTRIES = Object.freeze(
169
+ Object.entries(LANG_FAMILY).map(
170
+ ([lang, family]) => Object.freeze([lang, family])
171
+ )
172
+ );
124
173
  }
125
174
  });
126
175
 
@@ -285,7 +334,7 @@ function getTypeName(name) {
285
334
  function deduplicateRefs(refs) {
286
335
  const seen = /* @__PURE__ */ new Set();
287
336
  return refs.filter((r) => {
288
- const key = `${r.toName}:${r.callType}:${r.line}`;
337
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
289
338
  if (seen.has(key)) return false;
290
339
  seen.add(key);
291
340
  return true;
@@ -295,10 +344,16 @@ function getImportSpecifierName(spec) {
295
344
  return spec.propertyName?.text ?? spec.name.text;
296
345
  }
297
346
  function emitImportSpecifierRefs(node, refs, lineNum) {
347
+ const module = moduleSpecifierOf(node.moduleSpecifier);
298
348
  const clause = node.importClause;
299
- if (!clause) return;
349
+ if (!clause) {
350
+ if (module) {
351
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
352
+ }
353
+ return;
354
+ }
300
355
  if (clause.name) {
301
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
356
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
302
357
  }
303
358
  const bindings = clause.namedBindings;
304
359
  if (!bindings) return;
@@ -308,26 +363,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
308
363
  fromId: 0,
309
364
  toName: getImportSpecifierName(element),
310
365
  callType: "import",
311
- line: lineNum
366
+ line: lineNum,
367
+ module
312
368
  });
313
369
  }
314
370
  } else if (ts.isNamespaceImport(bindings)) {
315
- refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
371
+ refs.push({
372
+ fromId: 0,
373
+ toName: bindings.name.text,
374
+ callType: "import",
375
+ line: lineNum,
376
+ module
377
+ });
316
378
  }
317
379
  }
380
+ function moduleSpecifierOf(node) {
381
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
382
+ }
318
383
  function emitExportSpecifierRefs(node, refs, lineNum) {
384
+ const module = moduleSpecifierOf(node.moduleSpecifier);
319
385
  const clause = node.exportClause;
320
386
  if (clause && ts.isNamespaceExport(clause)) {
321
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
387
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
322
388
  return;
323
389
  }
324
390
  if (clause && ts.isNamedExports(clause)) {
325
391
  for (const element of clause.elements) {
326
392
  const originalName = element.propertyName?.text ?? element.name.text;
327
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
393
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
328
394
  }
329
395
  return;
330
396
  }
397
+ if (module) {
398
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
399
+ }
331
400
  }
332
401
  var ts, tsLoad, kindMapCache;
333
402
  var init_ts_parser = __esm({
@@ -340,21 +409,21 @@ var init_ts_parser = __esm({
340
409
  });
341
410
 
342
411
  // src/_win32-resolve.ts
343
- import * as fs6 from "node:fs";
344
- import * as path7 from "node:path";
412
+ import * as fs7 from "node:fs";
413
+ import * as path8 from "node:path";
345
414
  function resolveWin32Command(cmd) {
346
415
  if (process.platform !== "win32") return cmd;
347
- if (cmd.includes("/") || cmd.includes("\\") || path7.extname(cmd.replace(/\//g, "\\"))) {
416
+ if (cmd.includes("/") || cmd.includes("\\") || path8.extname(cmd.replace(/\//g, "\\"))) {
348
417
  return cmd;
349
418
  }
350
419
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
351
- const pathDirs = (process.env["PATH"] ?? "").split(path7.delimiter);
420
+ const pathDirs = (process.env["PATH"] ?? "").split(path8.delimiter);
352
421
  for (const dir of pathDirs) {
353
- const base = path7.join(dir, cmd);
422
+ const base = path8.join(dir, cmd);
354
423
  for (const ext of pathext) {
355
424
  const full = `${base}${ext}`;
356
425
  try {
357
- fs6.accessSync(full, fs6.constants.X_OK);
426
+ fs7.accessSync(full, fs7.constants.X_OK);
358
427
  return full;
359
428
  } catch {
360
429
  }
@@ -368,6 +437,82 @@ var init_win32_resolve = __esm({
368
437
  }
369
438
  });
370
439
 
440
+ // src/codebase-index/parser-output.ts
441
+ function coerceSymbols(value) {
442
+ if (!Array.isArray(value)) return [];
443
+ return value.flatMap((entry) => {
444
+ const candidate = entry;
445
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
446
+ return [
447
+ {
448
+ name: candidate.name,
449
+ kind: candidate.kind,
450
+ line: typeof candidate.line === "number" ? candidate.line : 1,
451
+ col: typeof candidate.col === "number" ? candidate.col : 0,
452
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
453
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
454
+ }
455
+ ];
456
+ });
457
+ }
458
+ function coerceRefs(value, lang) {
459
+ if (!Array.isArray(value)) return [];
460
+ return value.flatMap((entry) => {
461
+ const candidate = entry;
462
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
463
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
464
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
465
+ return [
466
+ {
467
+ fromId: 0,
468
+ toName: candidate.toName,
469
+ callType: candidate.callType,
470
+ line: typeof candidate.line === "number" ? candidate.line : 1,
471
+ lang,
472
+ module
473
+ }
474
+ ];
475
+ });
476
+ }
477
+ function parseParserOutput(stdout, lang) {
478
+ const trimmed = stdout.trim();
479
+ if (!trimmed) return { symbols: [], refs: [] };
480
+ let parsed;
481
+ try {
482
+ parsed = JSON.parse(trimmed);
483
+ } catch {
484
+ return { symbols: [], refs: [] };
485
+ }
486
+ if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
487
+ const record = parsed;
488
+ return {
489
+ symbols: coerceSymbols(record.symbols),
490
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
491
+ };
492
+ }
493
+ function dedupeRefs(refs) {
494
+ const seen = /* @__PURE__ */ new Set();
495
+ return refs.filter((ref) => {
496
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
497
+ if (seen.has(key)) return false;
498
+ seen.add(key);
499
+ return true;
500
+ });
501
+ }
502
+ var CALL_TYPES;
503
+ var init_parser_output = __esm({
504
+ "src/codebase-index/parser-output.ts"() {
505
+ "use strict";
506
+ CALL_TYPES = /* @__PURE__ */ new Set([
507
+ "call",
508
+ "type_ref",
509
+ "inherit",
510
+ "implement",
511
+ "import"
512
+ ]);
513
+ }
514
+ });
515
+
371
516
  // src/codebase-index/spawn-gate.ts
372
517
  function withSpawnGate(fn) {
373
518
  const run = chain.then(fn, fn);
@@ -393,8 +538,8 @@ __export(go_parser_exports, {
393
538
  });
394
539
  import { spawn as spawn2 } from "node:child_process";
395
540
  import * as os2 from "node:os";
396
- import * as path8 from "node:path";
397
- import * as fs7 from "node:fs/promises";
541
+ import * as path9 from "node:path";
542
+ import * as fs8 from "node:fs/promises";
398
543
  async function parseSymbols2(opts) {
399
544
  const { file, content, lang } = opts;
400
545
  try {
@@ -402,7 +547,8 @@ async function parseSymbols2(opts) {
402
547
  if (parsed.symbols.length > 0) {
403
548
  return parsed;
404
549
  }
405
- return fallbackParse(file, content, lang);
550
+ const fallback = fallbackParse(file, content, lang);
551
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
406
552
  } catch {
407
553
  return fallbackParse(file, content, lang);
408
554
  }
@@ -466,9 +612,9 @@ async function syncGoParse(filePath, content, lang) {
466
612
  try {
467
613
  let scriptPath = _cachedGoScriptPath;
468
614
  if (!scriptPath) {
469
- const tmpDir = await fs7.mkdtemp(path8.join(os2.tmpdir(), "ws-go-parse-"));
470
- scriptPath = path8.join(tmpDir, "parse.go");
471
- await fs7.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
615
+ const tmpDir = await fs8.mkdtemp(path9.join(os2.tmpdir(), "ws-go-parse-"));
616
+ scriptPath = path9.join(tmpDir, "parse.go");
617
+ await fs8.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
472
618
  _cachedGoScriptPath = scriptPath;
473
619
  }
474
620
  const goBinary = resolveWin32Command("go");
@@ -510,8 +656,8 @@ async function syncGoParse(filePath, content, lang) {
510
656
  if (code !== 0 || !stdout.trim()) {
511
657
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
512
658
  }
513
- const raw = JSON.parse(stdout.trim());
514
- const symbols = raw.map((s) => ({
659
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
660
+ const symbols = rawSymbols.map((s) => ({
515
661
  id: 0,
516
662
  lang,
517
663
  kind: s.kind,
@@ -524,7 +670,7 @@ async function syncGoParse(filePath, content, lang) {
524
670
  scope: s.scope ?? "",
525
671
  text: `${s.name} ${s.signature ?? ""}`.trim()
526
672
  }));
527
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
673
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
528
674
  } catch {
529
675
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
530
676
  }
@@ -534,6 +680,7 @@ var init_go_parser = __esm({
534
680
  "src/codebase-index/go-parser.ts"() {
535
681
  "use strict";
536
682
  init_win32_resolve();
683
+ init_parser_output();
537
684
  init_spawn_gate();
538
685
  init_languages();
539
686
  GO_PARSE_SCRIPT = `
@@ -547,6 +694,7 @@ import (
547
694
  "go/token"
548
695
  "io"
549
696
  "os"
697
+ "strconv"
550
698
  "strings"
551
699
  )
552
700
 
@@ -559,16 +707,34 @@ type Sym struct {
559
707
  Scope string \`json:"scope"\`
560
708
  }
561
709
 
710
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
711
+ // yields both. Module is the import path for CallType "import", else empty.
712
+ type Ref struct {
713
+ ToName string \`json:"toName"\`
714
+ CallType string \`json:"callType"\`
715
+ Line int \`json:"line"\`
716
+ Module string \`json:"module"\`
717
+ }
718
+
719
+ type Result struct {
720
+ Symbols []Sym \`json:"symbols"\`
721
+ Refs []Ref \`json:"refs"\`
722
+ }
723
+
724
+ func emptyResult() string {
725
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
726
+ }
727
+
562
728
  func main() {
563
729
  src, err := io.ReadAll(os.Stdin)
564
730
  if err != nil {
565
- fmt.Print("[]")
731
+ fmt.Print(emptyResult())
566
732
  return
567
733
  }
568
734
  fset := token.NewFileSet()
569
735
  node, err := parser.ParseFile(fset, "src.go", src, 0)
570
736
  if err != nil {
571
- fmt.Print("[]")
737
+ fmt.Print(emptyResult())
572
738
  return
573
739
  }
574
740
 
@@ -632,9 +798,43 @@ func main() {
632
798
  }
633
799
  }
634
800
 
635
- data, err := json.Marshal(syms)
801
+ refs := []Ref{}
802
+ ast.Inspect(node, func(n ast.Node) bool {
803
+ switch expr := n.(type) {
804
+ case *ast.CallExpr:
805
+ line := fset.Position(expr.Pos()).Line
806
+ switch fun := expr.Fun.(type) {
807
+ case *ast.Ident:
808
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
809
+ case *ast.SelectorExpr:
810
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
811
+ // declared symbol name, so it resolves the same way the TypeScript
812
+ // and Python extractors' call refs do.
813
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
814
+ }
815
+ case *ast.ImportSpec:
816
+ if expr.Path != nil {
817
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
818
+ line := fset.Position(expr.Pos()).Line
819
+ // A Go import names a package, not a symbol; the package's
820
+ // last path segment is the name it is referenced by.
821
+ name := importPath
822
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
823
+ name = importPath[idx+1:]
824
+ }
825
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
826
+ }
827
+ }
828
+ }
829
+ return true
830
+ })
831
+
832
+ if syms == nil {
833
+ syms = []Sym{}
834
+ }
835
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
636
836
  if err != nil {
637
- fmt.Print("[]")
837
+ fmt.Print(emptyResult())
638
838
  return
639
839
  }
640
840
  fmt.Print(string(data))
@@ -986,9 +1186,13 @@ var init_generic_parser = __esm({
986
1186
  ],
987
1187
  elixir: [
988
1188
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
989
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
1189
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
1190
+ // against this symbol, and a `Foo`-only capture never matches it.
1191
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
990
1192
  ],
991
1193
  haskell: [
1194
+ // Target of `import Data.List`.
1195
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
992
1196
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
993
1197
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
994
1198
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -1079,9 +1283,9 @@ __export(py_parser_exports, {
1079
1283
  parseSymbols: () => parseSymbols4
1080
1284
  });
1081
1285
  import { spawn as spawn3 } from "node:child_process";
1082
- import * as fs8 from "node:fs/promises";
1286
+ import * as fs9 from "node:fs/promises";
1083
1287
  import * as os3 from "node:os";
1084
- import * as path9 from "node:path";
1288
+ import * as path10 from "node:path";
1085
1289
  async function parseSymbols4(opts) {
1086
1290
  const { file, content, lang } = opts;
1087
1291
  try {
@@ -1159,10 +1363,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1159
1363
  async function syncPyParse(filePath, content, lang) {
1160
1364
  try {
1161
1365
  if (!_cachedScriptPath) {
1162
- const tmpDir = path9.join(os3.tmpdir(), "ws-py-parse");
1163
- await fs8.mkdir(tmpDir, { recursive: true });
1164
- _cachedScriptPath = path9.join(tmpDir, "parse.py");
1165
- await fs8.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1366
+ const tmpDir = path10.join(os3.tmpdir(), "ws-py-parse");
1367
+ await fs9.mkdir(tmpDir, { recursive: true });
1368
+ _cachedScriptPath = path10.join(tmpDir, "parse.py");
1369
+ await fs9.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1166
1370
  }
1167
1371
  cachedPyBinary ??= resolvePython();
1168
1372
  const pyBinary = await cachedPyBinary;
@@ -1176,7 +1380,7 @@ async function syncPyParse(filePath, content, lang) {
1176
1380
  if (code !== 0 || !stdout.trim()) {
1177
1381
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1178
1382
  }
1179
- const raw = JSON.parse(stdout.trim());
1383
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
1180
1384
  const symbols = raw.map((s) => ({
1181
1385
  id: 0,
1182
1386
  lang,
@@ -1190,7 +1394,7 @@ async function syncPyParse(filePath, content, lang) {
1190
1394
  scope: s.scope ?? "",
1191
1395
  text: `${s.name} ${s.signature ?? ""}`.trim()
1192
1396
  }));
1193
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1397
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
1194
1398
  } catch {
1195
1399
  return null;
1196
1400
  }
@@ -1201,6 +1405,7 @@ var init_py_parser = __esm({
1201
1405
  "use strict";
1202
1406
  init_win32_resolve();
1203
1407
  init_generic_parser();
1408
+ init_parser_output();
1204
1409
  init_spawn_gate();
1205
1410
  init_languages();
1206
1411
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -1262,7 +1467,18 @@ class Sym:
1262
1467
  def is_private(name):
1263
1468
  return name.startswith("__") and not name.endswith("__")
1264
1469
 
1470
+ def leaf_name(node):
1471
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
1472
+ # TypeScript and Go extractors record call refs, so resolution behaves the
1473
+ # same across languages.
1474
+ if isinstance(node, ast.Attribute):
1475
+ return node.attr
1476
+ if isinstance(node, ast.Name):
1477
+ return node.id
1478
+ return get_name(node).split(".")[-1]
1479
+
1265
1480
  syms = []
1481
+ refs = []
1266
1482
  errors = []
1267
1483
 
1268
1484
  try:
@@ -1270,7 +1486,7 @@ try:
1270
1486
  tree = ast.parse(source, filename=sys.argv[1])
1271
1487
  except Exception as e:
1272
1488
  errors.append(str(e))
1273
- print("[]")
1489
+ print(json.dumps({"symbols": [], "refs": []}))
1274
1490
  sys.exit(0)
1275
1491
 
1276
1492
  # Module-level scope
@@ -1404,7 +1620,42 @@ class ModuleVisitor(ast.NodeVisitor):
1404
1620
  visitor = ModuleVisitor()
1405
1621
  visitor.visit(tree)
1406
1622
 
1407
- print(json.dumps([s.to_dict() for s in syms]))
1623
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
1624
+ # into function bodies (it would index locals as symbols), but that is exactly
1625
+ # where the calls are.
1626
+ for node in ast.walk(tree):
1627
+ if isinstance(node, ast.Call):
1628
+ name = leaf_name(node.func)
1629
+ if name:
1630
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
1631
+ elif isinstance(node, ast.Import):
1632
+ for alias in node.names:
1633
+ refs.append({
1634
+ "toName": alias.name.split(".")[-1],
1635
+ "callType": "import",
1636
+ "line": node.lineno,
1637
+ "module": alias.name,
1638
+ })
1639
+ elif isinstance(node, ast.ImportFrom):
1640
+ # PEP 328: node.level is the number of leading dots. Preserving them is
1641
+ # what lets the resolver walk up from the importing file's package \u2014
1642
+ # dropping them made \`from .foo import X\` indistinguishable from an
1643
+ # absolute \`foo\`.
1644
+ module = ("." * (node.level or 0)) + (node.module or "")
1645
+ for alias in node.names:
1646
+ refs.append({
1647
+ "toName": alias.name,
1648
+ "callType": "import",
1649
+ "line": node.lineno,
1650
+ "module": module,
1651
+ })
1652
+ elif isinstance(node, ast.ClassDef):
1653
+ for base in node.bases:
1654
+ name = leaf_name(base)
1655
+ if name:
1656
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1657
+
1658
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1408
1659
  `;
1409
1660
  _cachedScriptPath = null;
1410
1661
  }
@@ -1417,107 +1668,10 @@ __export(rs_parser_exports, {
1417
1668
  parseSymbols: () => parseSymbols5
1418
1669
  });
1419
1670
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1420
- import { execFile, spawn as spawn4 } from "node:child_process";
1421
- import * as fs9 from "node:fs/promises";
1422
- import * as path10 from "node:path";
1423
1671
  async function parseSymbols5(opts) {
1424
1672
  const { file, content, lang } = opts;
1425
- const nativeAvailable = await checkNativeParser();
1426
- if (nativeAvailable) {
1427
- const result = await withSpawnGate(() => tryNativeParse(file, content));
1428
- if (result) return result;
1429
- }
1430
1673
  return regexParse({ file, content, lang });
1431
1674
  }
1432
- function probe(command, args) {
1433
- return new Promise((resolve4, reject) => {
1434
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
1435
- if (error) reject(error);
1436
- else resolve4();
1437
- });
1438
- });
1439
- }
1440
- function checkNativeParser() {
1441
- nativeParserAvailability ??= (async () => {
1442
- try {
1443
- await probe("rustc", ["--version"]);
1444
- const toolsDir = path10.join(process.cwd(), "tools");
1445
- await probe(
1446
- "cargo",
1447
- [
1448
- "metadata",
1449
- "--no-deps",
1450
- "--format-version",
1451
- "1",
1452
- "--manifest-path",
1453
- path10.join(toolsDir, "Cargo.toml")
1454
- ]
1455
- );
1456
- return true;
1457
- } catch {
1458
- return false;
1459
- }
1460
- })();
1461
- return nativeParserAvailability;
1462
- }
1463
- async function tryNativeParse(file, content) {
1464
- try {
1465
- const toolsDir = path10.join(process.cwd(), "tools");
1466
- const crateDir = path10.join(toolsDir, "syn-parser");
1467
- const tmpFile = path10.join(crateDir, "src", "input.rs");
1468
- await fs9.writeFile(tmpFile, content, "utf8");
1469
- const cargoBinary = resolveWin32Command("cargo");
1470
- const result = await new Promise(
1471
- (resolve4, reject) => {
1472
- let settled = false;
1473
- const proc = spawn4(
1474
- cargoBinary,
1475
- ["run", "--manifest-path", path10.join(toolsDir, "Cargo.toml")],
1476
- {
1477
- cwd: process.cwd(),
1478
- stdio: ["pipe", "pipe", "pipe"],
1479
- windowsHide: true
1480
- }
1481
- );
1482
- proc.on("error", (err) => {
1483
- if (settled) return;
1484
- settled = true;
1485
- reject(err);
1486
- });
1487
- let stdout2 = "";
1488
- proc.stdout?.on("data", (chunk) => {
1489
- stdout2 += chunk.toString();
1490
- });
1491
- proc.stderr?.resume();
1492
- const timer = setTimeout(() => {
1493
- if (settled) return;
1494
- settled = true;
1495
- proc.kill("SIGKILL");
1496
- reject(new Error("timeout"));
1497
- }, 15e3);
1498
- timer.unref?.();
1499
- proc.on("close", (c) => {
1500
- if (settled) return;
1501
- settled = true;
1502
- clearTimeout(timer);
1503
- resolve4({ code: c, stdout: stdout2 });
1504
- });
1505
- }
1506
- );
1507
- const { code, stdout } = result;
1508
- if (code === 0 && stdout.trim()) {
1509
- const symbols = JSON.parse(stdout.trim());
1510
- return {
1511
- file,
1512
- lang: "rs",
1513
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
1514
- mtimeMs: Date.now()
1515
- };
1516
- }
1517
- } catch {
1518
- }
1519
- return null;
1520
- }
1521
1675
  function regexParse(opts) {
1522
1676
  const { file, content, lang } = opts;
1523
1677
  const symbols = [];
@@ -1573,12 +1727,10 @@ function regexParse(opts) {
1573
1727
  });
1574
1728
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
1575
1729
  }
1576
- var nativeParserAvailability, RS_PATTERNS;
1730
+ var RS_PATTERNS;
1577
1731
  var init_rs_parser = __esm({
1578
1732
  "src/codebase-index/rs-parser.ts"() {
1579
1733
  "use strict";
1580
- init_win32_resolve();
1581
- init_spawn_gate();
1582
1734
  init_languages();
1583
1735
  RS_PATTERNS = [
1584
1736
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -1972,7 +2124,7 @@ var init_yaml_parser = __esm({
1972
2124
 
1973
2125
  // src/codebase-index/project-server-client.ts
1974
2126
  import { spawn } from "node:child_process";
1975
- import * as fs4 from "node:fs";
2127
+ import * as fs5 from "node:fs";
1976
2128
  import * as net from "node:net";
1977
2129
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1978
2130
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
@@ -2062,16 +2214,16 @@ function resetIndexCircuitBreaker() {
2062
2214
 
2063
2215
  // src/codebase-index/project-server-endpoint.ts
2064
2216
  import { createHash } from "node:crypto";
2065
- import * as fs3 from "node:fs";
2217
+ import * as fs4 from "node:fs";
2066
2218
  import * as os from "node:os";
2067
- import * as path4 from "node:path";
2219
+ import * as path5 from "node:path";
2068
2220
  import { fileURLToPath } from "node:url";
2069
2221
  import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
2070
2222
 
2071
2223
  // src/codebase-index/writer.ts
2072
2224
  import { expectDefined } from "@wrongstack/core/utils";
2073
- import * as fs2 from "node:fs";
2074
- import * as path3 from "node:path";
2225
+ import * as fs3 from "node:fs";
2226
+ import * as path4 from "node:path";
2075
2227
 
2076
2228
  // src/codebase-index/bm25.ts
2077
2229
  var K1 = 1.5;
@@ -2162,6 +2314,9 @@ var Bm25Index = class {
2162
2314
  }
2163
2315
  };
2164
2316
 
2317
+ // src/codebase-index/writer.ts
2318
+ init_languages();
2319
+
2165
2320
  // src/codebase-index/lsp-kind.ts
2166
2321
  function lspKindToInternalKind(k) {
2167
2322
  switch (k) {
@@ -2225,7 +2380,7 @@ function internalKindToLspKind(k) {
2225
2380
  }
2226
2381
 
2227
2382
  // src/codebase-index/schema.ts
2228
- var SCHEMA_VERSION = 3;
2383
+ var SCHEMA_VERSION = 4;
2229
2384
 
2230
2385
  // src/codebase-index/sqlite-runtime.ts
2231
2386
  import { createRequire } from "node:module";
@@ -2296,7 +2451,7 @@ function runSqliteWithRetry(fn) {
2296
2451
 
2297
2452
  // src/codebase-index/writer-admin.ts
2298
2453
  import * as fs from "node:fs";
2299
- import * as path from "node:path";
2454
+ import * as path2 from "node:path";
2300
2455
  var DB_FILE = "index.db";
2301
2456
  function getAllIndexableWithStatement(stmt) {
2302
2457
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -2355,7 +2510,7 @@ function getAllFileMetasWithStatement(stmt) {
2355
2510
  }
2356
2511
  function getIndexDbSizeBytes(indexDir) {
2357
2512
  try {
2358
- return fs.statSync(path.join(indexDir, DB_FILE)).size;
2513
+ return fs.statSync(path2.join(indexDir, DB_FILE)).size;
2359
2514
  } catch {
2360
2515
  return 0;
2361
2516
  }
@@ -2406,49 +2561,345 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
2406
2561
  }
2407
2562
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2408
2563
  if (refs.length === 0) return;
2409
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
2564
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
2410
2565
  for (let i = 0; i < refs.length; i += chunkSize) {
2411
2566
  const chunk = refs.slice(i, i + chunkSize);
2412
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
2567
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
2413
2568
  const insert = stmt(
2414
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
2569
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
2570
+ VALUES ${placeholders}`
2415
2571
  );
2416
2572
  const binds = [];
2417
2573
  for (const ref of chunk) {
2418
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
2574
+ binds.push(
2575
+ ref.fromId,
2576
+ ref.toName,
2577
+ ref.toId ?? null,
2578
+ ref.callType,
2579
+ ref.line,
2580
+ ref.lang ?? "",
2581
+ ref.module ?? null,
2582
+ ref.toFile ?? null
2583
+ );
2419
2584
  }
2420
2585
  insert.run(...binds);
2421
2586
  }
2422
2587
  }
2423
2588
 
2424
- // src/codebase-index/writer-graph-helpers.ts
2425
- import * as path2 from "node:path";
2426
- function derivePackage(filePath) {
2427
- const f = filePath.replace(/\\/g, "/");
2428
- const pkgsIdx = f.indexOf("/packages/");
2429
- if (pkgsIdx !== -1) {
2430
- const rest = f.slice(pkgsIdx + "/packages/".length);
2431
- const seg = rest.split("/")[0];
2432
- return seg ? `@wrongstack/${seg}` : void 0;
2433
- }
2434
- const appsIdx = f.indexOf("/apps/");
2589
+ // src/codebase-index/writer-graph-reader.ts
2590
+ init_languages();
2591
+
2592
+ // src/codebase-index/module-roots.ts
2593
+ init_languages();
2594
+ import * as fs2 from "node:fs/promises";
2595
+ import * as path3 from "node:path";
2596
+ function toPortablePath(file) {
2597
+ return file.replace(/\\/g, "/");
2598
+ }
2599
+ async function readTextIfPresent(file) {
2600
+ try {
2601
+ return await fs2.readFile(file, "utf8");
2602
+ } catch {
2603
+ return void 0;
2604
+ }
2605
+ }
2606
+ function parsePackageJsonName(source) {
2607
+ try {
2608
+ const parsed = JSON.parse(source);
2609
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
2610
+ } catch {
2611
+ return void 0;
2612
+ }
2613
+ }
2614
+ function parseGoModulePath(source) {
2615
+ for (const rawLine of source.split(/\r?\n/)) {
2616
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
2617
+ const match = /^module\s+(\S+)/.exec(line);
2618
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
2619
+ }
2620
+ return void 0;
2621
+ }
2622
+ function parseTomlTableName(source, tables) {
2623
+ let current = "";
2624
+ for (const rawLine of source.split(/\r?\n/)) {
2625
+ const line = rawLine.replace(/#.*$/, "").trim();
2626
+ if (line.startsWith("[[")) {
2627
+ current = "\0";
2628
+ continue;
2629
+ }
2630
+ const table = /^\[([^\]]+)\]$/.exec(line);
2631
+ if (table?.[1]) {
2632
+ current = table[1].trim();
2633
+ continue;
2634
+ }
2635
+ if (!tables.includes(current)) continue;
2636
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
2637
+ if (match?.[1]) return match[1];
2638
+ }
2639
+ return void 0;
2640
+ }
2641
+ function parsePomArtifactId(source) {
2642
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
2643
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
2644
+ }
2645
+ var LANGS_BY_KIND = {
2646
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
2647
+ cargo: ["rs"],
2648
+ go: ["go"],
2649
+ python: ["py"],
2650
+ maven: ["java", "kotlin", "scala"],
2651
+ gradle: ["java", "kotlin", "scala"],
2652
+ dotnet: ["csharp"]
2653
+ };
2654
+ function ancestorsOf(dir, stopAt) {
2655
+ const out = [];
2656
+ let current = dir;
2657
+ for (; ; ) {
2658
+ out.push(current);
2659
+ if (current === stopAt || current.length <= stopAt.length) break;
2660
+ const parent = path3.posix.dirname(current);
2661
+ if (parent === current) break;
2662
+ current = parent;
2663
+ }
2664
+ return out;
2665
+ }
2666
+ var MARKER_PROBES = [
2667
+ {
2668
+ kind: "npm",
2669
+ file: "package.json",
2670
+ build: (dir, source) => {
2671
+ const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
2672
+ return { name, importPath: name, sourceRoots: [dir] };
2673
+ }
2674
+ },
2675
+ {
2676
+ kind: "cargo",
2677
+ file: "Cargo.toml",
2678
+ build: (dir, source) => {
2679
+ const name = parseTomlTableName(source, ["package"]);
2680
+ if (!name) return void 0;
2681
+ return {
2682
+ name: `crate:${name}`,
2683
+ // Rust paths use underscores where crate names often use dashes.
2684
+ importPath: name.replace(/-/g, "_"),
2685
+ sourceRoots: [path3.posix.join(dir, "src")]
2686
+ };
2687
+ }
2688
+ },
2689
+ {
2690
+ kind: "go",
2691
+ file: "go.mod",
2692
+ build: (dir, source) => {
2693
+ const modulePath = parseGoModulePath(source);
2694
+ if (!modulePath) return void 0;
2695
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
2696
+ }
2697
+ },
2698
+ {
2699
+ kind: "python",
2700
+ file: "pyproject.toml",
2701
+ build: (dir, source) => {
2702
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
2703
+ return {
2704
+ name: `py:${name}`,
2705
+ importPath: void 0,
2706
+ // `src/` layout is the packaging-guide default; the root itself covers
2707
+ // the flat layout. Both are probed, missing ones simply never match.
2708
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2709
+ };
2710
+ }
2711
+ },
2712
+ {
2713
+ kind: "python",
2714
+ file: "setup.py",
2715
+ build: (dir) => ({
2716
+ name: `py:${path3.posix.basename(dir)}`,
2717
+ importPath: void 0,
2718
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2719
+ })
2720
+ },
2721
+ {
2722
+ kind: "maven",
2723
+ file: "pom.xml",
2724
+ build: (dir, source) => {
2725
+ const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
2726
+ return {
2727
+ name: `mvn:${artifactId}`,
2728
+ importPath: void 0,
2729
+ sourceRoots: [
2730
+ path3.posix.join(dir, "src/main/java"),
2731
+ path3.posix.join(dir, "src/main/kotlin"),
2732
+ path3.posix.join(dir, "src/main/scala"),
2733
+ path3.posix.join(dir, "src/test/java")
2734
+ ]
2735
+ };
2736
+ }
2737
+ },
2738
+ {
2739
+ kind: "gradle",
2740
+ file: "build.gradle",
2741
+ build: (dir) => buildGradleRoot(dir)
2742
+ },
2743
+ {
2744
+ kind: "gradle",
2745
+ file: "build.gradle.kts",
2746
+ build: (dir) => buildGradleRoot(dir)
2747
+ }
2748
+ ];
2749
+ function buildGradleRoot(dir) {
2750
+ return {
2751
+ name: `gradle:${path3.posix.basename(dir)}`,
2752
+ importPath: void 0,
2753
+ sourceRoots: [
2754
+ path3.posix.join(dir, "src/main/java"),
2755
+ path3.posix.join(dir, "src/main/kotlin"),
2756
+ path3.posix.join(dir, "src/main/scala")
2757
+ ]
2758
+ };
2759
+ }
2760
+ async function probeDotnetRoot(dir) {
2761
+ let entries;
2762
+ try {
2763
+ entries = await fs2.readdir(dir);
2764
+ } catch {
2765
+ return void 0;
2766
+ }
2767
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
2768
+ if (!project) return void 0;
2769
+ const name = project.slice(0, -".csproj".length);
2770
+ return {
2771
+ dir,
2772
+ kind: "dotnet",
2773
+ name: `csproj:${name}`,
2774
+ importPath: void 0,
2775
+ sourceRoots: [dir]
2776
+ };
2777
+ }
2778
+ async function detectModuleRoots(projectRoot, files) {
2779
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
2780
+ const langsByDir = /* @__PURE__ */ new Map();
2781
+ for (const file of files) {
2782
+ const portable = toPortablePath(file);
2783
+ const lang = detectLang(portable);
2784
+ if (!lang) continue;
2785
+ const dir = path3.posix.dirname(portable);
2786
+ let langs = langsByDir.get(dir);
2787
+ if (!langs) {
2788
+ langs = /* @__PURE__ */ new Set();
2789
+ langsByDir.set(dir, langs);
2790
+ }
2791
+ langs.add(lang);
2792
+ }
2793
+ const candidates = /* @__PURE__ */ new Map();
2794
+ for (const [dir, langs] of langsByDir) {
2795
+ for (const ancestor of ancestorsOf(dir, root)) {
2796
+ let merged = candidates.get(ancestor);
2797
+ if (!merged) {
2798
+ merged = /* @__PURE__ */ new Set();
2799
+ candidates.set(ancestor, merged);
2800
+ }
2801
+ for (const lang of langs) merged.add(lang);
2802
+ }
2803
+ }
2804
+ const roots = [];
2805
+ await Promise.all(
2806
+ [...candidates].map(async ([dir, langs]) => {
2807
+ for (const probe of MARKER_PROBES) {
2808
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
2809
+ const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
2810
+ if (source === void 0) continue;
2811
+ const built = probe.build(dir, source);
2812
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
2813
+ }
2814
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
2815
+ const dotnet = await probeDotnetRoot(dir);
2816
+ if (dotnet) roots.push(dotnet);
2817
+ }
2818
+ })
2819
+ );
2820
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
2821
+ return { projectRoot: root, roots };
2822
+ }
2823
+ function findOwningRoot(structure, file, kinds) {
2824
+ const portable = toPortablePath(file);
2825
+ for (const root of structure.roots) {
2826
+ if (kinds && !kinds.includes(root.kind)) continue;
2827
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
2828
+ }
2829
+ return void 0;
2830
+ }
2831
+ function derivePackageFromLayout(filePath) {
2832
+ const portable = toPortablePath(filePath);
2833
+ const packagesIdx = portable.indexOf("/packages/");
2834
+ if (packagesIdx !== -1) {
2835
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
2836
+ if (segment) return `@wrongstack/${segment}`;
2837
+ }
2838
+ const appsIdx = portable.indexOf("/apps/");
2435
2839
  if (appsIdx !== -1) {
2436
- const rest = f.slice(appsIdx + "/apps/".length);
2437
- const seg = rest.split("/")[0];
2438
- return seg ? `app:${seg}` : void 0;
2840
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
2841
+ if (segment) return `app:${segment}`;
2439
2842
  }
2440
2843
  return void 0;
2441
2844
  }
2442
- function packageFromImport(moduleName) {
2443
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
2444
- const parts = moduleName.split("/");
2445
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
2845
+ function pythonPackageLabel(structure, file, initDirs) {
2846
+ const portable = toPortablePath(file);
2847
+ const dir = path3.posix.dirname(portable);
2848
+ if (!initDirs.has(dir)) return void 0;
2849
+ const segments = [];
2850
+ let current = dir;
2851
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
2852
+ segments.unshift(path3.posix.basename(current));
2853
+ current = path3.posix.dirname(current);
2854
+ }
2855
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
2446
2856
  }
2447
- function buildPackageGraphNodes(fileCounts, files) {
2857
+ function assignPackageLabels(structure, files) {
2858
+ const initDirs = /* @__PURE__ */ new Set();
2859
+ for (const file of files) {
2860
+ const portable = toPortablePath(file);
2861
+ if (path3.posix.basename(portable) === "__init__.py") {
2862
+ initDirs.add(path3.posix.dirname(portable));
2863
+ }
2864
+ }
2865
+ const labels = /* @__PURE__ */ new Map();
2866
+ for (const file of files) {
2867
+ const portable = toPortablePath(file);
2868
+ const lang = detectLang(portable);
2869
+ if (lang === "go") {
2870
+ const owner2 = findOwningRoot(structure, portable, ["go"]);
2871
+ const dir = path3.posix.dirname(portable);
2872
+ if (owner2?.importPath) {
2873
+ const relative2 = path3.posix.relative(owner2.dir, dir);
2874
+ labels.set(file, relative2 ? `${owner2.importPath}/${relative2}` : owner2.importPath);
2875
+ } else {
2876
+ labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
2877
+ }
2878
+ continue;
2879
+ }
2880
+ if (lang === "py") {
2881
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
2882
+ if (dotted) {
2883
+ labels.set(file, dotted);
2884
+ continue;
2885
+ }
2886
+ }
2887
+ const owner = findOwningRoot(structure, portable);
2888
+ const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
2889
+ labels.set(file, label);
2890
+ }
2891
+ return labels;
2892
+ }
2893
+
2894
+ // src/codebase-index/writer-graph-helpers.ts
2895
+ function createPackageLabeller(stored) {
2896
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
2897
+ }
2898
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
2448
2899
  const pkgNodes = /* @__PURE__ */ new Map();
2449
2900
  const fileToPkg = /* @__PURE__ */ new Map();
2450
2901
  for (const { file, n } of fileCounts) {
2451
- const pkg = derivePackage(file) ?? "(root)";
2902
+ const pkg = packageOf(file);
2452
2903
  fileToPkg.set(file, pkg);
2453
2904
  const node = pkgNodes.get(pkg);
2454
2905
  if (node) {
@@ -2465,7 +2916,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2465
2916
  }
2466
2917
  }
2467
2918
  for (const { file } of files) {
2468
- const pkg = derivePackage(file) ?? "(root)";
2919
+ const pkg = packageOf(file);
2469
2920
  fileToPkg.set(file, pkg);
2470
2921
  const node = pkgNodes.get(pkg);
2471
2922
  if (node) {
@@ -2483,7 +2934,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2483
2934
  }
2484
2935
  return { pkgNodes, fileToPkg };
2485
2936
  }
2486
- function buildFileGraphNodeState(pkgSyms, localFiles) {
2937
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
2487
2938
  const fileNodes = /* @__PURE__ */ new Map();
2488
2939
  const symToFile = /* @__PURE__ */ new Map();
2489
2940
  const fileStats = /* @__PURE__ */ new Map();
@@ -2502,7 +2953,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2502
2953
  id: `file:${file}`,
2503
2954
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
2504
2955
  kind: "file",
2505
- package: derivePackage(file) ?? "(root)",
2956
+ package: packageOf(file),
2506
2957
  file,
2507
2958
  symbolCount: stats?.count ?? 0,
2508
2959
  lang: stats?.lang,
@@ -2514,7 +2965,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2514
2965
  }
2515
2966
  return { fileNodes, symToFile, fileStats, ensureFileNode };
2516
2967
  }
2517
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2968
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
2518
2969
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
2519
2970
  const aExternal = a.file === fileFilter ? 0 : 1;
2520
2971
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -2526,7 +2977,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2526
2977
  symbolId: s.id,
2527
2978
  symbolKind: s.kind,
2528
2979
  file: s.file,
2529
- package: derivePackage(s.file) ?? "(root)",
2980
+ package: packageOf(s.file),
2530
2981
  lang: s.lang,
2531
2982
  line: s.line,
2532
2983
  signature: s.signature,
@@ -2534,29 +2985,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2534
2985
  external: s.file !== fileFilter
2535
2986
  }));
2536
2987
  }
2537
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
2538
- if (!moduleName.startsWith(".")) return void 0;
2539
- const normalizedFrom = fromFile.replace(/\\/g, "/");
2540
- const absolute = path2.posix.normalize(
2541
- path2.posix.join(path2.posix.dirname(normalizedFrom), moduleName)
2542
- );
2543
- const extension = path2.posix.extname(absolute);
2544
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
2545
- const candidates = [
2546
- absolute,
2547
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
2548
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(absolute, `index${ext}`)),
2549
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(base, `index${ext}`))
2550
- ];
2551
- const indexedByPortablePath = new Map(
2552
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
2553
- );
2554
- for (const candidate of candidates) {
2555
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
2556
- if (indexed) return indexed;
2557
- }
2558
- return void 0;
2559
- }
2560
2988
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
2561
2989
  const key = `${source}\0${target}`;
2562
2990
  let edge = edgeMap.get(key);
@@ -2597,7 +3025,12 @@ function mapWriterRefRow(row) {
2597
3025
  toName: row.to_name,
2598
3026
  toId: row.to_id ?? void 0,
2599
3027
  callType: row.call_type,
2600
- line: row.line
3028
+ line: row.line,
3029
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
3030
+ // queries select; `undefined` keeps those rows valid Refs.
3031
+ lang: row.lang || void 0,
3032
+ module: row.module ?? void 0,
3033
+ toFile: row.to_file ?? void 0
2601
3034
  };
2602
3035
  }
2603
3036
 
@@ -2745,7 +3178,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
2745
3178
  function getPackageGraphWithStatement(stmt) {
2746
3179
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
2747
3180
  const files = stmt("SELECT DISTINCT file FROM files").all();
2748
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
3181
+ const packageOf = readPackageLabeller(stmt);
3182
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
2749
3183
  const refRows = stmt(
2750
3184
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
2751
3185
  FROM refs r
@@ -2756,32 +3190,42 @@ function getPackageGraphWithStatement(stmt) {
2756
3190
  ).all();
2757
3191
  const edgeMap = /* @__PURE__ */ new Map();
2758
3192
  for (const r of refRows) {
2759
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2760
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
3193
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3194
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
2761
3195
  if (fromPkg === toPkg) continue;
2762
3196
  const n = Number(r.n) || 0;
2763
3197
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
2764
3198
  }
2765
3199
  const importRows = stmt(
2766
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
3200
+ `SELECT s.file AS from_file,
3201
+ COALESCE(r.to_file, st.file) AS to_file,
3202
+ COUNT(*) AS n
2767
3203
  FROM refs r
2768
3204
  JOIN symbols s ON s.id = r.from_id
3205
+ LEFT JOIN symbols st ON st.id = r.to_id
2769
3206
  WHERE r.call_type = 'import'
2770
- GROUP BY r.to_name, s.file`
3207
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
3208
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
2771
3209
  ).all();
2772
3210
  for (const r of importRows) {
2773
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2774
- const toPkg = packageFromImport(r.to_name);
2775
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
3211
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3212
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
3213
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
2776
3214
  const n = Number(r.n) || 0;
2777
3215
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
2778
3216
  }
2779
3217
  const edges = materializeWeightedEdges(edgeMap, "pkg");
2780
3218
  return { nodes: [...pkgNodes.values()], edges };
2781
3219
  }
3220
+ function readPackageLabeller(stmt) {
3221
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
3222
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
3223
+ }
2782
3224
  function getFileGraphWithStatement(stmt, packageFilter) {
2783
3225
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
2784
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
3226
+ const packageOf = readPackageLabeller(stmt);
3227
+ const langOf = (file) => detectLang(file) ?? "other";
3228
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
2785
3229
  const localFiles = new Set(pkgFilePaths);
2786
3230
  if (localFiles.size === 0) return { nodes: [], edges: [] };
2787
3231
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -2790,9 +3234,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2790
3234
  ).all(...pkgFilePaths);
2791
3235
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
2792
3236
  pkgSyms,
2793
- localFiles
3237
+ localFiles,
3238
+ packageOf
2794
3239
  );
2795
- const indexedFiles = new Set(allFiles.map((f) => f.file));
2796
3240
  const refRows = stmt(
2797
3241
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
2798
3242
  FROM refs r
@@ -2815,7 +3259,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2815
3259
  for (const x of extras) {
2816
3260
  symToFile.set(x.id, x.file);
2817
3261
  if (!fileStats.has(x.file)) {
2818
- fileStats.set(x.file, { count: 0, lang: "ts" });
3262
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
2819
3263
  }
2820
3264
  }
2821
3265
  }
@@ -2832,17 +3276,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2832
3276
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
2833
3277
  }
2834
3278
  const importRows = stmt(
2835
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
3279
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
2836
3280
  FROM refs r
3281
+ LEFT JOIN symbols st ON st.id = r.to_id
2837
3282
  WHERE r.call_type = 'import'
3283
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
2838
3284
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
2839
- GROUP BY r.from_id, r.to_name`
3285
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
2840
3286
  ).all(...pkgFilePaths);
2841
3287
  for (const r of importRows) {
2842
3288
  const fromFile = symToFile.get(r.from_id);
2843
3289
  if (!fromFile || !localFiles.has(fromFile)) continue;
2844
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
3290
+ const toFile = r.to_file;
2845
3291
  if (!toFile || fromFile === toFile) continue;
3292
+ if (!fileStats.has(toFile)) {
3293
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
3294
+ }
2846
3295
  ensureFileNode(fromFile);
2847
3296
  ensureFileNode(toFile);
2848
3297
  const n = Number(r.n) || 0;
@@ -2892,7 +3341,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
2892
3341
  ).all(...missingIds);
2893
3342
  for (const s of extras) symById.set(s.id, s);
2894
3343
  }
2895
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
3344
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
2896
3345
  return { nodes, edges };
2897
3346
  }
2898
3347
 
@@ -2914,7 +3363,7 @@ function assignRefsToSymbols(refs, symbols) {
2914
3363
  }
2915
3364
  if (!owner && ref.callType === "import") owner = ordered[0];
2916
3365
  if (!owner || owner.id <= 0) continue;
2917
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3366
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
2918
3367
  if (seen.has(key)) continue;
2919
3368
  seen.add(key);
2920
3369
  assigned.push({ ...ref, fromId: owner.id });
@@ -2960,7 +3409,11 @@ var CORE_TABLES_SQL = `
2960
3409
  lang TEXT NOT NULL,
2961
3410
  mtime_ms INTEGER NOT NULL,
2962
3411
  symbol_count INTEGER NOT NULL DEFAULT 0,
2963
- last_indexed INTEGER NOT NULL
3412
+ last_indexed INTEGER NOT NULL,
3413
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
3414
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
3415
+ -- re-derived per query because the evidence lives on disk, not in the DB.
3416
+ package TEXT NOT NULL DEFAULT ''
2964
3417
  );
2965
3418
  CREATE TABLE IF NOT EXISTS symbols (
2966
3419
  id INTEGER PRIMARY KEY,
@@ -2977,6 +3430,9 @@ var CORE_TABLES_SQL = `
2977
3430
  file_fk TEXT NOT NULL
2978
3431
  );
2979
3432
  `;
3433
+ var FILE_INDEX_SQL = [
3434
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
3435
+ ];
2980
3436
  var SYMBOL_INDEX_SQL = [
2981
3437
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
2982
3438
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -2993,15 +3449,32 @@ var REFS_TABLE_SQL = `
2993
3449
  to_name TEXT NOT NULL,
2994
3450
  to_id INTEGER,
2995
3451
  call_type TEXT NOT NULL,
2996
- line INTEGER NOT NULL
3452
+ line INTEGER NOT NULL,
3453
+ lang TEXT NOT NULL DEFAULT '',
3454
+ module TEXT,
3455
+ to_file TEXT
2997
3456
  );
2998
3457
  `;
2999
3458
  var REFS_INDEX_SQL = [
3000
3459
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
3001
3460
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
3002
3461
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
3003
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
3462
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
3463
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
3464
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
3465
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
3466
+ // The post-index module resolution pass groups unresolved import refs by
3467
+ // (module, lang); graph readers then read to_file back.
3468
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
3469
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
3004
3470
  ];
3471
+ var LANG_FAMILY_TABLE_SQL = `
3472
+ CREATE TABLE IF NOT EXISTS lang_family (
3473
+ lang TEXT PRIMARY KEY,
3474
+ family TEXT NOT NULL
3475
+ );
3476
+ `;
3477
+ var LANG_FAMILY_WILDCARD = "*";
3005
3478
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
3006
3479
 
3007
3480
  // src/codebase-index/writer-search-helpers.ts
@@ -3213,15 +3686,69 @@ var IndexStore = class _IndexStore {
3213
3686
  }
3214
3687
  constructor(projectRoot, opts = {}) {
3215
3688
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
3216
- fs2.mkdirSync(this.indexDir, { recursive: true });
3689
+ fs3.mkdirSync(this.indexDir, { recursive: true });
3217
3690
  const Database = loadDatabaseSync();
3218
- this.db = new Database(path3.join(this.indexDir, DB_FILE2));
3691
+ this.db = new Database(path4.join(this.indexDir, DB_FILE2));
3219
3692
  applyIndexStorePragmas(this.db);
3220
3693
  this.initSchema();
3221
3694
  }
3222
3695
  runWithRetry(fn) {
3223
3696
  return runSqliteWithRetry(fn);
3224
3697
  }
3698
+ /**
3699
+ * Mirror the in-process language→family map into SQLite.
3700
+ *
3701
+ * Rewritten on every open rather than only on schema bumps: the mapping is
3702
+ * static lookup data, so a code-side change (a new language, a language
3703
+ * moving families) must take effect without forcing a full reindex.
3704
+ */
3705
+ seedLangFamilies() {
3706
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
3707
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
3708
+ insert.run("", LANG_FAMILY_WILDCARD);
3709
+ }
3710
+ /**
3711
+ * Add any column the current schema expects but the on-disk table lacks.
3712
+ *
3713
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
3714
+ * and the version check above only rebuilds on a version *mismatch*. That
3715
+ * leaves a real gap: several wstack processes share this database, and while
3716
+ * a version upgrade is rolling out one of them may still be running the
3717
+ * previous build. That older process sees the newer version number, drops the
3718
+ * tables, and recreates them from *its* DDL — without the newer columns —
3719
+ * while the metadata row still reads the new version. Every later query for
3720
+ * one of those columns then fails with `no such column`, and no amount of
3721
+ * reindexing fixes it, because the version numbers already agree.
3722
+ *
3723
+ * Repairing column-by-column makes the schema self-healing from any of those
3724
+ * states. Table and column names are compile-time literals from this module,
3725
+ * never user input.
3726
+ */
3727
+ repairMissingColumns() {
3728
+ const expected = [
3729
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
3730
+ {
3731
+ table: "refs",
3732
+ columns: [
3733
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
3734
+ ["module", "TEXT"],
3735
+ ["to_file", "TEXT"]
3736
+ ]
3737
+ }
3738
+ ];
3739
+ for (const { table, columns } of expected) {
3740
+ const present = new Set(
3741
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
3742
+ (row) => typeof row.name === "string" ? [row.name] : []
3743
+ )
3744
+ );
3745
+ if (present.size === 0) continue;
3746
+ for (const [name, type] of columns) {
3747
+ if (present.has(name)) continue;
3748
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
3749
+ }
3750
+ }
3751
+ }
3225
3752
  initSchema() {
3226
3753
  this.db.exec(METADATA_TABLE_SQL);
3227
3754
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -3244,9 +3771,13 @@ var IndexStore = class _IndexStore {
3244
3771
  );
3245
3772
  }
3246
3773
  this.db.exec(CORE_TABLES_SQL);
3247
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3248
3774
  this.db.exec(REFS_TABLE_SQL);
3775
+ this.repairMissingColumns();
3776
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
3777
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3249
3778
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
3779
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
3780
+ this.seedLangFamilies();
3250
3781
  try {
3251
3782
  this.db.exec(SYMBOLS_FTS_SQL);
3252
3783
  this.ftsAvailable = true;
@@ -3281,6 +3812,18 @@ var IndexStore = class _IndexStore {
3281
3812
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
3282
3813
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
3283
3814
  static MAX_SQL_VARS = 900;
3815
+ /**
3816
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
3817
+ * `sym` belong to the same language family — or the ref carries no language,
3818
+ * in which case the wildcard bind matches everything.
3819
+ *
3820
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
3821
+ */
3822
+ static FAMILY_MATCH_SQL = `(
3823
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
3824
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
3825
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
3826
+ )`;
3284
3827
  /**
3285
3828
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
3286
3829
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -3344,9 +3887,12 @@ var IndexStore = class _IndexStore {
3344
3887
  const placeholders = chunk.map(() => "?").join(",");
3345
3888
  const result = this.stmt(
3346
3889
  `UPDATE refs
3347
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
3890
+ SET to_id = (
3891
+ SELECT MIN(sym.id) FROM symbols sym
3892
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
3893
+ )
3348
3894
  WHERE to_name IN (${placeholders})`
3349
- ).run(...chunk);
3895
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
3350
3896
  changes += result.changes ?? 0;
3351
3897
  }
3352
3898
  return changes;
@@ -3473,6 +4019,115 @@ var IndexStore = class _IndexStore {
3473
4019
  getAllFileMetas() {
3474
4020
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
3475
4021
  }
4022
+ // ─── Project structure & module resolution ──────────────────────────────────
4023
+ /** Store the Code Atlas grouping label for each indexed file. */
4024
+ setFilePackages(entries) {
4025
+ if (entries.size === 0) return;
4026
+ this.runWithRetry(() => {
4027
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
4028
+ for (const [file, label] of entries) update.run(label, file);
4029
+ });
4030
+ }
4031
+ /**
4032
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
4033
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
4034
+ * Ordered so the resolver's choice among duplicate declarations is stable.
4035
+ */
4036
+ getNamespaceDeclarations() {
4037
+ return this.stmt(
4038
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
4039
+ ).all();
4040
+ }
4041
+ /** `file → package` for every indexed file that has a label. */
4042
+ getFilePackages() {
4043
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
4044
+ return new Map(rows.map((row) => [row.file, row.package]));
4045
+ }
4046
+ /**
4047
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
4048
+ *
4049
+ * Distinct rather than per-ref because resolution depends only on these three
4050
+ * values: a file importing the same module twenty times resolves it once.
4051
+ */
4052
+ getUnresolvedImports(onlyFiles) {
4053
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
4054
+ FROM refs r
4055
+ JOIN symbols s ON s.id = r.from_id
4056
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
4057
+ if (!onlyFiles?.length) {
4058
+ return this.stmt(base).all();
4059
+ }
4060
+ const out = [];
4061
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
4062
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
4063
+ const placeholders = chunk.map(() => "?").join(",");
4064
+ out.push(
4065
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
4066
+ );
4067
+ }
4068
+ return out;
4069
+ }
4070
+ /**
4071
+ * Write resolved import targets back onto `refs.to_file`.
4072
+ *
4073
+ * Applied through a temp table and a single UPDATE: one statement per
4074
+ * resolution would mean thousands of round-trips on a first index.
4075
+ */
4076
+ applyImportResolutions(resolutions) {
4077
+ if (resolutions.length === 0) return 0;
4078
+ return this.runWithRetry(() => {
4079
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4080
+ this.db.exec(
4081
+ `CREATE TEMP TABLE import_resolution (
4082
+ from_file TEXT NOT NULL,
4083
+ lang TEXT NOT NULL,
4084
+ module TEXT NOT NULL,
4085
+ to_file TEXT NOT NULL
4086
+ )`
4087
+ );
4088
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
4089
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
4090
+ const chunk = resolutions.slice(i, i + chunkSize);
4091
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
4092
+ const binds = [];
4093
+ for (const entry of chunk) {
4094
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
4095
+ }
4096
+ this.stmt(
4097
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
4098
+ VALUES ${placeholders}`
4099
+ ).run(...binds);
4100
+ }
4101
+ this.db.exec(
4102
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
4103
+ ON import_resolution(module, lang, from_file)`
4104
+ );
4105
+ const result = this.stmt(
4106
+ `UPDATE refs
4107
+ SET to_file = (
4108
+ SELECT ir.to_file
4109
+ FROM temp.import_resolution ir
4110
+ JOIN symbols s ON s.id = refs.from_id
4111
+ WHERE ir.module = refs.module
4112
+ AND ir.lang = refs.lang
4113
+ AND ir.from_file = s.file
4114
+ LIMIT 1
4115
+ )
4116
+ WHERE refs.call_type = 'import'
4117
+ AND refs.module IS NOT NULL
4118
+ AND EXISTS (
4119
+ SELECT 1
4120
+ FROM temp.import_resolution ir
4121
+ JOIN symbols s ON s.id = refs.from_id
4122
+ WHERE ir.module = refs.module
4123
+ AND ir.lang = refs.lang
4124
+ AND ir.from_file = s.file
4125
+ )`
4126
+ ).run();
4127
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4128
+ return result.changes ?? 0;
4129
+ });
4130
+ }
3476
4131
  // ─── Search ──────────────────────────────────────────────────────────────────
3477
4132
  search(query, filter, opts) {
3478
4133
  const built = this.buildSearchWhere(query, filter);
@@ -3859,9 +4514,12 @@ var IndexStore = class _IndexStore {
3859
4514
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
3860
4515
  * Call this after all symbols have been inserted to fill in cross-references.
3861
4516
  *
3862
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
3863
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
3864
- * that found a targetmatching the previous per-row loop's return value.
4517
+ * A match additionally requires the referencing ref and the target symbol to
4518
+ * be in the same {@link LangFamily}. Without that guard a name match is a
4519
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
4520
+ * `Config` are declared in most languages at once, and each collision draws a
4521
+ * Code Atlas edge between files that never reference each other. Refs stored
4522
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
3865
4523
  */
3866
4524
  resolveRefs() {
3867
4525
  return this.runWithRetry(() => {
@@ -3870,20 +4528,35 @@ var IndexStore = class _IndexStore {
3870
4528
  `UPDATE refs
3871
4529
  SET to_id = s.id
3872
4530
  FROM (
3873
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
3874
- ) AS s
4531
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
4532
+ FROM symbols sym
4533
+ JOIN lang_family lf ON lf.lang = sym.lang
4534
+ GROUP BY sym.name, lf.family
4535
+ UNION ALL
4536
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
4537
+ FROM symbols sym
4538
+ GROUP BY sym.name
4539
+ ) AS s,
4540
+ lang_family AS rf
3875
4541
  WHERE refs.to_id IS NULL
3876
4542
  AND refs.to_name IS NOT NULL
3877
- AND refs.to_name = s.name`
4543
+ AND rf.lang = refs.lang
4544
+ AND s.name = refs.to_name
4545
+ AND s.family = rf.family`
3878
4546
  ).run();
3879
4547
  return result.changes ?? 0;
3880
4548
  } catch {
3881
4549
  const result = this.stmt(
3882
4550
  `UPDATE refs SET to_id = (
3883
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
4551
+ SELECT sym.id FROM symbols sym
4552
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
4553
+ ORDER BY sym.id LIMIT 1
3884
4554
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
3885
- AND to_name IN (SELECT name FROM symbols)`
3886
- ).run();
4555
+ AND EXISTS (
4556
+ SELECT 1 FROM symbols sym
4557
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
4558
+ )`
4559
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
3887
4560
  return result.changes ?? 0;
3888
4561
  }
3889
4562
  });
@@ -4072,21 +4745,21 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
4072
4745
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
4073
4746
  var buildIdCache;
4074
4747
  function projectIndexServerBuildId(entrypoint) {
4075
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path4.resolve(entrypoint);
4748
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path5.resolve(entrypoint);
4076
4749
  try {
4077
- const stat2 = fs3.statSync(file);
4750
+ const stat2 = fs4.statSync(file);
4078
4751
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
4079
4752
  return buildIdCache.buildId;
4080
4753
  }
4081
- const buildId = createHash("sha256").update(fs3.readFileSync(file)).digest("hex").slice(0, 24);
4754
+ const buildId = createHash("sha256").update(fs4.readFileSync(file)).digest("hex").slice(0, 24);
4082
4755
  buildIdCache = { file, mtimeMs: stat2.mtimeMs, size: stat2.size, buildId };
4083
4756
  return buildId;
4084
4757
  } catch {
4085
- return `unreadable:${path4.basename(file)}`;
4758
+ return `unreadable:${path5.basename(file)}`;
4086
4759
  }
4087
4760
  }
4088
4761
  function normalizeLocalPath(value) {
4089
- const resolved = path4.resolve(value);
4762
+ const resolved = path5.resolve(value);
4090
4763
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4091
4764
  }
4092
4765
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -4098,11 +4771,11 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
4098
4771
  if (process.platform === "win32") {
4099
4772
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
4100
4773
  }
4101
- return path4.join(os.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
4774
+ return path5.join(os.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
4102
4775
  }
4103
4776
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
4104
- return path4.join(
4105
- path4.resolve(resolveIndexDir(projectRoot, indexDir)),
4777
+ return path5.join(
4778
+ path5.resolve(resolveIndexDir(projectRoot, indexDir)),
4106
4779
  PROJECT_INDEX_SERVER_METADATA_FILE
4107
4780
  );
4108
4781
  }
@@ -4142,7 +4815,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
4142
4815
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
4143
4816
  try {
4144
4817
  const url = new URL(rel, import.meta.url);
4145
- if (url.protocol === "file:" && fs4.existsSync(fileURLToPath2(url))) {
4818
+ if (url.protocol === "file:" && fs5.existsSync(fileURLToPath2(url))) {
4146
4819
  builtUrl = url;
4147
4820
  break;
4148
4821
  }
@@ -4399,7 +5072,7 @@ var ProjectServerConnection = class {
4399
5072
  currentAuthToken() {
4400
5073
  if (this.authToken === void 0) {
4401
5074
  try {
4402
- const raw = fs4.readFileSync(
5075
+ const raw = fs5.readFileSync(
4403
5076
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
4404
5077
  "utf8"
4405
5078
  );
@@ -4664,7 +5337,7 @@ var ProjectServerConnection = class {
4664
5337
  if (!url) throw new Error("built codebase-index project server is unavailable");
4665
5338
  if (process.platform !== "win32") {
4666
5339
  try {
4667
- fs4.rmSync(this.endpoint, { force: true });
5340
+ fs5.rmSync(this.endpoint, { force: true });
4668
5341
  } catch {
4669
5342
  }
4670
5343
  }
@@ -4688,8 +5361,8 @@ var ProjectServerConnection = class {
4688
5361
  process.kill(pid);
4689
5362
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
4690
5363
  try {
4691
- const metadata = JSON.parse(fs4.readFileSync(metadataPath, "utf8"));
4692
- if (metadata.pid === pid) fs4.rmSync(metadataPath, { force: true });
5364
+ const metadata = JSON.parse(fs5.readFileSync(metadataPath, "utf8"));
5365
+ if (metadata.pid === pid) fs5.rmSync(metadataPath, { force: true });
4693
5366
  } catch {
4694
5367
  }
4695
5368
  return true;
@@ -4793,7 +5466,7 @@ import { Worker } from "node:worker_threads";
4793
5466
 
4794
5467
  // src/codebase-index/indexer.ts
4795
5468
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
4796
- import { execFile as execFile2 } from "node:child_process";
5469
+ import { execFile } from "node:child_process";
4797
5470
  import * as fs10 from "node:fs/promises";
4798
5471
  import { availableParallelism } from "node:os";
4799
5472
  import * as path12 from "node:path";
@@ -4804,8 +5477,8 @@ import {
4804
5477
  } from "@wrongstack/core/utils";
4805
5478
 
4806
5479
  // src/codebase-index/gitignore.ts
4807
- import * as fs5 from "node:fs/promises";
4808
- import * as path5 from "node:path";
5480
+ import * as fs6 from "node:fs/promises";
5481
+ import * as path6 from "node:path";
4809
5482
  import { compileGlob } from "@wrongstack/core/utils";
4810
5483
  function globBody(glob) {
4811
5484
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -4851,7 +5524,7 @@ function compileGitignore(lines) {
4851
5524
  async function loadGitignoreMatcher(projectRoot) {
4852
5525
  let lines = [];
4853
5526
  try {
4854
- const raw = await fs5.readFile(path5.join(projectRoot, ".gitignore"), "utf8");
5527
+ const raw = await fs6.readFile(path6.join(projectRoot, ".gitignore"), "utf8");
4855
5528
  lines = raw.split("\n");
4856
5529
  } catch {
4857
5530
  }
@@ -4861,8 +5534,434 @@ async function loadGitignoreMatcher(projectRoot) {
4861
5534
  // src/codebase-index/indexer.ts
4862
5535
  init_languages();
4863
5536
 
5537
+ // src/codebase-index/module-resolver.ts
5538
+ init_languages();
5539
+ import * as path7 from "node:path";
5540
+ var EXTENSIONS = {
5541
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
5542
+ py: [".py", ".pyi"],
5543
+ rs: [".rs"],
5544
+ jvm: [".java", ".kt", ".scala"],
5545
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
5546
+ ruby: [".rb"],
5547
+ go: [".go"]
5548
+ };
5549
+ var DIRECTORY_ENTRIES = {
5550
+ js: ["index"],
5551
+ py: ["__init__"],
5552
+ rs: ["mod"],
5553
+ ruby: ["index"]
5554
+ };
5555
+ function normalizeNamespace(value) {
5556
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
5557
+ }
5558
+ var ModuleResolver = class {
5559
+ structure;
5560
+ /** Lowercased portable path → the path as indexed (case is preserved). */
5561
+ byPath;
5562
+ /** Lowercased portable directory → files directly inside it, as indexed. */
5563
+ byDir;
5564
+ /** Normalized namespace → the file declaring it (first by path, stable). */
5565
+ byNamespace;
5566
+ constructor(structure, files, namespaces = []) {
5567
+ this.structure = structure;
5568
+ this.byPath = /* @__PURE__ */ new Map();
5569
+ this.byDir = /* @__PURE__ */ new Map();
5570
+ this.byNamespace = /* @__PURE__ */ new Map();
5571
+ const dirsByKey = /* @__PURE__ */ new Map();
5572
+ for (const file of files) {
5573
+ const portable = toPortablePath(file);
5574
+ const pathKey = portable.toLowerCase();
5575
+ const priorPath = this.byPath.get(pathKey);
5576
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
5577
+ else this.byPath.set(pathKey, file);
5578
+ const dir = path7.posix.dirname(portable);
5579
+ const dirKey = dir.toLowerCase();
5580
+ const knownDir = dirsByKey.get(dirKey);
5581
+ if (knownDir === void 0) {
5582
+ dirsByKey.set(dirKey, dir);
5583
+ this.byDir.set(dirKey, [file]);
5584
+ } else if (knownDir === dir) {
5585
+ this.byDir.get(dirKey)?.push(file);
5586
+ } else {
5587
+ dirsByKey.delete(dirKey);
5588
+ this.byDir.delete(dirKey);
5589
+ }
5590
+ }
5591
+ for (const { name, file } of namespaces) {
5592
+ const lang = detectLang(file);
5593
+ if (!lang) continue;
5594
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
5595
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
5596
+ this.byNamespace.set(key, file);
5597
+ }
5598
+ }
5599
+ }
5600
+ /**
5601
+ * Resolve `specifier` as written in `fromFile`.
5602
+ * Returns the indexed target path, or `undefined` when it is external or
5603
+ * cannot be located.
5604
+ */
5605
+ resolve(fromFile, lang, specifier) {
5606
+ const spec = specifier.trim().replace(/\\/g, "/");
5607
+ if (!spec) return void 0;
5608
+ const from = toPortablePath(fromFile);
5609
+ switch (languageFamily(lang)) {
5610
+ case "js":
5611
+ return this.resolveJs(from, spec);
5612
+ case "go":
5613
+ return this.resolveGo(spec);
5614
+ case "py":
5615
+ return this.resolvePython(from, spec);
5616
+ case "rs":
5617
+ return this.resolveRust(from, spec);
5618
+ case "jvm":
5619
+ return this.resolveJvm(spec);
5620
+ case "c":
5621
+ return this.resolveInclude(from, spec);
5622
+ case "ruby":
5623
+ return this.resolveRuby(from, spec);
5624
+ case "dotnet":
5625
+ case "php":
5626
+ case "elixir":
5627
+ case "haskell":
5628
+ return this.resolveNamespace(lang, spec);
5629
+ default:
5630
+ return void 0;
5631
+ }
5632
+ }
5633
+ /**
5634
+ * Resolve a namespace specifier to the file declaring it.
5635
+ *
5636
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
5637
+ * names a namespace outright, while PHP's `use App\Models\User` names a
5638
+ * *class* inside `App\Models`, so the prefix is what was declared.
5639
+ */
5640
+ resolveNamespace(lang, spec) {
5641
+ const family = languageFamily(lang);
5642
+ const normalized = normalizeNamespace(spec);
5643
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
5644
+ if (exact) return exact;
5645
+ const segments = normalized.split(".").filter(Boolean);
5646
+ if (segments.length < 2) return void 0;
5647
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
5648
+ }
5649
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
5650
+ lookup(candidate) {
5651
+ return this.byPath.get(path7.posix.normalize(candidate).toLowerCase());
5652
+ }
5653
+ /**
5654
+ * Try `base` verbatim, then `base` + each extension, then each directory
5655
+ * entry point inside `base`.
5656
+ */
5657
+ lookupWithExtensions(base, family) {
5658
+ const direct = this.lookup(base);
5659
+ if (direct) return direct;
5660
+ const extensions = EXTENSIONS[family] ?? [];
5661
+ const suffix = path7.posix.extname(base);
5662
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
5663
+ for (const ext of extensions) {
5664
+ const hit = this.lookup(`${stem}${ext}`);
5665
+ if (hit) return hit;
5666
+ }
5667
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
5668
+ for (const ext of extensions) {
5669
+ const hit = this.lookup(path7.posix.join(base, `${entry}${ext}`));
5670
+ if (hit) return hit;
5671
+ }
5672
+ }
5673
+ return void 0;
5674
+ }
5675
+ /**
5676
+ * A representative indexed file inside `dir`, for ecosystems whose import
5677
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
5678
+ *
5679
+ * The choice is deterministic — a file named after the directory, else the
5680
+ * first by name — so the same import always produces the same edge. Package
5681
+ * grouping is unaffected either way: every file in the directory carries the
5682
+ * same package label, so the package-level edge is exact regardless of which
5683
+ * member represents it.
5684
+ */
5685
+ representativeIn(dir, family) {
5686
+ const members = this.byDir.get(path7.posix.normalize(dir).toLowerCase());
5687
+ if (!members?.length) return void 0;
5688
+ const extensions = EXTENSIONS[family] ?? [];
5689
+ const eligible = members.filter((file) => extensions.includes(path7.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
5690
+ if (eligible.length === 0) return void 0;
5691
+ const base = path7.posix.basename(path7.posix.normalize(dir)).toLowerCase();
5692
+ const named = eligible.find(
5693
+ (file) => path7.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
5694
+ );
5695
+ return named ?? eligible[0];
5696
+ }
5697
+ // ─── Per-family resolution ──────────────────────────────────────────────────
5698
+ /** Relative specifiers, then workspace package names and their subpaths. */
5699
+ resolveJs(fromFile, spec) {
5700
+ if (spec.startsWith(".")) {
5701
+ const absolute = path7.posix.join(path7.posix.dirname(fromFile), spec);
5702
+ return this.lookupWithExtensions(absolute, "js");
5703
+ }
5704
+ const owner = this.structure.roots.find(
5705
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
5706
+ );
5707
+ if (!owner?.importPath) return void 0;
5708
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
5709
+ if (!subpath) {
5710
+ return this.lookupWithExtensions(path7.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path7.posix.join(owner.dir, "index"), "js");
5711
+ }
5712
+ return this.lookupWithExtensions(path7.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path7.posix.join(owner.dir, "src", subpath), "js");
5713
+ }
5714
+ /** Go import paths are absolute module paths; a package is a directory. */
5715
+ resolveGo(spec) {
5716
+ const owner = this.structure.roots.find(
5717
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
5718
+ );
5719
+ if (!owner?.importPath) return void 0;
5720
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
5721
+ return this.representativeIn(path7.posix.join(owner.dir, subpath), "go");
5722
+ }
5723
+ /**
5724
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
5725
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
5726
+ */
5727
+ resolvePython(fromFile, spec) {
5728
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
5729
+ if (leadingDots > 0) {
5730
+ let base = path7.posix.dirname(fromFile);
5731
+ for (let i = 1; i < leadingDots; i++) base = path7.posix.dirname(base);
5732
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
5733
+ return this.lookupWithExtensions(path7.posix.join(base, ...rest), "py");
5734
+ }
5735
+ const segments = spec.split(".").filter(Boolean);
5736
+ if (segments.length === 0) return void 0;
5737
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
5738
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
5739
+ const hit = this.lookupWithExtensions(path7.posix.join(base, ...segments), "py");
5740
+ if (hit) return hit;
5741
+ if (segments.length > 1) {
5742
+ const parent = this.lookupWithExtensions(
5743
+ path7.posix.join(base, ...segments.slice(0, -1)),
5744
+ "py"
5745
+ );
5746
+ if (parent) return parent;
5747
+ }
5748
+ }
5749
+ return void 0;
5750
+ }
5751
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
5752
+ resolveRust(fromFile, spec) {
5753
+ const segments = spec.split("::").filter(Boolean);
5754
+ if (segments.length === 0) return void 0;
5755
+ const head = segments[0];
5756
+ if (head === "self" || head === "super") {
5757
+ let base = path7.posix.dirname(fromFile);
5758
+ for (const segment of segments) {
5759
+ if (segment === "super") base = path7.posix.dirname(base);
5760
+ else if (segment !== "self") break;
5761
+ }
5762
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
5763
+ return this.lookupWithExtensions(path7.posix.join(base, ...rest2), "rs");
5764
+ }
5765
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
5766
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
5767
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
5768
+ );
5769
+ if (!crate) {
5770
+ return this.lookupWithExtensions(
5771
+ path7.posix.join(path7.posix.dirname(fromFile), ...segments),
5772
+ "rs"
5773
+ );
5774
+ }
5775
+ const rest = segments.slice(1);
5776
+ for (const base of crate.sourceRoots) {
5777
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path7.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
5778
+ const exact = this.lookupWithExtensions(path7.posix.join(base, ...rest), "rs");
5779
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path7.posix.join(base, "lib"), "rs");
5780
+ if (hit) return hit;
5781
+ }
5782
+ return void 0;
5783
+ }
5784
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
5785
+ resolveJvm(spec) {
5786
+ const segments = spec.split(".").filter(Boolean);
5787
+ if (segments.length === 0) return void 0;
5788
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
5789
+ const wildcard = segments[segments.length - 1] === "*";
5790
+ const parts = wildcard ? segments.slice(0, -1) : segments;
5791
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
5792
+ const target = path7.posix.join(base, ...parts);
5793
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
5794
+ if (hit) return hit;
5795
+ }
5796
+ return void 0;
5797
+ }
5798
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
5799
+ resolveInclude(fromFile, spec) {
5800
+ const relative2 = this.lookupWithExtensions(
5801
+ path7.posix.join(path7.posix.dirname(fromFile), spec),
5802
+ "c"
5803
+ );
5804
+ if (relative2) return relative2;
5805
+ for (const base of [
5806
+ path7.posix.join(this.structure.projectRoot, "include"),
5807
+ this.structure.projectRoot
5808
+ ]) {
5809
+ const hit = this.lookupWithExtensions(path7.posix.join(base, spec), "c");
5810
+ if (hit) return hit;
5811
+ }
5812
+ return void 0;
5813
+ }
5814
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
5815
+ resolveRuby(fromFile, spec) {
5816
+ const relative2 = this.lookupWithExtensions(
5817
+ path7.posix.join(path7.posix.dirname(fromFile), spec),
5818
+ "ruby"
5819
+ );
5820
+ if (relative2) return relative2;
5821
+ for (const base of [
5822
+ path7.posix.join(this.structure.projectRoot, "lib"),
5823
+ this.structure.projectRoot
5824
+ ]) {
5825
+ const hit = this.lookupWithExtensions(path7.posix.join(base, spec), "ruby");
5826
+ if (hit) return hit;
5827
+ }
5828
+ return void 0;
5829
+ }
5830
+ };
5831
+
5832
+ // src/codebase-index/import-extractor.ts
5833
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
5834
+ var IMPORT_MAX_PER_FILE = 400;
5835
+ var DOTTED_IMPORT = [
5836
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
5837
+ ];
5838
+ var LANG_IMPORTS = {
5839
+ // Go and Python have real AST extractors; these patterns are the fallback for
5840
+ // machines with no Go toolchain or Python interpreter installed, where the
5841
+ // parser degrades to regex symbols and would otherwise contribute no edges.
5842
+ go: [
5843
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
5844
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
5845
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
5846
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
5847
+ ],
5848
+ py: [
5849
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
5850
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
5851
+ ],
5852
+ rs: [
5853
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
5854
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
5855
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
5856
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
5857
+ ],
5858
+ java: DOTTED_IMPORT,
5859
+ kotlin: DOTTED_IMPORT,
5860
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
5861
+ csharp: [
5862
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
5863
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
5864
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
5865
+ ],
5866
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
5867
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
5868
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
5869
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
5870
+ php: [
5871
+ // `use A\B\C` imports the class C, which is what the index has a symbol
5872
+ // for — the namespace symbol only covers the `A\B` prefix.
5873
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
5874
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
5875
+ ],
5876
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
5877
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
5878
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
5879
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
5880
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
5881
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
5882
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
5883
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
5884
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
5885
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
5886
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
5887
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
5888
+ html: [
5889
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
5890
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
5891
+ ],
5892
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
5893
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
5894
+ };
5895
+ function lastSegment(specifier) {
5896
+ const pathLike = /[/\\]|::/.test(specifier);
5897
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
5898
+ let last = segments[segments.length - 1] ?? specifier;
5899
+ if (last === "*" || last === "_") {
5900
+ last = segments[segments.length - 2] ?? specifier;
5901
+ }
5902
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
5903
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
5904
+ return dotted[dotted.length - 1] ?? last;
5905
+ }
5906
+ function newlineOffsets(content) {
5907
+ const offsets = [];
5908
+ for (let i = 0; i < content.length; i++) {
5909
+ if (content.charCodeAt(i) === 10) offsets.push(i);
5910
+ }
5911
+ return offsets;
5912
+ }
5913
+ function lineAt(offsets, index) {
5914
+ let low = 0;
5915
+ let high = offsets.length;
5916
+ while (low < high) {
5917
+ const mid = low + high >>> 1;
5918
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
5919
+ else high = mid;
5920
+ }
5921
+ return low + 1;
5922
+ }
5923
+ function hasImportPatterns(lang) {
5924
+ return LANG_IMPORTS[lang] !== void 0;
5925
+ }
5926
+ function extractImports(opts) {
5927
+ const patterns = LANG_IMPORTS[opts.lang];
5928
+ if (!patterns || !opts.content) return [];
5929
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
5930
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
5931
+ const refs = [];
5932
+ const seen = /* @__PURE__ */ new Set();
5933
+ const offsets = newlineOffsets(content);
5934
+ for (const pattern of patterns) {
5935
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
5936
+ for (const match of content.matchAll(re)) {
5937
+ if (refs.length >= limit) return refs;
5938
+ const specifier = match[1]?.trim();
5939
+ if (!specifier) continue;
5940
+ const module = specifier;
5941
+ const toName = pattern.name === "full" ? module : lastSegment(module);
5942
+ if (!toName) continue;
5943
+ const key = `${module}\0${toName}`;
5944
+ if (seen.has(key)) continue;
5945
+ seen.add(key);
5946
+ refs.push({
5947
+ fromId: 0,
5948
+ toName,
5949
+ callType: "import",
5950
+ line: lineAt(offsets, match.index ?? 0),
5951
+ lang: opts.lang,
5952
+ module
5953
+ });
5954
+ }
5955
+ }
5956
+ return refs;
5957
+ }
5958
+
4864
5959
  // src/codebase-index/parser-dispatch.ts
4865
5960
  async function parseFileContent(file, content, lang) {
5961
+ const parsed = await dispatch(file, content, lang);
5962
+ return withRelations(parsed, content, lang);
5963
+ }
5964
+ async function dispatch(file, content, lang) {
4866
5965
  switch (lang) {
4867
5966
  case "ts":
4868
5967
  case "tsx":
@@ -4897,6 +5996,13 @@ async function parseFileContent(file, content, lang) {
4897
5996
  }
4898
5997
  }
4899
5998
  }
5999
+ function withRelations(parsed, content, lang) {
6000
+ let refs = parsed.refs ?? [];
6001
+ if (refs.length === 0 && hasImportPatterns(lang)) {
6002
+ refs = extractImports({ content, lang });
6003
+ }
6004
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
6005
+ }
4900
6006
 
4901
6007
  // src/codebase-index/indexer.ts
4902
6008
  var YIELD_EVERY_N = 50;
@@ -4933,7 +6039,7 @@ function normalizeComparablePath(value) {
4933
6039
  }
4934
6040
  function gitOutput(projectRoot, args) {
4935
6041
  return new Promise((resolve4, reject) => {
4936
- execFile2(
6042
+ execFile(
4937
6043
  "git",
4938
6044
  ["-C", projectRoot, ...args],
4939
6045
  {
@@ -5064,13 +6170,40 @@ function assignRefsToSymbols2(refs, symbols) {
5064
6170
  }
5065
6171
  if (!owner && ref.callType === "import") owner = ordered[0];
5066
6172
  if (!owner || owner.id <= 0) continue;
5067
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
6173
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5068
6174
  if (seen.has(key)) continue;
5069
6175
  seen.add(key);
5070
6176
  assigned.push({ ...ref, fromId: owner.id });
5071
6177
  }
5072
6178
  return assigned;
5073
6179
  }
6180
+ async function resolveProjectRelations(store, projectRoot, opts) {
6181
+ if (opts.signal?.aborted) return;
6182
+ try {
6183
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
6184
+ if (indexedFiles.length === 0) return;
6185
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
6186
+ if (opts.signal?.aborted) return;
6187
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
6188
+ const resolver = new ModuleResolver(
6189
+ structure,
6190
+ indexedFiles,
6191
+ store.getNamespaceDeclarations()
6192
+ );
6193
+ const pending2 = store.getUnresolvedImports(opts.onlyFiles);
6194
+ const resolutions = [];
6195
+ for (const entry of pending2) {
6196
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
6197
+ if (toFile && toFile !== entry.fromFile) {
6198
+ resolutions.push({ ...entry, toFile });
6199
+ }
6200
+ }
6201
+ if (opts.signal?.aborted) return;
6202
+ store.applyImportResolutions(resolutions);
6203
+ } catch (err) {
6204
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
6205
+ }
6206
+ }
5074
6207
  async function runIndexer(_ctx, opts) {
5075
6208
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
5076
6209
  try {
@@ -5326,6 +6459,14 @@ async function runIndexerWithStore(store, opts) {
5326
6459
  }
5327
6460
  }
5328
6461
  if (needsFullRefResolution) store.resolveRefs();
6462
+ await resolveProjectRelations(store, projectRoot, {
6463
+ // A watcher run re-resolves only what it touched; a full run (or a contract
6464
+ // bump) re-resolves everything, because a newly indexed file can be the
6465
+ // target of imports written long before it.
6466
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
6467
+ errors,
6468
+ signal
6469
+ });
5329
6470
  store.setMetadata("ref_resolution_version", refResolutionVersion);
5330
6471
  store.setMetadata("relation_graph_version", relationGraphVersion);
5331
6472
  if (!opts.files || filesIndexed >= 50) store.optimize();