@wrongstack/tools 0.299.0 → 0.301.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.
Files changed (53) hide show
  1. package/dist/audit.js +6 -2
  2. package/dist/bash.js +6 -2
  3. package/dist/batch-tool-use.js +3 -1
  4. package/dist/browser/index.js +1 -1
  5. package/dist/builtin.d.ts +3 -2
  6. package/dist/builtin.js +1781 -376
  7. package/dist/codebase-index/bm25.d.ts +7 -1
  8. package/dist/codebase-index/import-extractor.d.ts +39 -0
  9. package/dist/codebase-index/index.js +1418 -267
  10. package/dist/codebase-index/languages.d.ts +24 -0
  11. package/dist/codebase-index/module-resolver.d.ts +78 -0
  12. package/dist/codebase-index/module-roots.d.ts +81 -0
  13. package/dist/codebase-index/parser-output.d.ts +29 -0
  14. package/dist/codebase-index/project-server.js +1402 -249
  15. package/dist/codebase-index/rs-parser.d.ts +22 -0
  16. package/dist/codebase-index/schema.d.ts +24 -1
  17. package/dist/codebase-index/worker.js +1401 -250
  18. package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
  19. package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
  20. package/dist/codebase-index/writer-schema.d.ts +15 -3
  21. package/dist/codebase-index/writer.d.ts +76 -3
  22. package/dist/exec.js +35 -2
  23. package/dist/format.js +6 -2
  24. package/dist/git.js +2 -5
  25. package/dist/glob.js +2 -2
  26. package/dist/grep.js +118 -3
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1925 -415
  29. package/dist/install.js +6 -2
  30. package/dist/json.js +132 -2
  31. package/dist/languages/index.js +6 -2
  32. package/dist/lint.js +6 -2
  33. package/dist/logs.js +81 -0
  34. package/dist/next-steps-tool.d.ts +26 -0
  35. package/dist/outdated.js +6 -2
  36. package/dist/pack.js +1781 -376
  37. package/dist/patch.js +206 -45
  38. package/dist/process-registry.d.ts +6 -0
  39. package/dist/process-registry.js +6 -2
  40. package/dist/ps-slash.js +6 -2
  41. package/dist/read.js +1410 -257
  42. package/dist/replace.js +81 -0
  43. package/dist/skill.js +51 -2
  44. package/dist/test.js +6 -2
  45. package/dist/tool-help.js +2 -2
  46. package/dist/tool-search.js +1 -1
  47. package/dist/tool-tier.d.ts +1 -1
  48. package/dist/tool-tier.js +1786 -397
  49. package/dist/tool-use.js +1 -1
  50. package/dist/tree.js +13 -3
  51. package/dist/typecheck.js +6 -2
  52. package/package.json +3 -3
  53. package/dist/codebase-index/refs-extractor.d.ts +0 -11
package/dist/read.js CHANGED
@@ -27,7 +27,10 @@ function detectLang(file) {
27
27
  if (!ext) return null;
28
28
  return EXT_TO_LANG[ext] ?? null;
29
29
  }
30
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
30
+ function languageFamily(lang) {
31
+ return LANG_FAMILY[lang] ?? "other";
32
+ }
33
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
31
34
  var init_languages = __esm({
32
35
  "src/codebase-index/languages.ts"() {
33
36
  "use strict";
@@ -118,6 +121,52 @@ var init_languages = __esm({
118
121
  procfile: "other",
119
122
  justfile: "other"
120
123
  };
124
+ LANG_FAMILY = {
125
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
126
+ // imports from — and is imported by — plain .ts files.
127
+ ts: "js",
128
+ tsx: "js",
129
+ js: "js",
130
+ jsx: "js",
131
+ vue: "js",
132
+ svelte: "js",
133
+ go: "go",
134
+ py: "py",
135
+ rs: "rs",
136
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
137
+ java: "jvm",
138
+ kotlin: "jvm",
139
+ scala: "jvm",
140
+ csharp: "dotnet",
141
+ // A .h header is consumed by both C and C++ translation units.
142
+ c: "c",
143
+ cpp: "c",
144
+ ruby: "ruby",
145
+ php: "php",
146
+ swift: "swift",
147
+ dart: "dart",
148
+ elixir: "elixir",
149
+ haskell: "haskell",
150
+ zig: "zig",
151
+ lua: "lua",
152
+ r: "r",
153
+ shell: "shell",
154
+ sql: "sql",
155
+ json: "data",
156
+ yaml: "data",
157
+ toml: "data",
158
+ html: "web",
159
+ css: "web",
160
+ proto: "proto",
161
+ graphql: "graphql",
162
+ md: "other",
163
+ other: "other"
164
+ };
165
+ LANG_FAMILY_ENTRIES = Object.freeze(
166
+ Object.entries(LANG_FAMILY).map(
167
+ ([lang, family]) => Object.freeze([lang, family])
168
+ )
169
+ );
121
170
  }
122
171
  });
123
172
 
@@ -282,7 +331,7 @@ function getTypeName(name) {
282
331
  function deduplicateRefs(refs) {
283
332
  const seen = /* @__PURE__ */ new Set();
284
333
  return refs.filter((r) => {
285
- const key = `${r.toName}:${r.callType}:${r.line}`;
334
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
286
335
  if (seen.has(key)) return false;
287
336
  seen.add(key);
288
337
  return true;
@@ -292,10 +341,16 @@ function getImportSpecifierName(spec) {
292
341
  return spec.propertyName?.text ?? spec.name.text;
293
342
  }
294
343
  function emitImportSpecifierRefs(node, refs, lineNum) {
344
+ const module = moduleSpecifierOf(node.moduleSpecifier);
295
345
  const clause = node.importClause;
296
- if (!clause) return;
346
+ if (!clause) {
347
+ if (module) {
348
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
349
+ }
350
+ return;
351
+ }
297
352
  if (clause.name) {
298
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
353
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
299
354
  }
300
355
  const bindings = clause.namedBindings;
301
356
  if (!bindings) return;
@@ -305,26 +360,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
305
360
  fromId: 0,
306
361
  toName: getImportSpecifierName(element),
307
362
  callType: "import",
308
- line: lineNum
363
+ line: lineNum,
364
+ module
309
365
  });
310
366
  }
311
367
  } else if (ts.isNamespaceImport(bindings)) {
312
- refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
368
+ refs.push({
369
+ fromId: 0,
370
+ toName: bindings.name.text,
371
+ callType: "import",
372
+ line: lineNum,
373
+ module
374
+ });
313
375
  }
314
376
  }
377
+ function moduleSpecifierOf(node) {
378
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
379
+ }
315
380
  function emitExportSpecifierRefs(node, refs, lineNum) {
381
+ const module = moduleSpecifierOf(node.moduleSpecifier);
316
382
  const clause = node.exportClause;
317
383
  if (clause && ts.isNamespaceExport(clause)) {
318
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
384
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
319
385
  return;
320
386
  }
321
387
  if (clause && ts.isNamedExports(clause)) {
322
388
  for (const element of clause.elements) {
323
389
  const originalName = element.propertyName?.text ?? element.name.text;
324
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
390
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
325
391
  }
326
392
  return;
327
393
  }
394
+ if (module) {
395
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
396
+ }
328
397
  }
329
398
  var ts, tsLoad, kindMapCache;
330
399
  var init_ts_parser = __esm({
@@ -337,21 +406,21 @@ var init_ts_parser = __esm({
337
406
  });
338
407
 
339
408
  // src/_win32-resolve.ts
340
- import * as fs2 from "node:fs";
341
- import * as path4 from "node:path";
409
+ import * as fs3 from "node:fs";
410
+ import * as path6 from "node:path";
342
411
  function resolveWin32Command(cmd) {
343
412
  if (process.platform !== "win32") return cmd;
344
- if (cmd.includes("/") || cmd.includes("\\") || path4.extname(cmd.replace(/\//g, "\\"))) {
413
+ if (cmd.includes("/") || cmd.includes("\\") || path6.extname(cmd.replace(/\//g, "\\"))) {
345
414
  return cmd;
346
415
  }
347
416
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
348
- const pathDirs = (process.env["PATH"] ?? "").split(path4.delimiter);
417
+ const pathDirs = (process.env["PATH"] ?? "").split(path6.delimiter);
349
418
  for (const dir of pathDirs) {
350
- const base = path4.join(dir, cmd);
419
+ const base = path6.join(dir, cmd);
351
420
  for (const ext of pathext) {
352
421
  const full = `${base}${ext}`;
353
422
  try {
354
- fs2.accessSync(full, fs2.constants.X_OK);
423
+ fs3.accessSync(full, fs3.constants.X_OK);
355
424
  return full;
356
425
  } catch {
357
426
  }
@@ -365,6 +434,82 @@ var init_win32_resolve = __esm({
365
434
  }
366
435
  });
367
436
 
437
+ // src/codebase-index/parser-output.ts
438
+ function coerceSymbols(value) {
439
+ if (!Array.isArray(value)) return [];
440
+ return value.flatMap((entry) => {
441
+ const candidate = entry;
442
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
443
+ return [
444
+ {
445
+ name: candidate.name,
446
+ kind: candidate.kind,
447
+ line: typeof candidate.line === "number" ? candidate.line : 1,
448
+ col: typeof candidate.col === "number" ? candidate.col : 0,
449
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
450
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
451
+ }
452
+ ];
453
+ });
454
+ }
455
+ function coerceRefs(value, lang) {
456
+ if (!Array.isArray(value)) return [];
457
+ return value.flatMap((entry) => {
458
+ const candidate = entry;
459
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
460
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
461
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
462
+ return [
463
+ {
464
+ fromId: 0,
465
+ toName: candidate.toName,
466
+ callType: candidate.callType,
467
+ line: typeof candidate.line === "number" ? candidate.line : 1,
468
+ lang,
469
+ module
470
+ }
471
+ ];
472
+ });
473
+ }
474
+ function parseParserOutput(stdout, lang) {
475
+ const trimmed = stdout.trim();
476
+ if (!trimmed) return { symbols: [], refs: [] };
477
+ let parsed;
478
+ try {
479
+ parsed = JSON.parse(trimmed);
480
+ } catch {
481
+ return { symbols: [], refs: [] };
482
+ }
483
+ if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
484
+ const record = parsed;
485
+ return {
486
+ symbols: coerceSymbols(record.symbols),
487
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
488
+ };
489
+ }
490
+ function dedupeRefs(refs) {
491
+ const seen = /* @__PURE__ */ new Set();
492
+ return refs.filter((ref) => {
493
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
494
+ if (seen.has(key)) return false;
495
+ seen.add(key);
496
+ return true;
497
+ });
498
+ }
499
+ var CALL_TYPES;
500
+ var init_parser_output = __esm({
501
+ "src/codebase-index/parser-output.ts"() {
502
+ "use strict";
503
+ CALL_TYPES = /* @__PURE__ */ new Set([
504
+ "call",
505
+ "type_ref",
506
+ "inherit",
507
+ "implement",
508
+ "import"
509
+ ]);
510
+ }
511
+ });
512
+
368
513
  // src/codebase-index/spawn-gate.ts
369
514
  function withSpawnGate(fn) {
370
515
  const run = chain.then(fn, fn);
@@ -390,8 +535,8 @@ __export(go_parser_exports, {
390
535
  });
391
536
  import { spawn } from "node:child_process";
392
537
  import * as os from "node:os";
393
- import * as path5 from "node:path";
394
- import * as fs3 from "node:fs/promises";
538
+ import * as path7 from "node:path";
539
+ import * as fs4 from "node:fs/promises";
395
540
  async function parseSymbols2(opts) {
396
541
  const { file, content, lang } = opts;
397
542
  try {
@@ -399,7 +544,8 @@ async function parseSymbols2(opts) {
399
544
  if (parsed.symbols.length > 0) {
400
545
  return parsed;
401
546
  }
402
- return fallbackParse(file, content, lang);
547
+ const fallback = fallbackParse(file, content, lang);
548
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
403
549
  } catch {
404
550
  return fallbackParse(file, content, lang);
405
551
  }
@@ -463,9 +609,9 @@ async function syncGoParse(filePath, content, lang) {
463
609
  try {
464
610
  let scriptPath = _cachedGoScriptPath;
465
611
  if (!scriptPath) {
466
- const tmpDir = await fs3.mkdtemp(path5.join(os.tmpdir(), "ws-go-parse-"));
467
- scriptPath = path5.join(tmpDir, "parse.go");
468
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
612
+ const tmpDir = await fs4.mkdtemp(path7.join(os.tmpdir(), "ws-go-parse-"));
613
+ scriptPath = path7.join(tmpDir, "parse.go");
614
+ await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
469
615
  _cachedGoScriptPath = scriptPath;
470
616
  }
471
617
  const goBinary = resolveWin32Command("go");
@@ -507,8 +653,8 @@ async function syncGoParse(filePath, content, lang) {
507
653
  if (code !== 0 || !stdout.trim()) {
508
654
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
509
655
  }
510
- const raw = JSON.parse(stdout.trim());
511
- const symbols = raw.map((s) => ({
656
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
657
+ const symbols = rawSymbols.map((s) => ({
512
658
  id: 0,
513
659
  lang,
514
660
  kind: s.kind,
@@ -521,7 +667,7 @@ async function syncGoParse(filePath, content, lang) {
521
667
  scope: s.scope ?? "",
522
668
  text: `${s.name} ${s.signature ?? ""}`.trim()
523
669
  }));
524
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
670
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
525
671
  } catch {
526
672
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
527
673
  }
@@ -531,6 +677,7 @@ var init_go_parser = __esm({
531
677
  "src/codebase-index/go-parser.ts"() {
532
678
  "use strict";
533
679
  init_win32_resolve();
680
+ init_parser_output();
534
681
  init_spawn_gate();
535
682
  init_languages();
536
683
  GO_PARSE_SCRIPT = `
@@ -544,6 +691,7 @@ import (
544
691
  "go/token"
545
692
  "io"
546
693
  "os"
694
+ "strconv"
547
695
  "strings"
548
696
  )
549
697
 
@@ -556,16 +704,34 @@ type Sym struct {
556
704
  Scope string \`json:"scope"\`
557
705
  }
558
706
 
707
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
708
+ // yields both. Module is the import path for CallType "import", else empty.
709
+ type Ref struct {
710
+ ToName string \`json:"toName"\`
711
+ CallType string \`json:"callType"\`
712
+ Line int \`json:"line"\`
713
+ Module string \`json:"module"\`
714
+ }
715
+
716
+ type Result struct {
717
+ Symbols []Sym \`json:"symbols"\`
718
+ Refs []Ref \`json:"refs"\`
719
+ }
720
+
721
+ func emptyResult() string {
722
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
723
+ }
724
+
559
725
  func main() {
560
726
  src, err := io.ReadAll(os.Stdin)
561
727
  if err != nil {
562
- fmt.Print("[]")
728
+ fmt.Print(emptyResult())
563
729
  return
564
730
  }
565
731
  fset := token.NewFileSet()
566
732
  node, err := parser.ParseFile(fset, "src.go", src, 0)
567
733
  if err != nil {
568
- fmt.Print("[]")
734
+ fmt.Print(emptyResult())
569
735
  return
570
736
  }
571
737
 
@@ -629,9 +795,43 @@ func main() {
629
795
  }
630
796
  }
631
797
 
632
- data, err := json.Marshal(syms)
798
+ refs := []Ref{}
799
+ ast.Inspect(node, func(n ast.Node) bool {
800
+ switch expr := n.(type) {
801
+ case *ast.CallExpr:
802
+ line := fset.Position(expr.Pos()).Line
803
+ switch fun := expr.Fun.(type) {
804
+ case *ast.Ident:
805
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
806
+ case *ast.SelectorExpr:
807
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
808
+ // declared symbol name, so it resolves the same way the TypeScript
809
+ // and Python extractors' call refs do.
810
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
811
+ }
812
+ case *ast.ImportSpec:
813
+ if expr.Path != nil {
814
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
815
+ line := fset.Position(expr.Pos()).Line
816
+ // A Go import names a package, not a symbol; the package's
817
+ // last path segment is the name it is referenced by.
818
+ name := importPath
819
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
820
+ name = importPath[idx+1:]
821
+ }
822
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
823
+ }
824
+ }
825
+ }
826
+ return true
827
+ })
828
+
829
+ if syms == nil {
830
+ syms = []Sym{}
831
+ }
832
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
633
833
  if err != nil {
634
- fmt.Print("[]")
834
+ fmt.Print(emptyResult())
635
835
  return
636
836
  }
637
837
  fmt.Print(string(data))
@@ -792,16 +992,23 @@ function looksBinary(content) {
792
992
  }
793
993
  return bad / sample.length > 0.1;
794
994
  }
795
- function lineColAt(content, index) {
796
- let line = 1;
797
- let lastNl = -1;
798
- for (let i = 0; i < index && i < content.length; i++) {
799
- if (content.charCodeAt(i) === 10) {
800
- line++;
801
- lastNl = i;
802
- }
995
+ function newlineOffsets2(content) {
996
+ const offsets = [];
997
+ for (let i = 0; i < content.length; i++) {
998
+ if (content.charCodeAt(i) === 10) offsets.push(i);
999
+ }
1000
+ return offsets;
1001
+ }
1002
+ function lineColAt(offsets, index) {
1003
+ let low = 0;
1004
+ let high = offsets.length;
1005
+ while (low < high) {
1006
+ const mid = low + high >>> 1;
1007
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
1008
+ else high = mid;
803
1009
  }
804
- return { line, col: index - lastNl };
1010
+ const lastNl = low > 0 ? offsets[low - 1] : -1;
1011
+ return { line: low + 1, col: index - lastNl };
805
1012
  }
806
1013
  function parseGeneric(opts) {
807
1014
  const { file, lang } = opts;
@@ -814,6 +1021,7 @@ function parseGeneric(opts) {
814
1021
  const patterns = patternsFor(lang);
815
1022
  const symbols = [];
816
1023
  const seen = /* @__PURE__ */ new Set();
1024
+ const nlOffsets = newlineOffsets2(content);
817
1025
  for (const pattern of patterns) {
818
1026
  const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
819
1027
  re.lastIndex = 0;
@@ -827,7 +1035,7 @@ function parseGeneric(opts) {
827
1035
  if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
828
1036
  continue;
829
1037
  }
830
- const { line, col } = lineColAt(content, match.index);
1038
+ const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
831
1039
  const key = `${name}\0${line}\0${pattern.kind}`;
832
1040
  if (seen.has(key)) continue;
833
1041
  seen.add(key);
@@ -983,9 +1191,13 @@ var init_generic_parser = __esm({
983
1191
  ],
984
1192
  elixir: [
985
1193
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
986
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
1194
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
1195
+ // against this symbol, and a `Foo`-only capture never matches it.
1196
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
987
1197
  ],
988
1198
  haskell: [
1199
+ // Target of `import Data.List`.
1200
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
989
1201
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
990
1202
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
991
1203
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -1076,9 +1288,9 @@ __export(py_parser_exports, {
1076
1288
  parseSymbols: () => parseSymbols4
1077
1289
  });
1078
1290
  import { spawn as spawn2 } from "node:child_process";
1079
- import * as fs4 from "node:fs/promises";
1291
+ import * as fs5 from "node:fs/promises";
1080
1292
  import * as os2 from "node:os";
1081
- import * as path6 from "node:path";
1293
+ import * as path8 from "node:path";
1082
1294
  async function parseSymbols4(opts) {
1083
1295
  const { file, content, lang } = opts;
1084
1296
  try {
@@ -1156,10 +1368,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1156
1368
  async function syncPyParse(filePath, content, lang) {
1157
1369
  try {
1158
1370
  if (!_cachedScriptPath) {
1159
- const tmpDir = path6.join(os2.tmpdir(), "ws-py-parse");
1160
- await fs4.mkdir(tmpDir, { recursive: true });
1161
- _cachedScriptPath = path6.join(tmpDir, "parse.py");
1162
- await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1371
+ const tmpDir = path8.join(os2.tmpdir(), "ws-py-parse");
1372
+ await fs5.mkdir(tmpDir, { recursive: true });
1373
+ _cachedScriptPath = path8.join(tmpDir, "parse.py");
1374
+ await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1163
1375
  }
1164
1376
  cachedPyBinary ??= resolvePython();
1165
1377
  const pyBinary = await cachedPyBinary;
@@ -1173,7 +1385,7 @@ async function syncPyParse(filePath, content, lang) {
1173
1385
  if (code !== 0 || !stdout.trim()) {
1174
1386
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1175
1387
  }
1176
- const raw = JSON.parse(stdout.trim());
1388
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
1177
1389
  const symbols = raw.map((s) => ({
1178
1390
  id: 0,
1179
1391
  lang,
@@ -1187,7 +1399,7 @@ async function syncPyParse(filePath, content, lang) {
1187
1399
  scope: s.scope ?? "",
1188
1400
  text: `${s.name} ${s.signature ?? ""}`.trim()
1189
1401
  }));
1190
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1402
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
1191
1403
  } catch {
1192
1404
  return null;
1193
1405
  }
@@ -1198,6 +1410,7 @@ var init_py_parser = __esm({
1198
1410
  "use strict";
1199
1411
  init_win32_resolve();
1200
1412
  init_generic_parser();
1413
+ init_parser_output();
1201
1414
  init_spawn_gate();
1202
1415
  init_languages();
1203
1416
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -1259,7 +1472,18 @@ class Sym:
1259
1472
  def is_private(name):
1260
1473
  return name.startswith("__") and not name.endswith("__")
1261
1474
 
1475
+ def leaf_name(node):
1476
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
1477
+ # TypeScript and Go extractors record call refs, so resolution behaves the
1478
+ # same across languages.
1479
+ if isinstance(node, ast.Attribute):
1480
+ return node.attr
1481
+ if isinstance(node, ast.Name):
1482
+ return node.id
1483
+ return get_name(node).split(".")[-1]
1484
+
1262
1485
  syms = []
1486
+ refs = []
1263
1487
  errors = []
1264
1488
 
1265
1489
  try:
@@ -1267,7 +1491,7 @@ try:
1267
1491
  tree = ast.parse(source, filename=sys.argv[1])
1268
1492
  except Exception as e:
1269
1493
  errors.append(str(e))
1270
- print("[]")
1494
+ print(json.dumps({"symbols": [], "refs": []}))
1271
1495
  sys.exit(0)
1272
1496
 
1273
1497
  # Module-level scope
@@ -1401,7 +1625,42 @@ class ModuleVisitor(ast.NodeVisitor):
1401
1625
  visitor = ModuleVisitor()
1402
1626
  visitor.visit(tree)
1403
1627
 
1404
- print(json.dumps([s.to_dict() for s in syms]))
1628
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
1629
+ # into function bodies (it would index locals as symbols), but that is exactly
1630
+ # where the calls are.
1631
+ for node in ast.walk(tree):
1632
+ if isinstance(node, ast.Call):
1633
+ name = leaf_name(node.func)
1634
+ if name:
1635
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
1636
+ elif isinstance(node, ast.Import):
1637
+ for alias in node.names:
1638
+ refs.append({
1639
+ "toName": alias.name.split(".")[-1],
1640
+ "callType": "import",
1641
+ "line": node.lineno,
1642
+ "module": alias.name,
1643
+ })
1644
+ elif isinstance(node, ast.ImportFrom):
1645
+ # PEP 328: node.level is the number of leading dots. Preserving them is
1646
+ # what lets the resolver walk up from the importing file's package \u2014
1647
+ # dropping them made \`from .foo import X\` indistinguishable from an
1648
+ # absolute \`foo\`.
1649
+ module = ("." * (node.level or 0)) + (node.module or "")
1650
+ for alias in node.names:
1651
+ refs.append({
1652
+ "toName": alias.name,
1653
+ "callType": "import",
1654
+ "line": node.lineno,
1655
+ "module": module,
1656
+ })
1657
+ elif isinstance(node, ast.ClassDef):
1658
+ for base in node.bases:
1659
+ name = leaf_name(base)
1660
+ if name:
1661
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1662
+
1663
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1405
1664
  `;
1406
1665
  _cachedScriptPath = null;
1407
1666
  }
@@ -1414,107 +1673,10 @@ __export(rs_parser_exports, {
1414
1673
  parseSymbols: () => parseSymbols5
1415
1674
  });
1416
1675
  import { expectDefined } from "@wrongstack/core/utils";
1417
- import { execFile, spawn as spawn3 } from "node:child_process";
1418
- import * as fs5 from "node:fs/promises";
1419
- import * as path7 from "node:path";
1420
1676
  async function parseSymbols5(opts) {
1421
1677
  const { file, content, lang } = opts;
1422
- const nativeAvailable = await checkNativeParser();
1423
- if (nativeAvailable) {
1424
- const result = await withSpawnGate(() => tryNativeParse(file, content));
1425
- if (result) return result;
1426
- }
1427
1678
  return regexParse({ file, content, lang });
1428
1679
  }
1429
- function probe(command, args) {
1430
- return new Promise((resolve4, reject) => {
1431
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
1432
- if (error) reject(error);
1433
- else resolve4();
1434
- });
1435
- });
1436
- }
1437
- function checkNativeParser() {
1438
- nativeParserAvailability ??= (async () => {
1439
- try {
1440
- await probe("rustc", ["--version"]);
1441
- const toolsDir = path7.join(process.cwd(), "tools");
1442
- await probe(
1443
- "cargo",
1444
- [
1445
- "metadata",
1446
- "--no-deps",
1447
- "--format-version",
1448
- "1",
1449
- "--manifest-path",
1450
- path7.join(toolsDir, "Cargo.toml")
1451
- ]
1452
- );
1453
- return true;
1454
- } catch {
1455
- return false;
1456
- }
1457
- })();
1458
- return nativeParserAvailability;
1459
- }
1460
- async function tryNativeParse(file, content) {
1461
- try {
1462
- const toolsDir = path7.join(process.cwd(), "tools");
1463
- const crateDir = path7.join(toolsDir, "syn-parser");
1464
- const tmpFile = path7.join(crateDir, "src", "input.rs");
1465
- await fs5.writeFile(tmpFile, content, "utf8");
1466
- const cargoBinary = resolveWin32Command("cargo");
1467
- const result = await new Promise(
1468
- (resolve4, reject) => {
1469
- let settled = false;
1470
- const proc = spawn3(
1471
- cargoBinary,
1472
- ["run", "--manifest-path", path7.join(toolsDir, "Cargo.toml")],
1473
- {
1474
- cwd: process.cwd(),
1475
- stdio: ["pipe", "pipe", "pipe"],
1476
- windowsHide: true
1477
- }
1478
- );
1479
- proc.on("error", (err) => {
1480
- if (settled) return;
1481
- settled = true;
1482
- reject(err);
1483
- });
1484
- let stdout2 = "";
1485
- proc.stdout?.on("data", (chunk) => {
1486
- stdout2 += chunk.toString();
1487
- });
1488
- proc.stderr?.resume();
1489
- const timer = setTimeout(() => {
1490
- if (settled) return;
1491
- settled = true;
1492
- proc.kill("SIGKILL");
1493
- reject(new Error("timeout"));
1494
- }, 15e3);
1495
- timer.unref?.();
1496
- proc.on("close", (c) => {
1497
- if (settled) return;
1498
- settled = true;
1499
- clearTimeout(timer);
1500
- resolve4({ code: c, stdout: stdout2 });
1501
- });
1502
- }
1503
- );
1504
- const { code, stdout } = result;
1505
- if (code === 0 && stdout.trim()) {
1506
- const symbols = JSON.parse(stdout.trim());
1507
- return {
1508
- file,
1509
- lang: "rs",
1510
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
1511
- mtimeMs: Date.now()
1512
- };
1513
- }
1514
- } catch {
1515
- }
1516
- return null;
1517
- }
1518
1680
  function regexParse(opts) {
1519
1681
  const { file, content, lang } = opts;
1520
1682
  const symbols = [];
@@ -1570,12 +1732,10 @@ function regexParse(opts) {
1570
1732
  });
1571
1733
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
1572
1734
  }
1573
- var nativeParserAvailability, RS_PATTERNS;
1735
+ var RS_PATTERNS;
1574
1736
  var init_rs_parser = __esm({
1575
1737
  "src/codebase-index/rs-parser.ts"() {
1576
1738
  "use strict";
1577
- init_win32_resolve();
1578
- init_spawn_gate();
1579
1739
  init_languages();
1580
1740
  RS_PATTERNS = [
1581
1741
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -1598,7 +1758,7 @@ __export(json_parser_exports, {
1598
1758
  parseSymbols: () => parseSymbols6
1599
1759
  });
1600
1760
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1601
- import * as path8 from "node:path";
1761
+ import * as path9 from "node:path";
1602
1762
  function parseSymbols6(opts) {
1603
1763
  const { file, content, lang } = opts;
1604
1764
  try {
@@ -1610,7 +1770,7 @@ function parseSymbols6(opts) {
1610
1770
  function regexParse2(opts) {
1611
1771
  const { file, content, lang } = opts;
1612
1772
  const symbols = [];
1613
- const basename6 = path8.basename(file).toLowerCase();
1773
+ const basename6 = path9.basename(file).toLowerCase();
1614
1774
  const isPackageJson = basename6 === "package.json";
1615
1775
  const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
1616
1776
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1636,11 +1796,11 @@ function regexParse2(opts) {
1636
1796
  const line = lineFromOffset(offset);
1637
1797
  symbols.push(
1638
1798
  makeSymbol({
1639
- name: path8.basename(file),
1799
+ name: path9.basename(file),
1640
1800
  kind: "object",
1641
1801
  line,
1642
1802
  col: 0,
1643
- signature: `"${path8.basename(file)}" = { ... }`,
1803
+ signature: `"${path9.basename(file)}" = { ... }`,
1644
1804
  file,
1645
1805
  lang
1646
1806
  })
@@ -2006,18 +2166,18 @@ async function resolveRealInsideRoot(absPath, ctx) {
2006
2166
  const realRoots = await Promise.all(
2007
2167
  allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
2008
2168
  );
2009
- let probe2 = absPath;
2169
+ let probe = absPath;
2010
2170
  const pendingTail = [];
2011
2171
  for (; ; ) {
2012
2172
  let real;
2013
2173
  try {
2014
- real = await fsp.realpath(probe2);
2174
+ real = await fsp.realpath(probe);
2015
2175
  } catch (err) {
2016
2176
  if (err.code === "ENOENT") {
2017
- const parent = path.dirname(probe2);
2018
- if (parent === probe2) return absPath;
2019
- pendingTail.unshift(path.basename(probe2));
2020
- probe2 = parent;
2177
+ const parent = path.dirname(probe);
2178
+ if (parent === probe) return absPath;
2179
+ pendingTail.unshift(path.basename(probe));
2180
+ probe = parent;
2021
2181
  continue;
2022
2182
  }
2023
2183
  throw err;
@@ -2126,7 +2286,7 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
2126
2286
 
2127
2287
  // src/codebase-index/indexer.ts
2128
2288
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
2129
- import { execFile as execFile2 } from "node:child_process";
2289
+ import { execFile } from "node:child_process";
2130
2290
  import * as fs8 from "node:fs/promises";
2131
2291
  import { availableParallelism } from "node:os";
2132
2292
  import * as path12 from "node:path";
@@ -2194,8 +2354,738 @@ async function loadGitignoreMatcher(projectRoot) {
2194
2354
  // src/codebase-index/indexer.ts
2195
2355
  init_languages();
2196
2356
 
2357
+ // src/codebase-index/module-resolver.ts
2358
+ init_languages();
2359
+ import * as path5 from "node:path";
2360
+
2361
+ // src/codebase-index/module-roots.ts
2362
+ init_languages();
2363
+ import * as fs2 from "node:fs/promises";
2364
+ import * as path4 from "node:path";
2365
+ function toPortablePath(file) {
2366
+ return file.replace(/\\/g, "/");
2367
+ }
2368
+ async function readTextIfPresent(file) {
2369
+ try {
2370
+ return await fs2.readFile(file, "utf8");
2371
+ } catch {
2372
+ return void 0;
2373
+ }
2374
+ }
2375
+ function parsePackageJsonName(source) {
2376
+ try {
2377
+ const parsed = JSON.parse(source);
2378
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
2379
+ } catch {
2380
+ return void 0;
2381
+ }
2382
+ }
2383
+ function parseGoModulePath(source) {
2384
+ for (const rawLine of source.split(/\r?\n/)) {
2385
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
2386
+ const match = /^module\s+(\S+)/.exec(line);
2387
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
2388
+ }
2389
+ return void 0;
2390
+ }
2391
+ function parseTomlTableName(source, tables) {
2392
+ let current = "";
2393
+ for (const rawLine of source.split(/\r?\n/)) {
2394
+ const line = rawLine.replace(/#.*$/, "").trim();
2395
+ if (line.startsWith("[[")) {
2396
+ current = "\0";
2397
+ continue;
2398
+ }
2399
+ const table = /^\[([^\]]+)\]$/.exec(line);
2400
+ if (table?.[1]) {
2401
+ current = table[1].trim();
2402
+ continue;
2403
+ }
2404
+ if (!tables.includes(current)) continue;
2405
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
2406
+ if (match?.[1]) return match[1];
2407
+ }
2408
+ return void 0;
2409
+ }
2410
+ function parsePomArtifactId(source) {
2411
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
2412
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
2413
+ }
2414
+ var LANGS_BY_KIND = {
2415
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
2416
+ cargo: ["rs"],
2417
+ go: ["go"],
2418
+ python: ["py"],
2419
+ maven: ["java", "kotlin", "scala"],
2420
+ gradle: ["java", "kotlin", "scala"],
2421
+ dotnet: ["csharp"]
2422
+ };
2423
+ function ancestorsOf(dir, stopAt) {
2424
+ const out = [];
2425
+ let current = dir;
2426
+ for (; ; ) {
2427
+ out.push(current);
2428
+ if (current === stopAt || current.length <= stopAt.length) break;
2429
+ const parent = path4.posix.dirname(current);
2430
+ if (parent === current) break;
2431
+ current = parent;
2432
+ }
2433
+ return out;
2434
+ }
2435
+ var MARKER_PROBES = [
2436
+ {
2437
+ kind: "npm",
2438
+ file: "package.json",
2439
+ build: (dir, source) => {
2440
+ const name = parsePackageJsonName(source) ?? path4.posix.basename(dir);
2441
+ return { name, importPath: name, sourceRoots: [dir] };
2442
+ }
2443
+ },
2444
+ {
2445
+ kind: "cargo",
2446
+ file: "Cargo.toml",
2447
+ build: (dir, source) => {
2448
+ const name = parseTomlTableName(source, ["package"]);
2449
+ if (!name) return void 0;
2450
+ return {
2451
+ name: `crate:${name}`,
2452
+ // Rust paths use underscores where crate names often use dashes.
2453
+ importPath: name.replace(/-/g, "_"),
2454
+ sourceRoots: [path4.posix.join(dir, "src")]
2455
+ };
2456
+ }
2457
+ },
2458
+ {
2459
+ kind: "go",
2460
+ file: "go.mod",
2461
+ build: (dir, source) => {
2462
+ const modulePath = parseGoModulePath(source);
2463
+ if (!modulePath) return void 0;
2464
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
2465
+ }
2466
+ },
2467
+ {
2468
+ kind: "python",
2469
+ file: "pyproject.toml",
2470
+ build: (dir, source) => {
2471
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path4.posix.basename(dir);
2472
+ return {
2473
+ name: `py:${name}`,
2474
+ importPath: void 0,
2475
+ // `src/` layout is the packaging-guide default; the root itself covers
2476
+ // the flat layout. Both are probed, missing ones simply never match.
2477
+ sourceRoots: [path4.posix.join(dir, "src"), dir]
2478
+ };
2479
+ }
2480
+ },
2481
+ {
2482
+ kind: "python",
2483
+ file: "setup.py",
2484
+ build: (dir) => ({
2485
+ name: `py:${path4.posix.basename(dir)}`,
2486
+ importPath: void 0,
2487
+ sourceRoots: [path4.posix.join(dir, "src"), dir]
2488
+ })
2489
+ },
2490
+ {
2491
+ kind: "maven",
2492
+ file: "pom.xml",
2493
+ build: (dir, source) => {
2494
+ const artifactId = parsePomArtifactId(source) ?? path4.posix.basename(dir);
2495
+ return {
2496
+ name: `mvn:${artifactId}`,
2497
+ importPath: void 0,
2498
+ sourceRoots: [
2499
+ path4.posix.join(dir, "src/main/java"),
2500
+ path4.posix.join(dir, "src/main/kotlin"),
2501
+ path4.posix.join(dir, "src/main/scala"),
2502
+ path4.posix.join(dir, "src/test/java")
2503
+ ]
2504
+ };
2505
+ }
2506
+ },
2507
+ {
2508
+ kind: "gradle",
2509
+ file: "build.gradle",
2510
+ build: (dir) => buildGradleRoot(dir)
2511
+ },
2512
+ {
2513
+ kind: "gradle",
2514
+ file: "build.gradle.kts",
2515
+ build: (dir) => buildGradleRoot(dir)
2516
+ }
2517
+ ];
2518
+ function buildGradleRoot(dir) {
2519
+ return {
2520
+ name: `gradle:${path4.posix.basename(dir)}`,
2521
+ importPath: void 0,
2522
+ sourceRoots: [
2523
+ path4.posix.join(dir, "src/main/java"),
2524
+ path4.posix.join(dir, "src/main/kotlin"),
2525
+ path4.posix.join(dir, "src/main/scala")
2526
+ ]
2527
+ };
2528
+ }
2529
+ async function probeDotnetRoot(dir) {
2530
+ let entries;
2531
+ try {
2532
+ entries = await fs2.readdir(dir);
2533
+ } catch {
2534
+ return void 0;
2535
+ }
2536
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
2537
+ if (!project) return void 0;
2538
+ const name = project.slice(0, -".csproj".length);
2539
+ return {
2540
+ dir,
2541
+ kind: "dotnet",
2542
+ name: `csproj:${name}`,
2543
+ importPath: void 0,
2544
+ sourceRoots: [dir]
2545
+ };
2546
+ }
2547
+ async function detectModuleRoots(projectRoot, files) {
2548
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
2549
+ const langsByDir = /* @__PURE__ */ new Map();
2550
+ for (const file of files) {
2551
+ const portable = toPortablePath(file);
2552
+ const lang = detectLang(portable);
2553
+ if (!lang) continue;
2554
+ const dir = path4.posix.dirname(portable);
2555
+ let langs = langsByDir.get(dir);
2556
+ if (!langs) {
2557
+ langs = /* @__PURE__ */ new Set();
2558
+ langsByDir.set(dir, langs);
2559
+ }
2560
+ langs.add(lang);
2561
+ }
2562
+ const candidates = /* @__PURE__ */ new Map();
2563
+ for (const [dir, langs] of langsByDir) {
2564
+ for (const ancestor of ancestorsOf(dir, root)) {
2565
+ let merged = candidates.get(ancestor);
2566
+ if (!merged) {
2567
+ merged = /* @__PURE__ */ new Set();
2568
+ candidates.set(ancestor, merged);
2569
+ }
2570
+ for (const lang of langs) merged.add(lang);
2571
+ }
2572
+ }
2573
+ const roots = [];
2574
+ await Promise.all(
2575
+ [...candidates].map(async ([dir, langs]) => {
2576
+ for (const probe of MARKER_PROBES) {
2577
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
2578
+ const source = await readTextIfPresent(path4.posix.join(dir, probe.file));
2579
+ if (source === void 0) continue;
2580
+ const built = probe.build(dir, source);
2581
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
2582
+ }
2583
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
2584
+ const dotnet = await probeDotnetRoot(dir);
2585
+ if (dotnet) roots.push(dotnet);
2586
+ }
2587
+ })
2588
+ );
2589
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
2590
+ return { projectRoot: root, roots };
2591
+ }
2592
+ function findOwningRoot(structure, file, kinds) {
2593
+ const portable = toPortablePath(file);
2594
+ for (const root of structure.roots) {
2595
+ if (kinds && !kinds.includes(root.kind)) continue;
2596
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
2597
+ }
2598
+ return void 0;
2599
+ }
2600
+ function derivePackageFromLayout(filePath) {
2601
+ const portable = toPortablePath(filePath);
2602
+ const packagesIdx = portable.indexOf("/packages/");
2603
+ if (packagesIdx !== -1) {
2604
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
2605
+ if (segment) return `@wrongstack/${segment}`;
2606
+ }
2607
+ const appsIdx = portable.indexOf("/apps/");
2608
+ if (appsIdx !== -1) {
2609
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
2610
+ if (segment) return `app:${segment}`;
2611
+ }
2612
+ return void 0;
2613
+ }
2614
+ function pythonPackageLabel(structure, file, initDirs) {
2615
+ const portable = toPortablePath(file);
2616
+ const dir = path4.posix.dirname(portable);
2617
+ if (!initDirs.has(dir)) return void 0;
2618
+ const segments = [];
2619
+ let current = dir;
2620
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
2621
+ segments.unshift(path4.posix.basename(current));
2622
+ current = path4.posix.dirname(current);
2623
+ }
2624
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
2625
+ }
2626
+ function assignPackageLabels(structure, files) {
2627
+ const initDirs = /* @__PURE__ */ new Set();
2628
+ for (const file of files) {
2629
+ const portable = toPortablePath(file);
2630
+ if (path4.posix.basename(portable) === "__init__.py") {
2631
+ initDirs.add(path4.posix.dirname(portable));
2632
+ }
2633
+ }
2634
+ const labels = /* @__PURE__ */ new Map();
2635
+ for (const file of files) {
2636
+ const portable = toPortablePath(file);
2637
+ const lang = detectLang(portable);
2638
+ if (lang === "go") {
2639
+ const owner2 = findOwningRoot(structure, portable, ["go"]);
2640
+ const dir = path4.posix.dirname(portable);
2641
+ if (owner2?.importPath) {
2642
+ const relative3 = path4.posix.relative(owner2.dir, dir);
2643
+ labels.set(file, relative3 ? `${owner2.importPath}/${relative3}` : owner2.importPath);
2644
+ } else {
2645
+ labels.set(file, `go:${path4.posix.relative(structure.projectRoot, dir) || "."}`);
2646
+ }
2647
+ continue;
2648
+ }
2649
+ if (lang === "py") {
2650
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
2651
+ if (dotted) {
2652
+ labels.set(file, dotted);
2653
+ continue;
2654
+ }
2655
+ }
2656
+ const owner = findOwningRoot(structure, portable);
2657
+ const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
2658
+ labels.set(file, label);
2659
+ }
2660
+ return labels;
2661
+ }
2662
+
2663
+ // src/codebase-index/module-resolver.ts
2664
+ var EXTENSIONS = {
2665
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
2666
+ py: [".py", ".pyi"],
2667
+ rs: [".rs"],
2668
+ jvm: [".java", ".kt", ".scala"],
2669
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
2670
+ ruby: [".rb"],
2671
+ go: [".go"]
2672
+ };
2673
+ var DIRECTORY_ENTRIES = {
2674
+ js: ["index"],
2675
+ py: ["__init__"],
2676
+ rs: ["mod"],
2677
+ ruby: ["index"]
2678
+ };
2679
+ function normalizeNamespace(value) {
2680
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
2681
+ }
2682
+ var ModuleResolver = class {
2683
+ structure;
2684
+ /** Lowercased portable path → the path as indexed (case is preserved). */
2685
+ byPath;
2686
+ /** Lowercased portable directory → files directly inside it, as indexed. */
2687
+ byDir;
2688
+ /** Normalized namespace → the file declaring it (first by path, stable). */
2689
+ byNamespace;
2690
+ constructor(structure, files, namespaces = []) {
2691
+ this.structure = structure;
2692
+ this.byPath = /* @__PURE__ */ new Map();
2693
+ this.byDir = /* @__PURE__ */ new Map();
2694
+ this.byNamespace = /* @__PURE__ */ new Map();
2695
+ const dirsByKey = /* @__PURE__ */ new Map();
2696
+ for (const file of files) {
2697
+ const portable = toPortablePath(file);
2698
+ const pathKey = portable.toLowerCase();
2699
+ const priorPath = this.byPath.get(pathKey);
2700
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
2701
+ else this.byPath.set(pathKey, file);
2702
+ const dir = path5.posix.dirname(portable);
2703
+ const dirKey = dir.toLowerCase();
2704
+ const knownDir = dirsByKey.get(dirKey);
2705
+ if (knownDir === void 0) {
2706
+ dirsByKey.set(dirKey, dir);
2707
+ this.byDir.set(dirKey, [file]);
2708
+ } else if (knownDir === dir) {
2709
+ this.byDir.get(dirKey)?.push(file);
2710
+ } else {
2711
+ dirsByKey.delete(dirKey);
2712
+ this.byDir.delete(dirKey);
2713
+ }
2714
+ }
2715
+ for (const { name, file } of namespaces) {
2716
+ const lang = detectLang(file);
2717
+ if (!lang) continue;
2718
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
2719
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
2720
+ this.byNamespace.set(key, file);
2721
+ }
2722
+ }
2723
+ }
2724
+ /**
2725
+ * Resolve `specifier` as written in `fromFile`.
2726
+ * Returns the indexed target path, or `undefined` when it is external or
2727
+ * cannot be located.
2728
+ */
2729
+ resolve(fromFile, lang, specifier) {
2730
+ const spec = specifier.trim().replace(/\\/g, "/");
2731
+ if (!spec) return void 0;
2732
+ const from = toPortablePath(fromFile);
2733
+ switch (languageFamily(lang)) {
2734
+ case "js":
2735
+ return this.resolveJs(from, spec);
2736
+ case "go":
2737
+ return this.resolveGo(spec);
2738
+ case "py":
2739
+ return this.resolvePython(from, spec);
2740
+ case "rs":
2741
+ return this.resolveRust(from, spec);
2742
+ case "jvm":
2743
+ return this.resolveJvm(spec);
2744
+ case "c":
2745
+ return this.resolveInclude(from, spec);
2746
+ case "ruby":
2747
+ return this.resolveRuby(from, spec);
2748
+ case "dotnet":
2749
+ case "php":
2750
+ case "elixir":
2751
+ case "haskell":
2752
+ return this.resolveNamespace(lang, spec);
2753
+ default:
2754
+ return void 0;
2755
+ }
2756
+ }
2757
+ /**
2758
+ * Resolve a namespace specifier to the file declaring it.
2759
+ *
2760
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
2761
+ * names a namespace outright, while PHP's `use App\Models\User` names a
2762
+ * *class* inside `App\Models`, so the prefix is what was declared.
2763
+ */
2764
+ resolveNamespace(lang, spec) {
2765
+ const family = languageFamily(lang);
2766
+ const normalized = normalizeNamespace(spec);
2767
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
2768
+ if (exact) return exact;
2769
+ const segments = normalized.split(".").filter(Boolean);
2770
+ if (segments.length < 2) return void 0;
2771
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
2772
+ }
2773
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
2774
+ lookup(candidate) {
2775
+ return this.byPath.get(path5.posix.normalize(candidate).toLowerCase());
2776
+ }
2777
+ /**
2778
+ * Try `base` verbatim, then `base` + each extension, then each directory
2779
+ * entry point inside `base`.
2780
+ */
2781
+ lookupWithExtensions(base, family) {
2782
+ const direct = this.lookup(base);
2783
+ if (direct) return direct;
2784
+ const extensions = EXTENSIONS[family] ?? [];
2785
+ const suffix = path5.posix.extname(base);
2786
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
2787
+ for (const ext of extensions) {
2788
+ const hit = this.lookup(`${stem}${ext}`);
2789
+ if (hit) return hit;
2790
+ }
2791
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
2792
+ for (const ext of extensions) {
2793
+ const hit = this.lookup(path5.posix.join(base, `${entry}${ext}`));
2794
+ if (hit) return hit;
2795
+ }
2796
+ }
2797
+ return void 0;
2798
+ }
2799
+ /**
2800
+ * A representative indexed file inside `dir`, for ecosystems whose import
2801
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
2802
+ *
2803
+ * The choice is deterministic — a file named after the directory, else the
2804
+ * first by name — so the same import always produces the same edge. Package
2805
+ * grouping is unaffected either way: every file in the directory carries the
2806
+ * same package label, so the package-level edge is exact regardless of which
2807
+ * member represents it.
2808
+ */
2809
+ representativeIn(dir, family) {
2810
+ const members = this.byDir.get(path5.posix.normalize(dir).toLowerCase());
2811
+ if (!members?.length) return void 0;
2812
+ const extensions = EXTENSIONS[family] ?? [];
2813
+ const eligible = members.filter((file) => extensions.includes(path5.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
2814
+ if (eligible.length === 0) return void 0;
2815
+ const base = path5.posix.basename(path5.posix.normalize(dir)).toLowerCase();
2816
+ const named = eligible.find(
2817
+ (file) => path5.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
2818
+ );
2819
+ return named ?? eligible[0];
2820
+ }
2821
+ // ─── Per-family resolution ──────────────────────────────────────────────────
2822
+ /** Relative specifiers, then workspace package names and their subpaths. */
2823
+ resolveJs(fromFile, spec) {
2824
+ if (spec.startsWith(".")) {
2825
+ const absolute = path5.posix.join(path5.posix.dirname(fromFile), spec);
2826
+ return this.lookupWithExtensions(absolute, "js");
2827
+ }
2828
+ const owner = this.structure.roots.find(
2829
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2830
+ );
2831
+ if (!owner?.importPath) return void 0;
2832
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2833
+ if (!subpath) {
2834
+ return this.lookupWithExtensions(path5.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path5.posix.join(owner.dir, "index"), "js");
2835
+ }
2836
+ return this.lookupWithExtensions(path5.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path5.posix.join(owner.dir, "src", subpath), "js");
2837
+ }
2838
+ /** Go import paths are absolute module paths; a package is a directory. */
2839
+ resolveGo(spec) {
2840
+ const owner = this.structure.roots.find(
2841
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2842
+ );
2843
+ if (!owner?.importPath) return void 0;
2844
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2845
+ return this.representativeIn(path5.posix.join(owner.dir, subpath), "go");
2846
+ }
2847
+ /**
2848
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
2849
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
2850
+ */
2851
+ resolvePython(fromFile, spec) {
2852
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
2853
+ if (leadingDots > 0) {
2854
+ let base = path5.posix.dirname(fromFile);
2855
+ for (let i = 1; i < leadingDots; i++) base = path5.posix.dirname(base);
2856
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
2857
+ return this.lookupWithExtensions(path5.posix.join(base, ...rest), "py");
2858
+ }
2859
+ const segments = spec.split(".").filter(Boolean);
2860
+ if (segments.length === 0) return void 0;
2861
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
2862
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2863
+ const hit = this.lookupWithExtensions(path5.posix.join(base, ...segments), "py");
2864
+ if (hit) return hit;
2865
+ if (segments.length > 1) {
2866
+ const parent = this.lookupWithExtensions(
2867
+ path5.posix.join(base, ...segments.slice(0, -1)),
2868
+ "py"
2869
+ );
2870
+ if (parent) return parent;
2871
+ }
2872
+ }
2873
+ return void 0;
2874
+ }
2875
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
2876
+ resolveRust(fromFile, spec) {
2877
+ const segments = spec.split("::").filter(Boolean);
2878
+ if (segments.length === 0) return void 0;
2879
+ const head = segments[0];
2880
+ if (head === "self" || head === "super") {
2881
+ let base = path5.posix.dirname(fromFile);
2882
+ for (const segment of segments) {
2883
+ if (segment === "super") base = path5.posix.dirname(base);
2884
+ else if (segment !== "self") break;
2885
+ }
2886
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
2887
+ return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
2888
+ }
2889
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
2890
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
2891
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
2892
+ );
2893
+ if (!crate) {
2894
+ return this.lookupWithExtensions(
2895
+ path5.posix.join(path5.posix.dirname(fromFile), ...segments),
2896
+ "rs"
2897
+ );
2898
+ }
2899
+ const rest = segments.slice(1);
2900
+ for (const base of crate.sourceRoots) {
2901
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
2902
+ const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
2903
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
2904
+ if (hit) return hit;
2905
+ }
2906
+ return void 0;
2907
+ }
2908
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
2909
+ resolveJvm(spec) {
2910
+ const segments = spec.split(".").filter(Boolean);
2911
+ if (segments.length === 0) return void 0;
2912
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
2913
+ const wildcard = segments[segments.length - 1] === "*";
2914
+ const parts = wildcard ? segments.slice(0, -1) : segments;
2915
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2916
+ const target = path5.posix.join(base, ...parts);
2917
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
2918
+ if (hit) return hit;
2919
+ }
2920
+ return void 0;
2921
+ }
2922
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
2923
+ resolveInclude(fromFile, spec) {
2924
+ const relative3 = this.lookupWithExtensions(
2925
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
2926
+ "c"
2927
+ );
2928
+ if (relative3) return relative3;
2929
+ for (const base of [
2930
+ path5.posix.join(this.structure.projectRoot, "include"),
2931
+ this.structure.projectRoot
2932
+ ]) {
2933
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
2934
+ if (hit) return hit;
2935
+ }
2936
+ return void 0;
2937
+ }
2938
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
2939
+ resolveRuby(fromFile, spec) {
2940
+ const relative3 = this.lookupWithExtensions(
2941
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
2942
+ "ruby"
2943
+ );
2944
+ if (relative3) return relative3;
2945
+ for (const base of [
2946
+ path5.posix.join(this.structure.projectRoot, "lib"),
2947
+ this.structure.projectRoot
2948
+ ]) {
2949
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
2950
+ if (hit) return hit;
2951
+ }
2952
+ return void 0;
2953
+ }
2954
+ };
2955
+
2956
+ // src/codebase-index/import-extractor.ts
2957
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
2958
+ var IMPORT_MAX_PER_FILE = 400;
2959
+ var DOTTED_IMPORT = [
2960
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
2961
+ ];
2962
+ var LANG_IMPORTS = {
2963
+ // Go and Python have real AST extractors; these patterns are the fallback for
2964
+ // machines with no Go toolchain or Python interpreter installed, where the
2965
+ // parser degrades to regex symbols and would otherwise contribute no edges.
2966
+ go: [
2967
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
2968
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
2969
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
2970
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
2971
+ ],
2972
+ py: [
2973
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
2974
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
2975
+ ],
2976
+ rs: [
2977
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
2978
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
2979
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
2980
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
2981
+ ],
2982
+ java: DOTTED_IMPORT,
2983
+ kotlin: DOTTED_IMPORT,
2984
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
2985
+ csharp: [
2986
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
2987
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
2988
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
2989
+ ],
2990
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
2991
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2992
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2993
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
2994
+ php: [
2995
+ // `use A\B\C` imports the class C, which is what the index has a symbol
2996
+ // for — the namespace symbol only covers the `A\B` prefix.
2997
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
2998
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
2999
+ ],
3000
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
3001
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
3002
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
3003
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
3004
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
3005
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
3006
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
3007
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
3008
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
3009
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
3010
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3011
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3012
+ html: [
3013
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
3014
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
3015
+ ],
3016
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
3017
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
3018
+ };
3019
+ function lastSegment(specifier) {
3020
+ const pathLike = /[/\\]|::/.test(specifier);
3021
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
3022
+ let last = segments[segments.length - 1] ?? specifier;
3023
+ if (last === "*" || last === "_") {
3024
+ last = segments[segments.length - 2] ?? specifier;
3025
+ }
3026
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
3027
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
3028
+ return dotted[dotted.length - 1] ?? last;
3029
+ }
3030
+ function newlineOffsets(content) {
3031
+ const offsets = [];
3032
+ for (let i = 0; i < content.length; i++) {
3033
+ if (content.charCodeAt(i) === 10) offsets.push(i);
3034
+ }
3035
+ return offsets;
3036
+ }
3037
+ function lineAt(offsets, index) {
3038
+ let low = 0;
3039
+ let high = offsets.length;
3040
+ while (low < high) {
3041
+ const mid = low + high >>> 1;
3042
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
3043
+ else high = mid;
3044
+ }
3045
+ return low + 1;
3046
+ }
3047
+ function hasImportPatterns(lang) {
3048
+ return LANG_IMPORTS[lang] !== void 0;
3049
+ }
3050
+ function extractImports(opts) {
3051
+ const patterns = LANG_IMPORTS[opts.lang];
3052
+ if (!patterns || !opts.content) return [];
3053
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
3054
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
3055
+ const refs = [];
3056
+ const seen = /* @__PURE__ */ new Set();
3057
+ const offsets = newlineOffsets(content);
3058
+ for (const pattern of patterns) {
3059
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
3060
+ for (const match of content.matchAll(re)) {
3061
+ if (refs.length >= limit) return refs;
3062
+ const specifier = match[1]?.trim();
3063
+ if (!specifier) continue;
3064
+ const module = specifier;
3065
+ const toName = pattern.name === "full" ? module : lastSegment(module);
3066
+ if (!toName) continue;
3067
+ const key = `${module}\0${toName}`;
3068
+ if (seen.has(key)) continue;
3069
+ seen.add(key);
3070
+ refs.push({
3071
+ fromId: 0,
3072
+ toName,
3073
+ callType: "import",
3074
+ line: lineAt(offsets, match.index ?? 0),
3075
+ lang: opts.lang,
3076
+ module
3077
+ });
3078
+ }
3079
+ }
3080
+ return refs;
3081
+ }
3082
+
2197
3083
  // src/codebase-index/parser-dispatch.ts
2198
3084
  async function parseFileContent(file, content, lang) {
3085
+ const parsed = await dispatch(file, content, lang);
3086
+ return withRelations(parsed, content, lang);
3087
+ }
3088
+ async function dispatch(file, content, lang) {
2199
3089
  switch (lang) {
2200
3090
  case "ts":
2201
3091
  case "tsx":
@@ -2230,6 +3120,13 @@ async function parseFileContent(file, content, lang) {
2230
3120
  }
2231
3121
  }
2232
3122
  }
3123
+ function withRelations(parsed, content, lang) {
3124
+ let refs = parsed.refs ?? [];
3125
+ if (refs.length === 0 && hasImportPatterns(lang)) {
3126
+ refs = extractImports({ content, lang });
3127
+ }
3128
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
3129
+ }
2233
3130
 
2234
3131
  // src/codebase-index/writer.ts
2235
3132
  import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
@@ -2239,9 +3136,9 @@ import * as path11 from "node:path";
2239
3136
  // src/codebase-index/bm25.ts
2240
3137
  var K1 = 1.5;
2241
3138
  var B = 0.75;
3139
+ var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
2242
3140
  function tokenise(text) {
2243
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
2244
- return sanitised.toLowerCase().split(" ").filter(Boolean);
3141
+ return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
2245
3142
  }
2246
3143
  function splitName(name) {
2247
3144
  return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
@@ -2325,6 +3222,9 @@ var Bm25Index = class {
2325
3222
  }
2326
3223
  };
2327
3224
 
3225
+ // src/codebase-index/writer.ts
3226
+ init_languages();
3227
+
2328
3228
  // src/codebase-index/lsp-kind.ts
2329
3229
  function lspKindToInternalKind(k) {
2330
3230
  switch (k) {
@@ -2359,7 +3259,7 @@ function lspKindToInternalKind(k) {
2359
3259
  }
2360
3260
 
2361
3261
  // src/codebase-index/schema.ts
2362
- var SCHEMA_VERSION = 3;
3262
+ var SCHEMA_VERSION = 4;
2363
3263
 
2364
3264
  // src/codebase-index/sqlite-runtime.ts
2365
3265
  import { createRequire } from "node:module";
@@ -2430,7 +3330,7 @@ function runSqliteWithRetry(fn) {
2430
3330
 
2431
3331
  // src/codebase-index/writer-admin.ts
2432
3332
  import * as fs6 from "node:fs";
2433
- import * as path9 from "node:path";
3333
+ import * as path10 from "node:path";
2434
3334
  var DB_FILE = "index.db";
2435
3335
  function getAllIndexableWithStatement(stmt) {
2436
3336
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -2489,7 +3389,7 @@ function getAllFileMetasWithStatement(stmt) {
2489
3389
  }
2490
3390
  function getIndexDbSizeBytes(indexDir) {
2491
3391
  try {
2492
- return fs6.statSync(path9.join(indexDir, DB_FILE)).size;
3392
+ return fs6.statSync(path10.join(indexDir, DB_FILE)).size;
2493
3393
  } catch {
2494
3394
  return 0;
2495
3395
  }
@@ -2540,49 +3440,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
2540
3440
  }
2541
3441
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2542
3442
  if (refs.length === 0) return;
2543
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
3443
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
2544
3444
  for (let i = 0; i < refs.length; i += chunkSize) {
2545
3445
  const chunk = refs.slice(i, i + chunkSize);
2546
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
3446
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
2547
3447
  const insert = stmt(
2548
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
3448
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
3449
+ VALUES ${placeholders}`
2549
3450
  );
2550
3451
  const binds = [];
2551
3452
  for (const ref of chunk) {
2552
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
3453
+ binds.push(
3454
+ ref.fromId,
3455
+ ref.toName,
3456
+ ref.toId ?? null,
3457
+ ref.callType,
3458
+ ref.line,
3459
+ ref.lang ?? "",
3460
+ ref.module ?? null,
3461
+ ref.toFile ?? null
3462
+ );
2553
3463
  }
2554
3464
  insert.run(...binds);
2555
3465
  }
2556
3466
  }
2557
3467
 
3468
+ // src/codebase-index/writer-graph-reader.ts
3469
+ init_languages();
3470
+
2558
3471
  // src/codebase-index/writer-graph-helpers.ts
2559
- import * as path10 from "node:path";
2560
- function derivePackage(filePath) {
2561
- const f = filePath.replace(/\\/g, "/");
2562
- const pkgsIdx = f.indexOf("/packages/");
2563
- if (pkgsIdx !== -1) {
2564
- const rest = f.slice(pkgsIdx + "/packages/".length);
2565
- const seg = rest.split("/")[0];
2566
- return seg ? `@wrongstack/${seg}` : void 0;
2567
- }
2568
- const appsIdx = f.indexOf("/apps/");
2569
- if (appsIdx !== -1) {
2570
- const rest = f.slice(appsIdx + "/apps/".length);
2571
- const seg = rest.split("/")[0];
2572
- return seg ? `app:${seg}` : void 0;
2573
- }
2574
- return void 0;
2575
- }
2576
- function packageFromImport(moduleName) {
2577
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
2578
- const parts = moduleName.split("/");
2579
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
3472
+ function createPackageLabeller(stored) {
3473
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
2580
3474
  }
2581
- function buildPackageGraphNodes(fileCounts, files) {
3475
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
2582
3476
  const pkgNodes = /* @__PURE__ */ new Map();
2583
3477
  const fileToPkg = /* @__PURE__ */ new Map();
2584
3478
  for (const { file, n } of fileCounts) {
2585
- const pkg = derivePackage(file) ?? "(root)";
3479
+ const pkg = packageOf(file);
2586
3480
  fileToPkg.set(file, pkg);
2587
3481
  const node = pkgNodes.get(pkg);
2588
3482
  if (node) {
@@ -2599,7 +3493,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2599
3493
  }
2600
3494
  }
2601
3495
  for (const { file } of files) {
2602
- const pkg = derivePackage(file) ?? "(root)";
3496
+ const pkg = packageOf(file);
2603
3497
  fileToPkg.set(file, pkg);
2604
3498
  const node = pkgNodes.get(pkg);
2605
3499
  if (node) {
@@ -2617,7 +3511,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2617
3511
  }
2618
3512
  return { pkgNodes, fileToPkg };
2619
3513
  }
2620
- function buildFileGraphNodeState(pkgSyms, localFiles) {
3514
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
2621
3515
  const fileNodes = /* @__PURE__ */ new Map();
2622
3516
  const symToFile = /* @__PURE__ */ new Map();
2623
3517
  const fileStats = /* @__PURE__ */ new Map();
@@ -2636,7 +3530,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2636
3530
  id: `file:${file}`,
2637
3531
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
2638
3532
  kind: "file",
2639
- package: derivePackage(file) ?? "(root)",
3533
+ package: packageOf(file),
2640
3534
  file,
2641
3535
  symbolCount: stats?.count ?? 0,
2642
3536
  lang: stats?.lang,
@@ -2648,7 +3542,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2648
3542
  }
2649
3543
  return { fileNodes, symToFile, fileStats, ensureFileNode };
2650
3544
  }
2651
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
3545
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
2652
3546
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
2653
3547
  const aExternal = a.file === fileFilter ? 0 : 1;
2654
3548
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -2660,7 +3554,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2660
3554
  symbolId: s.id,
2661
3555
  symbolKind: s.kind,
2662
3556
  file: s.file,
2663
- package: derivePackage(s.file) ?? "(root)",
3557
+ package: packageOf(s.file),
2664
3558
  lang: s.lang,
2665
3559
  line: s.line,
2666
3560
  signature: s.signature,
@@ -2668,29 +3562,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2668
3562
  external: s.file !== fileFilter
2669
3563
  }));
2670
3564
  }
2671
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
2672
- if (!moduleName.startsWith(".")) return void 0;
2673
- const normalizedFrom = fromFile.replace(/\\/g, "/");
2674
- const absolute = path10.posix.normalize(
2675
- path10.posix.join(path10.posix.dirname(normalizedFrom), moduleName)
2676
- );
2677
- const extension = path10.posix.extname(absolute);
2678
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
2679
- const candidates = [
2680
- absolute,
2681
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
2682
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path10.posix.join(absolute, `index${ext}`)),
2683
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path10.posix.join(base, `index${ext}`))
2684
- ];
2685
- const indexedByPortablePath = new Map(
2686
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
2687
- );
2688
- for (const candidate of candidates) {
2689
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
2690
- if (indexed) return indexed;
2691
- }
2692
- return void 0;
2693
- }
2694
3565
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
2695
3566
  const key = `${source}\0${target}`;
2696
3567
  let edge = edgeMap.get(key);
@@ -2731,7 +3602,12 @@ function mapWriterRefRow(row) {
2731
3602
  toName: row.to_name,
2732
3603
  toId: row.to_id ?? void 0,
2733
3604
  callType: row.call_type,
2734
- line: row.line
3605
+ line: row.line,
3606
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
3607
+ // queries select; `undefined` keeps those rows valid Refs.
3608
+ lang: row.lang || void 0,
3609
+ module: row.module ?? void 0,
3610
+ toFile: row.to_file ?? void 0
2735
3611
  };
2736
3612
  }
2737
3613
 
@@ -2879,7 +3755,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
2879
3755
  function getPackageGraphWithStatement(stmt) {
2880
3756
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
2881
3757
  const files = stmt("SELECT DISTINCT file FROM files").all();
2882
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
3758
+ const packageOf = readPackageLabeller(stmt);
3759
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
2883
3760
  const refRows = stmt(
2884
3761
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
2885
3762
  FROM refs r
@@ -2890,32 +3767,42 @@ function getPackageGraphWithStatement(stmt) {
2890
3767
  ).all();
2891
3768
  const edgeMap = /* @__PURE__ */ new Map();
2892
3769
  for (const r of refRows) {
2893
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2894
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
3770
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3771
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
2895
3772
  if (fromPkg === toPkg) continue;
2896
3773
  const n = Number(r.n) || 0;
2897
3774
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
2898
3775
  }
2899
3776
  const importRows = stmt(
2900
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
3777
+ `SELECT s.file AS from_file,
3778
+ COALESCE(r.to_file, st.file) AS to_file,
3779
+ COUNT(*) AS n
2901
3780
  FROM refs r
2902
3781
  JOIN symbols s ON s.id = r.from_id
3782
+ LEFT JOIN symbols st ON st.id = r.to_id
2903
3783
  WHERE r.call_type = 'import'
2904
- GROUP BY r.to_name, s.file`
3784
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
3785
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
2905
3786
  ).all();
2906
3787
  for (const r of importRows) {
2907
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2908
- const toPkg = packageFromImport(r.to_name);
2909
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
3788
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3789
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
3790
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
2910
3791
  const n = Number(r.n) || 0;
2911
3792
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
2912
3793
  }
2913
3794
  const edges = materializeWeightedEdges(edgeMap, "pkg");
2914
3795
  return { nodes: [...pkgNodes.values()], edges };
2915
3796
  }
3797
+ function readPackageLabeller(stmt) {
3798
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
3799
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
3800
+ }
2916
3801
  function getFileGraphWithStatement(stmt, packageFilter) {
2917
3802
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
2918
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
3803
+ const packageOf = readPackageLabeller(stmt);
3804
+ const langOf = (file) => detectLang(file) ?? "other";
3805
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
2919
3806
  const localFiles = new Set(pkgFilePaths);
2920
3807
  if (localFiles.size === 0) return { nodes: [], edges: [] };
2921
3808
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -2924,9 +3811,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2924
3811
  ).all(...pkgFilePaths);
2925
3812
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
2926
3813
  pkgSyms,
2927
- localFiles
3814
+ localFiles,
3815
+ packageOf
2928
3816
  );
2929
- const indexedFiles = new Set(allFiles.map((f) => f.file));
2930
3817
  const refRows = stmt(
2931
3818
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
2932
3819
  FROM refs r
@@ -2949,7 +3836,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2949
3836
  for (const x of extras) {
2950
3837
  symToFile.set(x.id, x.file);
2951
3838
  if (!fileStats.has(x.file)) {
2952
- fileStats.set(x.file, { count: 0, lang: "ts" });
3839
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
2953
3840
  }
2954
3841
  }
2955
3842
  }
@@ -2966,17 +3853,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2966
3853
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
2967
3854
  }
2968
3855
  const importRows = stmt(
2969
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
3856
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
2970
3857
  FROM refs r
3858
+ LEFT JOIN symbols st ON st.id = r.to_id
2971
3859
  WHERE r.call_type = 'import'
3860
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
2972
3861
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
2973
- GROUP BY r.from_id, r.to_name`
3862
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
2974
3863
  ).all(...pkgFilePaths);
2975
3864
  for (const r of importRows) {
2976
3865
  const fromFile = symToFile.get(r.from_id);
2977
3866
  if (!fromFile || !localFiles.has(fromFile)) continue;
2978
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
3867
+ const toFile = r.to_file;
2979
3868
  if (!toFile || fromFile === toFile) continue;
3869
+ if (!fileStats.has(toFile)) {
3870
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
3871
+ }
2980
3872
  ensureFileNode(fromFile);
2981
3873
  ensureFileNode(toFile);
2982
3874
  const n = Number(r.n) || 0;
@@ -3026,7 +3918,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
3026
3918
  ).all(...missingIds);
3027
3919
  for (const s of extras) symById.set(s.id, s);
3028
3920
  }
3029
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
3921
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
3030
3922
  return { nodes, edges };
3031
3923
  }
3032
3924
 
@@ -3048,7 +3940,7 @@ function assignRefsToSymbols(refs, symbols) {
3048
3940
  }
3049
3941
  if (!owner && ref.callType === "import") owner = ordered[0];
3050
3942
  if (!owner || owner.id <= 0) continue;
3051
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3943
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
3052
3944
  if (seen.has(key)) continue;
3053
3945
  seen.add(key);
3054
3946
  assigned.push({ ...ref, fromId: owner.id });
@@ -3094,7 +3986,11 @@ var CORE_TABLES_SQL = `
3094
3986
  lang TEXT NOT NULL,
3095
3987
  mtime_ms INTEGER NOT NULL,
3096
3988
  symbol_count INTEGER NOT NULL DEFAULT 0,
3097
- last_indexed INTEGER NOT NULL
3989
+ last_indexed INTEGER NOT NULL,
3990
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
3991
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
3992
+ -- re-derived per query because the evidence lives on disk, not in the DB.
3993
+ package TEXT NOT NULL DEFAULT ''
3098
3994
  );
3099
3995
  CREATE TABLE IF NOT EXISTS symbols (
3100
3996
  id INTEGER PRIMARY KEY,
@@ -3111,6 +4007,9 @@ var CORE_TABLES_SQL = `
3111
4007
  file_fk TEXT NOT NULL
3112
4008
  );
3113
4009
  `;
4010
+ var FILE_INDEX_SQL = [
4011
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
4012
+ ];
3114
4013
  var SYMBOL_INDEX_SQL = [
3115
4014
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
3116
4015
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -3127,15 +4026,32 @@ var REFS_TABLE_SQL = `
3127
4026
  to_name TEXT NOT NULL,
3128
4027
  to_id INTEGER,
3129
4028
  call_type TEXT NOT NULL,
3130
- line INTEGER NOT NULL
4029
+ line INTEGER NOT NULL,
4030
+ lang TEXT NOT NULL DEFAULT '',
4031
+ module TEXT,
4032
+ to_file TEXT
3131
4033
  );
3132
4034
  `;
3133
4035
  var REFS_INDEX_SQL = [
3134
4036
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
3135
4037
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
3136
4038
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
3137
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
4039
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
4040
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
4041
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
4042
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
4043
+ // The post-index module resolution pass groups unresolved import refs by
4044
+ // (module, lang); graph readers then read to_file back.
4045
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
4046
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
3138
4047
  ];
4048
+ var LANG_FAMILY_TABLE_SQL = `
4049
+ CREATE TABLE IF NOT EXISTS lang_family (
4050
+ lang TEXT PRIMARY KEY,
4051
+ family TEXT NOT NULL
4052
+ );
4053
+ `;
4054
+ var LANG_FAMILY_WILDCARD = "*";
3139
4055
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
3140
4056
 
3141
4057
  // src/codebase-index/writer-search-helpers.ts
@@ -3356,6 +4272,60 @@ var IndexStore = class _IndexStore {
3356
4272
  runWithRetry(fn) {
3357
4273
  return runSqliteWithRetry(fn);
3358
4274
  }
4275
+ /**
4276
+ * Mirror the in-process language→family map into SQLite.
4277
+ *
4278
+ * Rewritten on every open rather than only on schema bumps: the mapping is
4279
+ * static lookup data, so a code-side change (a new language, a language
4280
+ * moving families) must take effect without forcing a full reindex.
4281
+ */
4282
+ seedLangFamilies() {
4283
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
4284
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
4285
+ insert.run("", LANG_FAMILY_WILDCARD);
4286
+ }
4287
+ /**
4288
+ * Add any column the current schema expects but the on-disk table lacks.
4289
+ *
4290
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
4291
+ * and the version check above only rebuilds on a version *mismatch*. That
4292
+ * leaves a real gap: several wstack processes share this database, and while
4293
+ * a version upgrade is rolling out one of them may still be running the
4294
+ * previous build. That older process sees the newer version number, drops the
4295
+ * tables, and recreates them from *its* DDL — without the newer columns —
4296
+ * while the metadata row still reads the new version. Every later query for
4297
+ * one of those columns then fails with `no such column`, and no amount of
4298
+ * reindexing fixes it, because the version numbers already agree.
4299
+ *
4300
+ * Repairing column-by-column makes the schema self-healing from any of those
4301
+ * states. Table and column names are compile-time literals from this module,
4302
+ * never user input.
4303
+ */
4304
+ repairMissingColumns() {
4305
+ const expected = [
4306
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
4307
+ {
4308
+ table: "refs",
4309
+ columns: [
4310
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
4311
+ ["module", "TEXT"],
4312
+ ["to_file", "TEXT"]
4313
+ ]
4314
+ }
4315
+ ];
4316
+ for (const { table, columns } of expected) {
4317
+ const present = new Set(
4318
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
4319
+ (row) => typeof row.name === "string" ? [row.name] : []
4320
+ )
4321
+ );
4322
+ if (present.size === 0) continue;
4323
+ for (const [name, type] of columns) {
4324
+ if (present.has(name)) continue;
4325
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
4326
+ }
4327
+ }
4328
+ }
3359
4329
  initSchema() {
3360
4330
  this.db.exec(METADATA_TABLE_SQL);
3361
4331
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -3378,9 +4348,13 @@ var IndexStore = class _IndexStore {
3378
4348
  );
3379
4349
  }
3380
4350
  this.db.exec(CORE_TABLES_SQL);
3381
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3382
4351
  this.db.exec(REFS_TABLE_SQL);
4352
+ this.repairMissingColumns();
4353
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
4354
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3383
4355
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
4356
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
4357
+ this.seedLangFamilies();
3384
4358
  try {
3385
4359
  this.db.exec(SYMBOLS_FTS_SQL);
3386
4360
  this.ftsAvailable = true;
@@ -3415,6 +4389,18 @@ var IndexStore = class _IndexStore {
3415
4389
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
3416
4390
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
3417
4391
  static MAX_SQL_VARS = 900;
4392
+ /**
4393
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
4394
+ * `sym` belong to the same language family — or the ref carries no language,
4395
+ * in which case the wildcard bind matches everything.
4396
+ *
4397
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
4398
+ */
4399
+ static FAMILY_MATCH_SQL = `(
4400
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
4401
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
4402
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
4403
+ )`;
3418
4404
  /**
3419
4405
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
3420
4406
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -3478,9 +4464,12 @@ var IndexStore = class _IndexStore {
3478
4464
  const placeholders = chunk.map(() => "?").join(",");
3479
4465
  const result = this.stmt(
3480
4466
  `UPDATE refs
3481
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
4467
+ SET to_id = (
4468
+ SELECT MIN(sym.id) FROM symbols sym
4469
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
4470
+ )
3482
4471
  WHERE to_name IN (${placeholders})`
3483
- ).run(...chunk);
4472
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
3484
4473
  changes += result.changes ?? 0;
3485
4474
  }
3486
4475
  return changes;
@@ -3607,6 +4596,115 @@ var IndexStore = class _IndexStore {
3607
4596
  getAllFileMetas() {
3608
4597
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
3609
4598
  }
4599
+ // ─── Project structure & module resolution ──────────────────────────────────
4600
+ /** Store the Code Atlas grouping label for each indexed file. */
4601
+ setFilePackages(entries) {
4602
+ if (entries.size === 0) return;
4603
+ this.runWithRetry(() => {
4604
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
4605
+ for (const [file, label] of entries) update.run(label, file);
4606
+ });
4607
+ }
4608
+ /**
4609
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
4610
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
4611
+ * Ordered so the resolver's choice among duplicate declarations is stable.
4612
+ */
4613
+ getNamespaceDeclarations() {
4614
+ return this.stmt(
4615
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
4616
+ ).all();
4617
+ }
4618
+ /** `file → package` for every indexed file that has a label. */
4619
+ getFilePackages() {
4620
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
4621
+ return new Map(rows.map((row) => [row.file, row.package]));
4622
+ }
4623
+ /**
4624
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
4625
+ *
4626
+ * Distinct rather than per-ref because resolution depends only on these three
4627
+ * values: a file importing the same module twenty times resolves it once.
4628
+ */
4629
+ getUnresolvedImports(onlyFiles) {
4630
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
4631
+ FROM refs r
4632
+ JOIN symbols s ON s.id = r.from_id
4633
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
4634
+ if (!onlyFiles?.length) {
4635
+ return this.stmt(base).all();
4636
+ }
4637
+ const out = [];
4638
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
4639
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
4640
+ const placeholders = chunk.map(() => "?").join(",");
4641
+ out.push(
4642
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
4643
+ );
4644
+ }
4645
+ return out;
4646
+ }
4647
+ /**
4648
+ * Write resolved import targets back onto `refs.to_file`.
4649
+ *
4650
+ * Applied through a temp table and a single UPDATE: one statement per
4651
+ * resolution would mean thousands of round-trips on a first index.
4652
+ */
4653
+ applyImportResolutions(resolutions) {
4654
+ if (resolutions.length === 0) return 0;
4655
+ return this.runWithRetry(() => {
4656
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4657
+ this.db.exec(
4658
+ `CREATE TEMP TABLE import_resolution (
4659
+ from_file TEXT NOT NULL,
4660
+ lang TEXT NOT NULL,
4661
+ module TEXT NOT NULL,
4662
+ to_file TEXT NOT NULL
4663
+ )`
4664
+ );
4665
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
4666
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
4667
+ const chunk = resolutions.slice(i, i + chunkSize);
4668
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
4669
+ const binds = [];
4670
+ for (const entry of chunk) {
4671
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
4672
+ }
4673
+ this.stmt(
4674
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
4675
+ VALUES ${placeholders}`
4676
+ ).run(...binds);
4677
+ }
4678
+ this.db.exec(
4679
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
4680
+ ON import_resolution(module, lang, from_file)`
4681
+ );
4682
+ const result = this.stmt(
4683
+ `UPDATE refs
4684
+ SET to_file = (
4685
+ SELECT ir.to_file
4686
+ FROM temp.import_resolution ir
4687
+ JOIN symbols s ON s.id = refs.from_id
4688
+ WHERE ir.module = refs.module
4689
+ AND ir.lang = refs.lang
4690
+ AND ir.from_file = s.file
4691
+ LIMIT 1
4692
+ )
4693
+ WHERE refs.call_type = 'import'
4694
+ AND refs.module IS NOT NULL
4695
+ AND EXISTS (
4696
+ SELECT 1
4697
+ FROM temp.import_resolution ir
4698
+ JOIN symbols s ON s.id = refs.from_id
4699
+ WHERE ir.module = refs.module
4700
+ AND ir.lang = refs.lang
4701
+ AND ir.from_file = s.file
4702
+ )`
4703
+ ).run();
4704
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4705
+ return result.changes ?? 0;
4706
+ });
4707
+ }
3610
4708
  // ─── Search ──────────────────────────────────────────────────────────────────
3611
4709
  search(query, filter, opts) {
3612
4710
  const built = this.buildSearchWhere(query, filter);
@@ -3993,9 +5091,12 @@ var IndexStore = class _IndexStore {
3993
5091
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
3994
5092
  * Call this after all symbols have been inserted to fill in cross-references.
3995
5093
  *
3996
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
3997
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
3998
- * that found a targetmatching the previous per-row loop's return value.
5094
+ * A match additionally requires the referencing ref and the target symbol to
5095
+ * be in the same {@link LangFamily}. Without that guard a name match is a
5096
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
5097
+ * `Config` are declared in most languages at once, and each collision draws a
5098
+ * Code Atlas edge between files that never reference each other. Refs stored
5099
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
3999
5100
  */
4000
5101
  resolveRefs() {
4001
5102
  return this.runWithRetry(() => {
@@ -4004,20 +5105,35 @@ var IndexStore = class _IndexStore {
4004
5105
  `UPDATE refs
4005
5106
  SET to_id = s.id
4006
5107
  FROM (
4007
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
4008
- ) AS s
5108
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5109
+ FROM symbols sym
5110
+ JOIN lang_family lf ON lf.lang = sym.lang
5111
+ GROUP BY sym.name, lf.family
5112
+ UNION ALL
5113
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5114
+ FROM symbols sym
5115
+ GROUP BY sym.name
5116
+ ) AS s,
5117
+ lang_family AS rf
4009
5118
  WHERE refs.to_id IS NULL
4010
5119
  AND refs.to_name IS NOT NULL
4011
- AND refs.to_name = s.name`
5120
+ AND rf.lang = refs.lang
5121
+ AND s.name = refs.to_name
5122
+ AND s.family = rf.family`
4012
5123
  ).run();
4013
5124
  return result.changes ?? 0;
4014
5125
  } catch {
4015
5126
  const result = this.stmt(
4016
5127
  `UPDATE refs SET to_id = (
4017
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
5128
+ SELECT sym.id FROM symbols sym
5129
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5130
+ ORDER BY sym.id LIMIT 1
4018
5131
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
4019
- AND to_name IN (SELECT name FROM symbols)`
4020
- ).run();
5132
+ AND EXISTS (
5133
+ SELECT 1 FROM symbols sym
5134
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5135
+ )`
5136
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
4021
5137
  return result.changes ?? 0;
4022
5138
  }
4023
5139
  });
@@ -4235,7 +5351,7 @@ function normalizeComparablePath(value) {
4235
5351
  }
4236
5352
  function gitOutput(projectRoot, args) {
4237
5353
  return new Promise((resolve4, reject) => {
4238
- execFile2(
5354
+ execFile(
4239
5355
  "git",
4240
5356
  ["-C", projectRoot, ...args],
4241
5357
  {
@@ -4366,13 +5482,40 @@ function assignRefsToSymbols2(refs, symbols) {
4366
5482
  }
4367
5483
  if (!owner && ref.callType === "import") owner = ordered[0];
4368
5484
  if (!owner || owner.id <= 0) continue;
4369
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
5485
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4370
5486
  if (seen.has(key)) continue;
4371
5487
  seen.add(key);
4372
5488
  assigned.push({ ...ref, fromId: owner.id });
4373
5489
  }
4374
5490
  return assigned;
4375
5491
  }
5492
+ async function resolveProjectRelations(store, projectRoot, opts) {
5493
+ if (opts.signal?.aborted) return;
5494
+ try {
5495
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
5496
+ if (indexedFiles.length === 0) return;
5497
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
5498
+ if (opts.signal?.aborted) return;
5499
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
5500
+ const resolver = new ModuleResolver(
5501
+ structure,
5502
+ indexedFiles,
5503
+ store.getNamespaceDeclarations()
5504
+ );
5505
+ const pending2 = store.getUnresolvedImports(opts.onlyFiles);
5506
+ const resolutions = [];
5507
+ for (const entry of pending2) {
5508
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
5509
+ if (toFile && toFile !== entry.fromFile) {
5510
+ resolutions.push({ ...entry, toFile });
5511
+ }
5512
+ }
5513
+ if (opts.signal?.aborted) return;
5514
+ store.applyImportResolutions(resolutions);
5515
+ } catch (err) {
5516
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
5517
+ }
5518
+ }
4376
5519
  async function runIndexerWithStore(store, opts) {
4377
5520
  const { projectRoot, langs, ignore = [], signal } = opts;
4378
5521
  const relationGraphVersion = "2";
@@ -4617,6 +5760,14 @@ async function runIndexerWithStore(store, opts) {
4617
5760
  }
4618
5761
  }
4619
5762
  if (needsFullRefResolution) store.resolveRefs();
5763
+ await resolveProjectRelations(store, projectRoot, {
5764
+ // A watcher run re-resolves only what it touched; a full run (or a contract
5765
+ // bump) re-resolves everything, because a newly indexed file can be the
5766
+ // target of imports written long before it.
5767
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
5768
+ errors,
5769
+ signal
5770
+ });
4620
5771
  store.setMetadata("ref_resolution_version", refResolutionVersion);
4621
5772
  store.setMetadata("relation_graph_version", relationGraphVersion);
4622
5773
  if (!opts.files || filesIndexed >= 50) store.optimize();
@@ -4717,7 +5868,7 @@ function outgoingCallsService(args) {
4717
5868
  }
4718
5869
 
4719
5870
  // src/codebase-index/project-server-client.ts
4720
- import { spawn as spawn4 } from "node:child_process";
5871
+ import { spawn as spawn3 } from "node:child_process";
4721
5872
  import * as fs10 from "node:fs";
4722
5873
  import * as net from "node:net";
4723
5874
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -4735,7 +5886,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
4735
5886
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
4736
5887
  var buildIdCache;
4737
5888
  function projectIndexServerBuildId(entrypoint) {
4738
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path13.resolve(entrypoint);
5889
+ const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
5890
+ const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
5891
+ const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path13.resolve(cleanHref);
4739
5892
  try {
4740
5893
  const stat3 = fs9.statSync(file);
4741
5894
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
@@ -5333,7 +6486,7 @@ var ProjectServerConnection = class {
5333
6486
  }
5334
6487
  const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
5335
6488
  if (this.indexDir) args.push("--index-dir", this.indexDir);
5336
- const child = spawn4(process.execPath, args, {
6489
+ const child = spawn3(process.execPath, args, {
5337
6490
  detached: true,
5338
6491
  stdio: "ignore",
5339
6492
  windowsHide: true,