@wrongstack/tools 0.298.3 → 0.300.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/audit.js +6 -2
  2. package/dist/bash.js +20 -2
  3. package/dist/builtin.js +1901 -327
  4. package/dist/codebase-index/background-indexer.d.ts +6 -1
  5. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +34 -0
  6. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +34 -0
  7. package/dist/codebase-index/import-extractor.d.ts +39 -0
  8. package/dist/codebase-index/index-service.d.ts +24 -2
  9. package/dist/codebase-index/index.d.ts +4 -2
  10. package/dist/codebase-index/index.js +1779 -256
  11. package/dist/codebase-index/languages.d.ts +24 -0
  12. package/dist/codebase-index/module-resolver.d.ts +78 -0
  13. package/dist/codebase-index/module-roots.d.ts +81 -0
  14. package/dist/codebase-index/parser-output.d.ts +29 -0
  15. package/dist/codebase-index/project-server.js +1556 -237
  16. package/dist/codebase-index/rs-parser.d.ts +22 -0
  17. package/dist/codebase-index/schema.d.ts +47 -1
  18. package/dist/codebase-index/worker-protocol.d.ts +14 -0
  19. package/dist/codebase-index/worker.js +1545 -238
  20. package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
  21. package/dist/codebase-index/writer-graph-reader.d.ts +24 -1
  22. package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
  23. package/dist/codebase-index/writer-schema.d.ts +15 -3
  24. package/dist/codebase-index/writer.d.ts +97 -4
  25. package/dist/exec.js +39 -2
  26. package/dist/format.js +6 -2
  27. package/dist/index.js +1901 -327
  28. package/dist/install.js +6 -2
  29. package/dist/json.js +51 -2
  30. package/dist/languages/index.js +6 -2
  31. package/dist/lint.js +6 -2
  32. package/dist/outdated.js +6 -2
  33. package/dist/pack.js +1899 -327
  34. package/dist/process-registry.d.ts +6 -0
  35. package/dist/process-registry.js +6 -2
  36. package/dist/ps-slash.js +10 -2
  37. package/dist/read.js +1551 -244
  38. package/dist/test.js +6 -2
  39. package/dist/tool-tier.js +1901 -327
  40. package/dist/typecheck.js +6 -2
  41. package/package.json +3 -3
  42. package/dist/codebase-index/refs-extractor.d.ts +0 -11
@@ -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 path3 from "node:path";
409
+ import * as fs3 from "node:fs";
410
+ import * as path5 from "node:path";
342
411
  function resolveWin32Command(cmd) {
343
412
  if (process.platform !== "win32") return cmd;
344
- if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
413
+ if (cmd.includes("/") || cmd.includes("\\") || path5.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(path3.delimiter);
417
+ const pathDirs = (process.env["PATH"] ?? "").split(path5.delimiter);
349
418
  for (const dir of pathDirs) {
350
- const base = path3.join(dir, cmd);
419
+ const base = path5.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 path4 from "node:path";
394
- import * as fs3 from "node:fs/promises";
538
+ import * as path6 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(path4.join(os.tmpdir(), "ws-go-parse-"));
467
- scriptPath = path4.join(tmpDir, "parse.go");
468
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
612
+ const tmpDir = await fs4.mkdtemp(path6.join(os.tmpdir(), "ws-go-parse-"));
613
+ scriptPath = path6.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))
@@ -983,9 +1183,13 @@ var init_generic_parser = __esm({
983
1183
  ],
984
1184
  elixir: [
985
1185
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
986
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
1186
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
1187
+ // against this symbol, and a `Foo`-only capture never matches it.
1188
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
987
1189
  ],
988
1190
  haskell: [
1191
+ // Target of `import Data.List`.
1192
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
989
1193
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
990
1194
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
991
1195
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -1076,9 +1280,9 @@ __export(py_parser_exports, {
1076
1280
  parseSymbols: () => parseSymbols4
1077
1281
  });
1078
1282
  import { spawn as spawn2 } from "node:child_process";
1079
- import * as fs4 from "node:fs/promises";
1283
+ import * as fs5 from "node:fs/promises";
1080
1284
  import * as os2 from "node:os";
1081
- import * as path5 from "node:path";
1285
+ import * as path7 from "node:path";
1082
1286
  async function parseSymbols4(opts) {
1083
1287
  const { file, content, lang } = opts;
1084
1288
  try {
@@ -1156,10 +1360,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1156
1360
  async function syncPyParse(filePath, content, lang) {
1157
1361
  try {
1158
1362
  if (!_cachedScriptPath) {
1159
- const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
1160
- await fs4.mkdir(tmpDir, { recursive: true });
1161
- _cachedScriptPath = path5.join(tmpDir, "parse.py");
1162
- await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1363
+ const tmpDir = path7.join(os2.tmpdir(), "ws-py-parse");
1364
+ await fs5.mkdir(tmpDir, { recursive: true });
1365
+ _cachedScriptPath = path7.join(tmpDir, "parse.py");
1366
+ await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1163
1367
  }
1164
1368
  cachedPyBinary ??= resolvePython();
1165
1369
  const pyBinary = await cachedPyBinary;
@@ -1173,7 +1377,7 @@ async function syncPyParse(filePath, content, lang) {
1173
1377
  if (code !== 0 || !stdout.trim()) {
1174
1378
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1175
1379
  }
1176
- const raw = JSON.parse(stdout.trim());
1380
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
1177
1381
  const symbols = raw.map((s) => ({
1178
1382
  id: 0,
1179
1383
  lang,
@@ -1187,7 +1391,7 @@ async function syncPyParse(filePath, content, lang) {
1187
1391
  scope: s.scope ?? "",
1188
1392
  text: `${s.name} ${s.signature ?? ""}`.trim()
1189
1393
  }));
1190
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1394
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
1191
1395
  } catch {
1192
1396
  return null;
1193
1397
  }
@@ -1198,6 +1402,7 @@ var init_py_parser = __esm({
1198
1402
  "use strict";
1199
1403
  init_win32_resolve();
1200
1404
  init_generic_parser();
1405
+ init_parser_output();
1201
1406
  init_spawn_gate();
1202
1407
  init_languages();
1203
1408
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -1259,7 +1464,18 @@ class Sym:
1259
1464
  def is_private(name):
1260
1465
  return name.startswith("__") and not name.endswith("__")
1261
1466
 
1467
+ def leaf_name(node):
1468
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
1469
+ # TypeScript and Go extractors record call refs, so resolution behaves the
1470
+ # same across languages.
1471
+ if isinstance(node, ast.Attribute):
1472
+ return node.attr
1473
+ if isinstance(node, ast.Name):
1474
+ return node.id
1475
+ return get_name(node).split(".")[-1]
1476
+
1262
1477
  syms = []
1478
+ refs = []
1263
1479
  errors = []
1264
1480
 
1265
1481
  try:
@@ -1267,7 +1483,7 @@ try:
1267
1483
  tree = ast.parse(source, filename=sys.argv[1])
1268
1484
  except Exception as e:
1269
1485
  errors.append(str(e))
1270
- print("[]")
1486
+ print(json.dumps({"symbols": [], "refs": []}))
1271
1487
  sys.exit(0)
1272
1488
 
1273
1489
  # Module-level scope
@@ -1401,7 +1617,42 @@ class ModuleVisitor(ast.NodeVisitor):
1401
1617
  visitor = ModuleVisitor()
1402
1618
  visitor.visit(tree)
1403
1619
 
1404
- print(json.dumps([s.to_dict() for s in syms]))
1620
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
1621
+ # into function bodies (it would index locals as symbols), but that is exactly
1622
+ # where the calls are.
1623
+ for node in ast.walk(tree):
1624
+ if isinstance(node, ast.Call):
1625
+ name = leaf_name(node.func)
1626
+ if name:
1627
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
1628
+ elif isinstance(node, ast.Import):
1629
+ for alias in node.names:
1630
+ refs.append({
1631
+ "toName": alias.name.split(".")[-1],
1632
+ "callType": "import",
1633
+ "line": node.lineno,
1634
+ "module": alias.name,
1635
+ })
1636
+ elif isinstance(node, ast.ImportFrom):
1637
+ # PEP 328: node.level is the number of leading dots. Preserving them is
1638
+ # what lets the resolver walk up from the importing file's package \u2014
1639
+ # dropping them made \`from .foo import X\` indistinguishable from an
1640
+ # absolute \`foo\`.
1641
+ module = ("." * (node.level or 0)) + (node.module or "")
1642
+ for alias in node.names:
1643
+ refs.append({
1644
+ "toName": alias.name,
1645
+ "callType": "import",
1646
+ "line": node.lineno,
1647
+ "module": module,
1648
+ })
1649
+ elif isinstance(node, ast.ClassDef):
1650
+ for base in node.bases:
1651
+ name = leaf_name(base)
1652
+ if name:
1653
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1654
+
1655
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1405
1656
  `;
1406
1657
  _cachedScriptPath = null;
1407
1658
  }
@@ -1414,107 +1665,10 @@ __export(rs_parser_exports, {
1414
1665
  parseSymbols: () => parseSymbols5
1415
1666
  });
1416
1667
  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 path6 from "node:path";
1420
1668
  async function parseSymbols5(opts) {
1421
1669
  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
1670
  return regexParse({ file, content, lang });
1428
1671
  }
1429
- function probe(command, args) {
1430
- return new Promise((resolve2, reject) => {
1431
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
1432
- if (error) reject(error);
1433
- else resolve2();
1434
- });
1435
- });
1436
- }
1437
- function checkNativeParser() {
1438
- nativeParserAvailability ??= (async () => {
1439
- try {
1440
- await probe("rustc", ["--version"]);
1441
- const toolsDir = path6.join(process.cwd(), "tools");
1442
- await probe(
1443
- "cargo",
1444
- [
1445
- "metadata",
1446
- "--no-deps",
1447
- "--format-version",
1448
- "1",
1449
- "--manifest-path",
1450
- path6.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 = path6.join(process.cwd(), "tools");
1463
- const crateDir = path6.join(toolsDir, "syn-parser");
1464
- const tmpFile = path6.join(crateDir, "src", "input.rs");
1465
- await fs5.writeFile(tmpFile, content, "utf8");
1466
- const cargoBinary = resolveWin32Command("cargo");
1467
- const result = await new Promise(
1468
- (resolve2, reject) => {
1469
- let settled = false;
1470
- const proc = spawn3(
1471
- cargoBinary,
1472
- ["run", "--manifest-path", path6.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
- resolve2({ 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
1672
  function regexParse(opts) {
1519
1673
  const { file, content, lang } = opts;
1520
1674
  const symbols = [];
@@ -1570,12 +1724,10 @@ function regexParse(opts) {
1570
1724
  });
1571
1725
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
1572
1726
  }
1573
- var nativeParserAvailability, RS_PATTERNS;
1727
+ var RS_PATTERNS;
1574
1728
  var init_rs_parser = __esm({
1575
1729
  "src/codebase-index/rs-parser.ts"() {
1576
1730
  "use strict";
1577
- init_win32_resolve();
1578
- init_spawn_gate();
1579
1731
  init_languages();
1580
1732
  RS_PATTERNS = [
1581
1733
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -1598,7 +1750,7 @@ __export(json_parser_exports, {
1598
1750
  parseSymbols: () => parseSymbols6
1599
1751
  });
1600
1752
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1601
- import * as path7 from "node:path";
1753
+ import * as path8 from "node:path";
1602
1754
  function parseSymbols6(opts) {
1603
1755
  const { file, content, lang } = opts;
1604
1756
  try {
@@ -1610,7 +1762,7 @@ function parseSymbols6(opts) {
1610
1762
  function regexParse2(opts) {
1611
1763
  const { file, content, lang } = opts;
1612
1764
  const symbols = [];
1613
- const basename4 = path7.basename(file).toLowerCase();
1765
+ const basename4 = path8.basename(file).toLowerCase();
1614
1766
  const isPackageJson = basename4 === "package.json";
1615
1767
  const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
1616
1768
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1636,11 +1788,11 @@ function regexParse2(opts) {
1636
1788
  const line = lineFromOffset(offset);
1637
1789
  symbols.push(
1638
1790
  makeSymbol({
1639
- name: path7.basename(file),
1791
+ name: path8.basename(file),
1640
1792
  kind: "object",
1641
1793
  line,
1642
1794
  col: 0,
1643
- signature: `"${path7.basename(file)}" = { ... }`,
1795
+ signature: `"${path8.basename(file)}" = { ... }`,
1644
1796
  file,
1645
1797
  lang
1646
1798
  })
@@ -1972,7 +2124,7 @@ import { parentPort } from "node:worker_threads";
1972
2124
 
1973
2125
  // src/codebase-index/indexer.ts
1974
2126
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
1975
- import { execFile as execFile2 } from "node:child_process";
2127
+ import { execFile } from "node:child_process";
1976
2128
  import * as fs8 from "node:fs/promises";
1977
2129
  import { availableParallelism } from "node:os";
1978
2130
  import * as path11 from "node:path";
@@ -2040,8 +2192,738 @@ async function loadGitignoreMatcher(projectRoot) {
2040
2192
  // src/codebase-index/indexer.ts
2041
2193
  init_languages();
2042
2194
 
2195
+ // src/codebase-index/module-resolver.ts
2196
+ init_languages();
2197
+ import * as path4 from "node:path";
2198
+
2199
+ // src/codebase-index/module-roots.ts
2200
+ init_languages();
2201
+ import * as fs2 from "node:fs/promises";
2202
+ import * as path3 from "node:path";
2203
+ function toPortablePath(file) {
2204
+ return file.replace(/\\/g, "/");
2205
+ }
2206
+ async function readTextIfPresent(file) {
2207
+ try {
2208
+ return await fs2.readFile(file, "utf8");
2209
+ } catch {
2210
+ return void 0;
2211
+ }
2212
+ }
2213
+ function parsePackageJsonName(source) {
2214
+ try {
2215
+ const parsed = JSON.parse(source);
2216
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
2217
+ } catch {
2218
+ return void 0;
2219
+ }
2220
+ }
2221
+ function parseGoModulePath(source) {
2222
+ for (const rawLine of source.split(/\r?\n/)) {
2223
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
2224
+ const match = /^module\s+(\S+)/.exec(line);
2225
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
2226
+ }
2227
+ return void 0;
2228
+ }
2229
+ function parseTomlTableName(source, tables) {
2230
+ let current = "";
2231
+ for (const rawLine of source.split(/\r?\n/)) {
2232
+ const line = rawLine.replace(/#.*$/, "").trim();
2233
+ if (line.startsWith("[[")) {
2234
+ current = "\0";
2235
+ continue;
2236
+ }
2237
+ const table = /^\[([^\]]+)\]$/.exec(line);
2238
+ if (table?.[1]) {
2239
+ current = table[1].trim();
2240
+ continue;
2241
+ }
2242
+ if (!tables.includes(current)) continue;
2243
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
2244
+ if (match?.[1]) return match[1];
2245
+ }
2246
+ return void 0;
2247
+ }
2248
+ function parsePomArtifactId(source) {
2249
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
2250
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
2251
+ }
2252
+ var LANGS_BY_KIND = {
2253
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
2254
+ cargo: ["rs"],
2255
+ go: ["go"],
2256
+ python: ["py"],
2257
+ maven: ["java", "kotlin", "scala"],
2258
+ gradle: ["java", "kotlin", "scala"],
2259
+ dotnet: ["csharp"]
2260
+ };
2261
+ function ancestorsOf(dir, stopAt) {
2262
+ const out = [];
2263
+ let current = dir;
2264
+ for (; ; ) {
2265
+ out.push(current);
2266
+ if (current === stopAt || current.length <= stopAt.length) break;
2267
+ const parent = path3.posix.dirname(current);
2268
+ if (parent === current) break;
2269
+ current = parent;
2270
+ }
2271
+ return out;
2272
+ }
2273
+ var MARKER_PROBES = [
2274
+ {
2275
+ kind: "npm",
2276
+ file: "package.json",
2277
+ build: (dir, source) => {
2278
+ const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
2279
+ return { name, importPath: name, sourceRoots: [dir] };
2280
+ }
2281
+ },
2282
+ {
2283
+ kind: "cargo",
2284
+ file: "Cargo.toml",
2285
+ build: (dir, source) => {
2286
+ const name = parseTomlTableName(source, ["package"]);
2287
+ if (!name) return void 0;
2288
+ return {
2289
+ name: `crate:${name}`,
2290
+ // Rust paths use underscores where crate names often use dashes.
2291
+ importPath: name.replace(/-/g, "_"),
2292
+ sourceRoots: [path3.posix.join(dir, "src")]
2293
+ };
2294
+ }
2295
+ },
2296
+ {
2297
+ kind: "go",
2298
+ file: "go.mod",
2299
+ build: (dir, source) => {
2300
+ const modulePath = parseGoModulePath(source);
2301
+ if (!modulePath) return void 0;
2302
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
2303
+ }
2304
+ },
2305
+ {
2306
+ kind: "python",
2307
+ file: "pyproject.toml",
2308
+ build: (dir, source) => {
2309
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
2310
+ return {
2311
+ name: `py:${name}`,
2312
+ importPath: void 0,
2313
+ // `src/` layout is the packaging-guide default; the root itself covers
2314
+ // the flat layout. Both are probed, missing ones simply never match.
2315
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2316
+ };
2317
+ }
2318
+ },
2319
+ {
2320
+ kind: "python",
2321
+ file: "setup.py",
2322
+ build: (dir) => ({
2323
+ name: `py:${path3.posix.basename(dir)}`,
2324
+ importPath: void 0,
2325
+ sourceRoots: [path3.posix.join(dir, "src"), dir]
2326
+ })
2327
+ },
2328
+ {
2329
+ kind: "maven",
2330
+ file: "pom.xml",
2331
+ build: (dir, source) => {
2332
+ const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
2333
+ return {
2334
+ name: `mvn:${artifactId}`,
2335
+ importPath: void 0,
2336
+ sourceRoots: [
2337
+ path3.posix.join(dir, "src/main/java"),
2338
+ path3.posix.join(dir, "src/main/kotlin"),
2339
+ path3.posix.join(dir, "src/main/scala"),
2340
+ path3.posix.join(dir, "src/test/java")
2341
+ ]
2342
+ };
2343
+ }
2344
+ },
2345
+ {
2346
+ kind: "gradle",
2347
+ file: "build.gradle",
2348
+ build: (dir) => buildGradleRoot(dir)
2349
+ },
2350
+ {
2351
+ kind: "gradle",
2352
+ file: "build.gradle.kts",
2353
+ build: (dir) => buildGradleRoot(dir)
2354
+ }
2355
+ ];
2356
+ function buildGradleRoot(dir) {
2357
+ return {
2358
+ name: `gradle:${path3.posix.basename(dir)}`,
2359
+ importPath: void 0,
2360
+ sourceRoots: [
2361
+ path3.posix.join(dir, "src/main/java"),
2362
+ path3.posix.join(dir, "src/main/kotlin"),
2363
+ path3.posix.join(dir, "src/main/scala")
2364
+ ]
2365
+ };
2366
+ }
2367
+ async function probeDotnetRoot(dir) {
2368
+ let entries;
2369
+ try {
2370
+ entries = await fs2.readdir(dir);
2371
+ } catch {
2372
+ return void 0;
2373
+ }
2374
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
2375
+ if (!project) return void 0;
2376
+ const name = project.slice(0, -".csproj".length);
2377
+ return {
2378
+ dir,
2379
+ kind: "dotnet",
2380
+ name: `csproj:${name}`,
2381
+ importPath: void 0,
2382
+ sourceRoots: [dir]
2383
+ };
2384
+ }
2385
+ async function detectModuleRoots(projectRoot, files) {
2386
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
2387
+ const langsByDir = /* @__PURE__ */ new Map();
2388
+ for (const file of files) {
2389
+ const portable = toPortablePath(file);
2390
+ const lang = detectLang(portable);
2391
+ if (!lang) continue;
2392
+ const dir = path3.posix.dirname(portable);
2393
+ let langs = langsByDir.get(dir);
2394
+ if (!langs) {
2395
+ langs = /* @__PURE__ */ new Set();
2396
+ langsByDir.set(dir, langs);
2397
+ }
2398
+ langs.add(lang);
2399
+ }
2400
+ const candidates = /* @__PURE__ */ new Map();
2401
+ for (const [dir, langs] of langsByDir) {
2402
+ for (const ancestor of ancestorsOf(dir, root)) {
2403
+ let merged = candidates.get(ancestor);
2404
+ if (!merged) {
2405
+ merged = /* @__PURE__ */ new Set();
2406
+ candidates.set(ancestor, merged);
2407
+ }
2408
+ for (const lang of langs) merged.add(lang);
2409
+ }
2410
+ }
2411
+ const roots = [];
2412
+ await Promise.all(
2413
+ [...candidates].map(async ([dir, langs]) => {
2414
+ for (const probe of MARKER_PROBES) {
2415
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
2416
+ const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
2417
+ if (source === void 0) continue;
2418
+ const built = probe.build(dir, source);
2419
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
2420
+ }
2421
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
2422
+ const dotnet = await probeDotnetRoot(dir);
2423
+ if (dotnet) roots.push(dotnet);
2424
+ }
2425
+ })
2426
+ );
2427
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
2428
+ return { projectRoot: root, roots };
2429
+ }
2430
+ function findOwningRoot(structure, file, kinds) {
2431
+ const portable = toPortablePath(file);
2432
+ for (const root of structure.roots) {
2433
+ if (kinds && !kinds.includes(root.kind)) continue;
2434
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
2435
+ }
2436
+ return void 0;
2437
+ }
2438
+ function derivePackageFromLayout(filePath) {
2439
+ const portable = toPortablePath(filePath);
2440
+ const packagesIdx = portable.indexOf("/packages/");
2441
+ if (packagesIdx !== -1) {
2442
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
2443
+ if (segment) return `@wrongstack/${segment}`;
2444
+ }
2445
+ const appsIdx = portable.indexOf("/apps/");
2446
+ if (appsIdx !== -1) {
2447
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
2448
+ if (segment) return `app:${segment}`;
2449
+ }
2450
+ return void 0;
2451
+ }
2452
+ function pythonPackageLabel(structure, file, initDirs) {
2453
+ const portable = toPortablePath(file);
2454
+ const dir = path3.posix.dirname(portable);
2455
+ if (!initDirs.has(dir)) return void 0;
2456
+ const segments = [];
2457
+ let current = dir;
2458
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
2459
+ segments.unshift(path3.posix.basename(current));
2460
+ current = path3.posix.dirname(current);
2461
+ }
2462
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
2463
+ }
2464
+ function assignPackageLabels(structure, files) {
2465
+ const initDirs = /* @__PURE__ */ new Set();
2466
+ for (const file of files) {
2467
+ const portable = toPortablePath(file);
2468
+ if (path3.posix.basename(portable) === "__init__.py") {
2469
+ initDirs.add(path3.posix.dirname(portable));
2470
+ }
2471
+ }
2472
+ const labels = /* @__PURE__ */ new Map();
2473
+ for (const file of files) {
2474
+ const portable = toPortablePath(file);
2475
+ const lang = detectLang(portable);
2476
+ if (lang === "go") {
2477
+ const owner2 = findOwningRoot(structure, portable, ["go"]);
2478
+ const dir = path3.posix.dirname(portable);
2479
+ if (owner2?.importPath) {
2480
+ const relative2 = path3.posix.relative(owner2.dir, dir);
2481
+ labels.set(file, relative2 ? `${owner2.importPath}/${relative2}` : owner2.importPath);
2482
+ } else {
2483
+ labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
2484
+ }
2485
+ continue;
2486
+ }
2487
+ if (lang === "py") {
2488
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
2489
+ if (dotted) {
2490
+ labels.set(file, dotted);
2491
+ continue;
2492
+ }
2493
+ }
2494
+ const owner = findOwningRoot(structure, portable);
2495
+ const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
2496
+ labels.set(file, label);
2497
+ }
2498
+ return labels;
2499
+ }
2500
+
2501
+ // src/codebase-index/module-resolver.ts
2502
+ var EXTENSIONS = {
2503
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
2504
+ py: [".py", ".pyi"],
2505
+ rs: [".rs"],
2506
+ jvm: [".java", ".kt", ".scala"],
2507
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
2508
+ ruby: [".rb"],
2509
+ go: [".go"]
2510
+ };
2511
+ var DIRECTORY_ENTRIES = {
2512
+ js: ["index"],
2513
+ py: ["__init__"],
2514
+ rs: ["mod"],
2515
+ ruby: ["index"]
2516
+ };
2517
+ function normalizeNamespace(value) {
2518
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
2519
+ }
2520
+ var ModuleResolver = class {
2521
+ structure;
2522
+ /** Lowercased portable path → the path as indexed (case is preserved). */
2523
+ byPath;
2524
+ /** Lowercased portable directory → files directly inside it, as indexed. */
2525
+ byDir;
2526
+ /** Normalized namespace → the file declaring it (first by path, stable). */
2527
+ byNamespace;
2528
+ constructor(structure, files, namespaces = []) {
2529
+ this.structure = structure;
2530
+ this.byPath = /* @__PURE__ */ new Map();
2531
+ this.byDir = /* @__PURE__ */ new Map();
2532
+ this.byNamespace = /* @__PURE__ */ new Map();
2533
+ const dirsByKey = /* @__PURE__ */ new Map();
2534
+ for (const file of files) {
2535
+ const portable = toPortablePath(file);
2536
+ const pathKey = portable.toLowerCase();
2537
+ const priorPath = this.byPath.get(pathKey);
2538
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
2539
+ else this.byPath.set(pathKey, file);
2540
+ const dir = path4.posix.dirname(portable);
2541
+ const dirKey = dir.toLowerCase();
2542
+ const knownDir = dirsByKey.get(dirKey);
2543
+ if (knownDir === void 0) {
2544
+ dirsByKey.set(dirKey, dir);
2545
+ this.byDir.set(dirKey, [file]);
2546
+ } else if (knownDir === dir) {
2547
+ this.byDir.get(dirKey)?.push(file);
2548
+ } else {
2549
+ dirsByKey.delete(dirKey);
2550
+ this.byDir.delete(dirKey);
2551
+ }
2552
+ }
2553
+ for (const { name, file } of namespaces) {
2554
+ const lang = detectLang(file);
2555
+ if (!lang) continue;
2556
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
2557
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
2558
+ this.byNamespace.set(key, file);
2559
+ }
2560
+ }
2561
+ }
2562
+ /**
2563
+ * Resolve `specifier` as written in `fromFile`.
2564
+ * Returns the indexed target path, or `undefined` when it is external or
2565
+ * cannot be located.
2566
+ */
2567
+ resolve(fromFile, lang, specifier) {
2568
+ const spec = specifier.trim().replace(/\\/g, "/");
2569
+ if (!spec) return void 0;
2570
+ const from = toPortablePath(fromFile);
2571
+ switch (languageFamily(lang)) {
2572
+ case "js":
2573
+ return this.resolveJs(from, spec);
2574
+ case "go":
2575
+ return this.resolveGo(spec);
2576
+ case "py":
2577
+ return this.resolvePython(from, spec);
2578
+ case "rs":
2579
+ return this.resolveRust(from, spec);
2580
+ case "jvm":
2581
+ return this.resolveJvm(spec);
2582
+ case "c":
2583
+ return this.resolveInclude(from, spec);
2584
+ case "ruby":
2585
+ return this.resolveRuby(from, spec);
2586
+ case "dotnet":
2587
+ case "php":
2588
+ case "elixir":
2589
+ case "haskell":
2590
+ return this.resolveNamespace(lang, spec);
2591
+ default:
2592
+ return void 0;
2593
+ }
2594
+ }
2595
+ /**
2596
+ * Resolve a namespace specifier to the file declaring it.
2597
+ *
2598
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
2599
+ * names a namespace outright, while PHP's `use App\Models\User` names a
2600
+ * *class* inside `App\Models`, so the prefix is what was declared.
2601
+ */
2602
+ resolveNamespace(lang, spec) {
2603
+ const family = languageFamily(lang);
2604
+ const normalized = normalizeNamespace(spec);
2605
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
2606
+ if (exact) return exact;
2607
+ const segments = normalized.split(".").filter(Boolean);
2608
+ if (segments.length < 2) return void 0;
2609
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
2610
+ }
2611
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
2612
+ lookup(candidate) {
2613
+ return this.byPath.get(path4.posix.normalize(candidate).toLowerCase());
2614
+ }
2615
+ /**
2616
+ * Try `base` verbatim, then `base` + each extension, then each directory
2617
+ * entry point inside `base`.
2618
+ */
2619
+ lookupWithExtensions(base, family) {
2620
+ const direct = this.lookup(base);
2621
+ if (direct) return direct;
2622
+ const extensions = EXTENSIONS[family] ?? [];
2623
+ const suffix = path4.posix.extname(base);
2624
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
2625
+ for (const ext of extensions) {
2626
+ const hit = this.lookup(`${stem}${ext}`);
2627
+ if (hit) return hit;
2628
+ }
2629
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
2630
+ for (const ext of extensions) {
2631
+ const hit = this.lookup(path4.posix.join(base, `${entry}${ext}`));
2632
+ if (hit) return hit;
2633
+ }
2634
+ }
2635
+ return void 0;
2636
+ }
2637
+ /**
2638
+ * A representative indexed file inside `dir`, for ecosystems whose import
2639
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
2640
+ *
2641
+ * The choice is deterministic — a file named after the directory, else the
2642
+ * first by name — so the same import always produces the same edge. Package
2643
+ * grouping is unaffected either way: every file in the directory carries the
2644
+ * same package label, so the package-level edge is exact regardless of which
2645
+ * member represents it.
2646
+ */
2647
+ representativeIn(dir, family) {
2648
+ const members = this.byDir.get(path4.posix.normalize(dir).toLowerCase());
2649
+ if (!members?.length) return void 0;
2650
+ const extensions = EXTENSIONS[family] ?? [];
2651
+ const eligible = members.filter((file) => extensions.includes(path4.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
2652
+ if (eligible.length === 0) return void 0;
2653
+ const base = path4.posix.basename(path4.posix.normalize(dir)).toLowerCase();
2654
+ const named = eligible.find(
2655
+ (file) => path4.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
2656
+ );
2657
+ return named ?? eligible[0];
2658
+ }
2659
+ // ─── Per-family resolution ──────────────────────────────────────────────────
2660
+ /** Relative specifiers, then workspace package names and their subpaths. */
2661
+ resolveJs(fromFile, spec) {
2662
+ if (spec.startsWith(".")) {
2663
+ const absolute = path4.posix.join(path4.posix.dirname(fromFile), spec);
2664
+ return this.lookupWithExtensions(absolute, "js");
2665
+ }
2666
+ const owner = this.structure.roots.find(
2667
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2668
+ );
2669
+ if (!owner?.importPath) return void 0;
2670
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2671
+ if (!subpath) {
2672
+ return this.lookupWithExtensions(path4.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "index"), "js");
2673
+ }
2674
+ return this.lookupWithExtensions(path4.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "src", subpath), "js");
2675
+ }
2676
+ /** Go import paths are absolute module paths; a package is a directory. */
2677
+ resolveGo(spec) {
2678
+ const owner = this.structure.roots.find(
2679
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
2680
+ );
2681
+ if (!owner?.importPath) return void 0;
2682
+ const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
2683
+ return this.representativeIn(path4.posix.join(owner.dir, subpath), "go");
2684
+ }
2685
+ /**
2686
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
2687
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
2688
+ */
2689
+ resolvePython(fromFile, spec) {
2690
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
2691
+ if (leadingDots > 0) {
2692
+ let base = path4.posix.dirname(fromFile);
2693
+ for (let i = 1; i < leadingDots; i++) base = path4.posix.dirname(base);
2694
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
2695
+ return this.lookupWithExtensions(path4.posix.join(base, ...rest), "py");
2696
+ }
2697
+ const segments = spec.split(".").filter(Boolean);
2698
+ if (segments.length === 0) return void 0;
2699
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
2700
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2701
+ const hit = this.lookupWithExtensions(path4.posix.join(base, ...segments), "py");
2702
+ if (hit) return hit;
2703
+ if (segments.length > 1) {
2704
+ const parent = this.lookupWithExtensions(
2705
+ path4.posix.join(base, ...segments.slice(0, -1)),
2706
+ "py"
2707
+ );
2708
+ if (parent) return parent;
2709
+ }
2710
+ }
2711
+ return void 0;
2712
+ }
2713
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
2714
+ resolveRust(fromFile, spec) {
2715
+ const segments = spec.split("::").filter(Boolean);
2716
+ if (segments.length === 0) return void 0;
2717
+ const head = segments[0];
2718
+ if (head === "self" || head === "super") {
2719
+ let base = path4.posix.dirname(fromFile);
2720
+ for (const segment of segments) {
2721
+ if (segment === "super") base = path4.posix.dirname(base);
2722
+ else if (segment !== "self") break;
2723
+ }
2724
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
2725
+ return this.lookupWithExtensions(path4.posix.join(base, ...rest2), "rs");
2726
+ }
2727
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
2728
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
2729
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
2730
+ );
2731
+ if (!crate) {
2732
+ return this.lookupWithExtensions(
2733
+ path4.posix.join(path4.posix.dirname(fromFile), ...segments),
2734
+ "rs"
2735
+ );
2736
+ }
2737
+ const rest = segments.slice(1);
2738
+ for (const base of crate.sourceRoots) {
2739
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path4.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
2740
+ const exact = this.lookupWithExtensions(path4.posix.join(base, ...rest), "rs");
2741
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path4.posix.join(base, "lib"), "rs");
2742
+ if (hit) return hit;
2743
+ }
2744
+ return void 0;
2745
+ }
2746
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
2747
+ resolveJvm(spec) {
2748
+ const segments = spec.split(".").filter(Boolean);
2749
+ if (segments.length === 0) return void 0;
2750
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
2751
+ const wildcard = segments[segments.length - 1] === "*";
2752
+ const parts = wildcard ? segments.slice(0, -1) : segments;
2753
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
2754
+ const target = path4.posix.join(base, ...parts);
2755
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
2756
+ if (hit) return hit;
2757
+ }
2758
+ return void 0;
2759
+ }
2760
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
2761
+ resolveInclude(fromFile, spec) {
2762
+ const relative2 = this.lookupWithExtensions(
2763
+ path4.posix.join(path4.posix.dirname(fromFile), spec),
2764
+ "c"
2765
+ );
2766
+ if (relative2) return relative2;
2767
+ for (const base of [
2768
+ path4.posix.join(this.structure.projectRoot, "include"),
2769
+ this.structure.projectRoot
2770
+ ]) {
2771
+ const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "c");
2772
+ if (hit) return hit;
2773
+ }
2774
+ return void 0;
2775
+ }
2776
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
2777
+ resolveRuby(fromFile, spec) {
2778
+ const relative2 = this.lookupWithExtensions(
2779
+ path4.posix.join(path4.posix.dirname(fromFile), spec),
2780
+ "ruby"
2781
+ );
2782
+ if (relative2) return relative2;
2783
+ for (const base of [
2784
+ path4.posix.join(this.structure.projectRoot, "lib"),
2785
+ this.structure.projectRoot
2786
+ ]) {
2787
+ const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "ruby");
2788
+ if (hit) return hit;
2789
+ }
2790
+ return void 0;
2791
+ }
2792
+ };
2793
+
2794
+ // src/codebase-index/import-extractor.ts
2795
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
2796
+ var IMPORT_MAX_PER_FILE = 400;
2797
+ var DOTTED_IMPORT = [
2798
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
2799
+ ];
2800
+ var LANG_IMPORTS = {
2801
+ // Go and Python have real AST extractors; these patterns are the fallback for
2802
+ // machines with no Go toolchain or Python interpreter installed, where the
2803
+ // parser degrades to regex symbols and would otherwise contribute no edges.
2804
+ go: [
2805
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
2806
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
2807
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
2808
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
2809
+ ],
2810
+ py: [
2811
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
2812
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
2813
+ ],
2814
+ rs: [
2815
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
2816
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
2817
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
2818
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
2819
+ ],
2820
+ java: DOTTED_IMPORT,
2821
+ kotlin: DOTTED_IMPORT,
2822
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
2823
+ csharp: [
2824
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
2825
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
2826
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
2827
+ ],
2828
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
2829
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2830
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
2831
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
2832
+ php: [
2833
+ // `use A\B\C` imports the class C, which is what the index has a symbol
2834
+ // for — the namespace symbol only covers the `A\B` prefix.
2835
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
2836
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
2837
+ ],
2838
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
2839
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
2840
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
2841
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
2842
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
2843
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
2844
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
2845
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
2846
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
2847
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
2848
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
2849
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
2850
+ html: [
2851
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
2852
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
2853
+ ],
2854
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
2855
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
2856
+ };
2857
+ function lastSegment(specifier) {
2858
+ const pathLike = /[/\\]|::/.test(specifier);
2859
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
2860
+ let last = segments[segments.length - 1] ?? specifier;
2861
+ if (last === "*" || last === "_") {
2862
+ last = segments[segments.length - 2] ?? specifier;
2863
+ }
2864
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
2865
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
2866
+ return dotted[dotted.length - 1] ?? last;
2867
+ }
2868
+ function newlineOffsets(content) {
2869
+ const offsets = [];
2870
+ for (let i = 0; i < content.length; i++) {
2871
+ if (content.charCodeAt(i) === 10) offsets.push(i);
2872
+ }
2873
+ return offsets;
2874
+ }
2875
+ function lineAt(offsets, index) {
2876
+ let low = 0;
2877
+ let high = offsets.length;
2878
+ while (low < high) {
2879
+ const mid = low + high >>> 1;
2880
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
2881
+ else high = mid;
2882
+ }
2883
+ return low + 1;
2884
+ }
2885
+ function hasImportPatterns(lang) {
2886
+ return LANG_IMPORTS[lang] !== void 0;
2887
+ }
2888
+ function extractImports(opts) {
2889
+ const patterns = LANG_IMPORTS[opts.lang];
2890
+ if (!patterns || !opts.content) return [];
2891
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
2892
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
2893
+ const refs = [];
2894
+ const seen = /* @__PURE__ */ new Set();
2895
+ const offsets = newlineOffsets(content);
2896
+ for (const pattern of patterns) {
2897
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
2898
+ for (const match of content.matchAll(re)) {
2899
+ if (refs.length >= limit) return refs;
2900
+ const specifier = match[1]?.trim();
2901
+ if (!specifier) continue;
2902
+ const module = specifier;
2903
+ const toName = pattern.name === "full" ? module : lastSegment(module);
2904
+ if (!toName) continue;
2905
+ const key = `${module}\0${toName}`;
2906
+ if (seen.has(key)) continue;
2907
+ seen.add(key);
2908
+ refs.push({
2909
+ fromId: 0,
2910
+ toName,
2911
+ callType: "import",
2912
+ line: lineAt(offsets, match.index ?? 0),
2913
+ lang: opts.lang,
2914
+ module
2915
+ });
2916
+ }
2917
+ }
2918
+ return refs;
2919
+ }
2920
+
2043
2921
  // src/codebase-index/parser-dispatch.ts
2044
2922
  async function parseFileContent(file, content, lang) {
2923
+ const parsed = await dispatch(file, content, lang);
2924
+ return withRelations(parsed, content, lang);
2925
+ }
2926
+ async function dispatch(file, content, lang) {
2045
2927
  switch (lang) {
2046
2928
  case "ts":
2047
2929
  case "tsx":
@@ -2076,6 +2958,13 @@ async function parseFileContent(file, content, lang) {
2076
2958
  }
2077
2959
  }
2078
2960
  }
2961
+ function withRelations(parsed, content, lang) {
2962
+ let refs = parsed.refs ?? [];
2963
+ if (refs.length === 0 && hasImportPatterns(lang)) {
2964
+ refs = extractImports({ content, lang });
2965
+ }
2966
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
2967
+ }
2079
2968
 
2080
2969
  // src/codebase-index/writer.ts
2081
2970
  import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
@@ -2171,6 +3060,9 @@ var Bm25Index = class {
2171
3060
  }
2172
3061
  };
2173
3062
 
3063
+ // src/codebase-index/writer.ts
3064
+ init_languages();
3065
+
2174
3066
  // src/codebase-index/lsp-kind.ts
2175
3067
  function lspKindToInternalKind(k) {
2176
3068
  switch (k) {
@@ -2205,7 +3097,7 @@ function lspKindToInternalKind(k) {
2205
3097
  }
2206
3098
 
2207
3099
  // src/codebase-index/schema.ts
2208
- var SCHEMA_VERSION = 3;
3100
+ var SCHEMA_VERSION = 4;
2209
3101
 
2210
3102
  // src/codebase-index/sqlite-runtime.ts
2211
3103
  import { createRequire } from "node:module";
@@ -2352,7 +3244,7 @@ function runSqliteWithRetry(fn) {
2352
3244
 
2353
3245
  // src/codebase-index/writer-admin.ts
2354
3246
  import * as fs6 from "node:fs";
2355
- import * as path8 from "node:path";
3247
+ import * as path9 from "node:path";
2356
3248
  var DB_FILE = "index.db";
2357
3249
  function getAllIndexableWithStatement(stmt) {
2358
3250
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -2411,7 +3303,7 @@ function getAllFileMetasWithStatement(stmt) {
2411
3303
  }
2412
3304
  function getIndexDbSizeBytes(indexDir) {
2413
3305
  try {
2414
- return fs6.statSync(path8.join(indexDir, DB_FILE)).size;
3306
+ return fs6.statSync(path9.join(indexDir, DB_FILE)).size;
2415
3307
  } catch {
2416
3308
  return 0;
2417
3309
  }
@@ -2462,49 +3354,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
2462
3354
  }
2463
3355
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2464
3356
  if (refs.length === 0) return;
2465
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
3357
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
2466
3358
  for (let i = 0; i < refs.length; i += chunkSize) {
2467
3359
  const chunk = refs.slice(i, i + chunkSize);
2468
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
3360
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
2469
3361
  const insert = stmt(
2470
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
3362
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
3363
+ VALUES ${placeholders}`
2471
3364
  );
2472
3365
  const binds = [];
2473
3366
  for (const ref of chunk) {
2474
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
3367
+ binds.push(
3368
+ ref.fromId,
3369
+ ref.toName,
3370
+ ref.toId ?? null,
3371
+ ref.callType,
3372
+ ref.line,
3373
+ ref.lang ?? "",
3374
+ ref.module ?? null,
3375
+ ref.toFile ?? null
3376
+ );
2475
3377
  }
2476
3378
  insert.run(...binds);
2477
3379
  }
2478
3380
  }
2479
3381
 
3382
+ // src/codebase-index/writer-graph-reader.ts
3383
+ init_languages();
3384
+
2480
3385
  // src/codebase-index/writer-graph-helpers.ts
2481
- import * as path9 from "node:path";
2482
- function derivePackage(filePath) {
2483
- const f = filePath.replace(/\\/g, "/");
2484
- const pkgsIdx = f.indexOf("/packages/");
2485
- if (pkgsIdx !== -1) {
2486
- const rest = f.slice(pkgsIdx + "/packages/".length);
2487
- const seg = rest.split("/")[0];
2488
- return seg ? `@wrongstack/${seg}` : void 0;
2489
- }
2490
- const appsIdx = f.indexOf("/apps/");
2491
- if (appsIdx !== -1) {
2492
- const rest = f.slice(appsIdx + "/apps/".length);
2493
- const seg = rest.split("/")[0];
2494
- return seg ? `app:${seg}` : void 0;
2495
- }
2496
- return void 0;
2497
- }
2498
- function packageFromImport(moduleName) {
2499
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
2500
- const parts = moduleName.split("/");
2501
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
3386
+ function createPackageLabeller(stored) {
3387
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
2502
3388
  }
2503
- function buildPackageGraphNodes(fileCounts, files) {
3389
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
2504
3390
  const pkgNodes = /* @__PURE__ */ new Map();
2505
3391
  const fileToPkg = /* @__PURE__ */ new Map();
2506
3392
  for (const { file, n } of fileCounts) {
2507
- const pkg = derivePackage(file) ?? "(root)";
3393
+ const pkg = packageOf(file);
2508
3394
  fileToPkg.set(file, pkg);
2509
3395
  const node = pkgNodes.get(pkg);
2510
3396
  if (node) {
@@ -2521,7 +3407,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2521
3407
  }
2522
3408
  }
2523
3409
  for (const { file } of files) {
2524
- const pkg = derivePackage(file) ?? "(root)";
3410
+ const pkg = packageOf(file);
2525
3411
  fileToPkg.set(file, pkg);
2526
3412
  const node = pkgNodes.get(pkg);
2527
3413
  if (node) {
@@ -2539,7 +3425,7 @@ function buildPackageGraphNodes(fileCounts, files) {
2539
3425
  }
2540
3426
  return { pkgNodes, fileToPkg };
2541
3427
  }
2542
- function buildFileGraphNodeState(pkgSyms, localFiles) {
3428
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
2543
3429
  const fileNodes = /* @__PURE__ */ new Map();
2544
3430
  const symToFile = /* @__PURE__ */ new Map();
2545
3431
  const fileStats = /* @__PURE__ */ new Map();
@@ -2558,7 +3444,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2558
3444
  id: `file:${file}`,
2559
3445
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
2560
3446
  kind: "file",
2561
- package: derivePackage(file) ?? "(root)",
3447
+ package: packageOf(file),
2562
3448
  file,
2563
3449
  symbolCount: stats?.count ?? 0,
2564
3450
  lang: stats?.lang,
@@ -2570,7 +3456,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
2570
3456
  }
2571
3457
  return { fileNodes, symToFile, fileStats, ensureFileNode };
2572
3458
  }
2573
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
3459
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
2574
3460
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
2575
3461
  const aExternal = a.file === fileFilter ? 0 : 1;
2576
3462
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -2582,7 +3468,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2582
3468
  symbolId: s.id,
2583
3469
  symbolKind: s.kind,
2584
3470
  file: s.file,
2585
- package: derivePackage(s.file) ?? "(root)",
3471
+ package: packageOf(s.file),
2586
3472
  lang: s.lang,
2587
3473
  line: s.line,
2588
3474
  signature: s.signature,
@@ -2590,29 +3476,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2590
3476
  external: s.file !== fileFilter
2591
3477
  }));
2592
3478
  }
2593
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
2594
- if (!moduleName.startsWith(".")) return void 0;
2595
- const normalizedFrom = fromFile.replace(/\\/g, "/");
2596
- const absolute = path9.posix.normalize(
2597
- path9.posix.join(path9.posix.dirname(normalizedFrom), moduleName)
2598
- );
2599
- const extension = path9.posix.extname(absolute);
2600
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
2601
- const candidates = [
2602
- absolute,
2603
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
2604
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(absolute, `index${ext}`)),
2605
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(base, `index${ext}`))
2606
- ];
2607
- const indexedByPortablePath = new Map(
2608
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
2609
- );
2610
- for (const candidate of candidates) {
2611
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
2612
- if (indexed) return indexed;
2613
- }
2614
- return void 0;
2615
- }
2616
3479
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
2617
3480
  const key = `${source}\0${target}`;
2618
3481
  let edge = edgeMap.get(key);
@@ -2653,11 +3516,146 @@ function mapWriterRefRow(row) {
2653
3516
  toName: row.to_name,
2654
3517
  toId: row.to_id ?? void 0,
2655
3518
  callType: row.call_type,
2656
- line: row.line
3519
+ line: row.line,
3520
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
3521
+ // queries select; `undefined` keeps those rows valid Refs.
3522
+ lang: row.lang || void 0,
3523
+ module: row.module ?? void 0,
3524
+ toFile: row.to_file ?? void 0
2657
3525
  };
2658
3526
  }
2659
3527
 
2660
3528
  // src/codebase-index/writer-graph-reader.ts
3529
+ var MAX_SQL_VARS = 900;
3530
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
3531
+ const results = [];
3532
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
3533
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
3534
+ const placeholders = chunk.map(() => "?").join(",");
3535
+ const sql = buildSql(placeholders);
3536
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
3537
+ }
3538
+ return results;
3539
+ }
3540
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
3541
+ let total = 0;
3542
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
3543
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
3544
+ const placeholders = chunk.map(() => "?").join(",");
3545
+ const sql = buildSql(placeholders);
3546
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
3547
+ total += rows[0]?.n ?? 0;
3548
+ }
3549
+ return total;
3550
+ }
3551
+ function mapCallSiteRow(row) {
3552
+ return {
3553
+ symbol: {
3554
+ id: row.sym_id,
3555
+ name: row.sym_name,
3556
+ kind: row.sym_kind,
3557
+ lang: row.sym_lang,
3558
+ file: row.sym_file,
3559
+ line: row.sym_line,
3560
+ signature: row.sym_signature
3561
+ },
3562
+ callType: row.call_type,
3563
+ line: row.ref_line
3564
+ };
3565
+ }
3566
+ function resolveSymbolIds(stmt, symbolName, file) {
3567
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
3568
+ const args = file ? [symbolName, file] : [symbolName];
3569
+ const rows = stmt(baseSql).all(...args);
3570
+ return rows.map((r) => r.id);
3571
+ }
3572
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
3573
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
3574
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
3575
+ let matchIds = targetIds;
3576
+ let ambiguous = false;
3577
+ if (file !== void 0) {
3578
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
3579
+ if (allNamedIds.length > targetIds.length) {
3580
+ matchIds = allNamedIds;
3581
+ ambiguous = true;
3582
+ }
3583
+ }
3584
+ const useFallback = !file;
3585
+ const rows = chunkedIdQuery(
3586
+ stmt,
3587
+ matchIds,
3588
+ (ph) => `SELECT
3589
+ s.id AS sym_id,
3590
+ s.name AS sym_name,
3591
+ s.kind AS sym_kind,
3592
+ s.lang AS sym_lang,
3593
+ s.file AS sym_file,
3594
+ s.line AS sym_line,
3595
+ s.signature AS sym_signature,
3596
+ r.call_type,
3597
+ r.line AS ref_line
3598
+ FROM refs r
3599
+ JOIN symbols s ON s.id = r.from_id
3600
+ WHERE r.to_id IN (${ph})
3601
+ ORDER BY r.line, r.id`,
3602
+ []
3603
+ );
3604
+ if (useFallback) {
3605
+ const fallbackRows = stmt(
3606
+ `SELECT
3607
+ s.id AS sym_id,
3608
+ s.name AS sym_name,
3609
+ s.kind AS sym_kind,
3610
+ s.lang AS sym_lang,
3611
+ s.file AS sym_file,
3612
+ s.line AS sym_line,
3613
+ s.signature AS sym_signature,
3614
+ r.call_type,
3615
+ r.line AS ref_line
3616
+ FROM refs r
3617
+ JOIN symbols s ON s.id = r.from_id
3618
+ WHERE r.to_id IS NULL AND r.to_name = ?
3619
+ ORDER BY r.line, r.id`
3620
+ ).all(symbolName);
3621
+ rows.push(...fallbackRows);
3622
+ }
3623
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
3624
+ const allCalls = rows.map(mapCallSiteRow);
3625
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
3626
+ }
3627
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
3628
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
3629
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
3630
+ const unresolvedCount = chunkedIdScalar(
3631
+ stmt,
3632
+ sourceIds,
3633
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
3634
+ );
3635
+ const rows = chunkedIdQuery(
3636
+ stmt,
3637
+ sourceIds,
3638
+ (ph) => `SELECT
3639
+ s.id AS sym_id,
3640
+ s.name AS sym_name,
3641
+ s.kind AS sym_kind,
3642
+ s.lang AS sym_lang,
3643
+ s.file AS sym_file,
3644
+ s.line AS sym_line,
3645
+ s.signature AS sym_signature,
3646
+ r.call_type,
3647
+ r.line AS ref_line
3648
+ FROM refs r
3649
+ JOIN symbols s ON s.id = r.to_id
3650
+ WHERE r.from_id IN (${ph})
3651
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
3652
+ ORDER BY r.line, r.id`,
3653
+ []
3654
+ );
3655
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
3656
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
3657
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
3658
+ }
2661
3659
  function findRefsToWithStatement(stmt, symbolId) {
2662
3660
  return stmt(
2663
3661
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
@@ -2671,7 +3669,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
2671
3669
  function getPackageGraphWithStatement(stmt) {
2672
3670
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
2673
3671
  const files = stmt("SELECT DISTINCT file FROM files").all();
2674
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
3672
+ const packageOf = readPackageLabeller(stmt);
3673
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
2675
3674
  const refRows = stmt(
2676
3675
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
2677
3676
  FROM refs r
@@ -2682,32 +3681,42 @@ function getPackageGraphWithStatement(stmt) {
2682
3681
  ).all();
2683
3682
  const edgeMap = /* @__PURE__ */ new Map();
2684
3683
  for (const r of refRows) {
2685
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2686
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
3684
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3685
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
2687
3686
  if (fromPkg === toPkg) continue;
2688
3687
  const n = Number(r.n) || 0;
2689
3688
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
2690
3689
  }
2691
3690
  const importRows = stmt(
2692
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
3691
+ `SELECT s.file AS from_file,
3692
+ COALESCE(r.to_file, st.file) AS to_file,
3693
+ COUNT(*) AS n
2693
3694
  FROM refs r
2694
3695
  JOIN symbols s ON s.id = r.from_id
3696
+ LEFT JOIN symbols st ON st.id = r.to_id
2695
3697
  WHERE r.call_type = 'import'
2696
- GROUP BY r.to_name, s.file`
3698
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
3699
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
2697
3700
  ).all();
2698
3701
  for (const r of importRows) {
2699
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
2700
- const toPkg = packageFromImport(r.to_name);
2701
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
3702
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
3703
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
3704
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
2702
3705
  const n = Number(r.n) || 0;
2703
3706
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
2704
3707
  }
2705
3708
  const edges = materializeWeightedEdges(edgeMap, "pkg");
2706
3709
  return { nodes: [...pkgNodes.values()], edges };
2707
3710
  }
3711
+ function readPackageLabeller(stmt) {
3712
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
3713
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
3714
+ }
2708
3715
  function getFileGraphWithStatement(stmt, packageFilter) {
2709
3716
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
2710
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
3717
+ const packageOf = readPackageLabeller(stmt);
3718
+ const langOf = (file) => detectLang(file) ?? "other";
3719
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
2711
3720
  const localFiles = new Set(pkgFilePaths);
2712
3721
  if (localFiles.size === 0) return { nodes: [], edges: [] };
2713
3722
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -2716,9 +3725,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2716
3725
  ).all(...pkgFilePaths);
2717
3726
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
2718
3727
  pkgSyms,
2719
- localFiles
3728
+ localFiles,
3729
+ packageOf
2720
3730
  );
2721
- const indexedFiles = new Set(allFiles.map((f) => f.file));
2722
3731
  const refRows = stmt(
2723
3732
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
2724
3733
  FROM refs r
@@ -2741,7 +3750,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2741
3750
  for (const x of extras) {
2742
3751
  symToFile.set(x.id, x.file);
2743
3752
  if (!fileStats.has(x.file)) {
2744
- fileStats.set(x.file, { count: 0, lang: "ts" });
3753
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
2745
3754
  }
2746
3755
  }
2747
3756
  }
@@ -2758,17 +3767,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
2758
3767
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
2759
3768
  }
2760
3769
  const importRows = stmt(
2761
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
3770
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
2762
3771
  FROM refs r
3772
+ LEFT JOIN symbols st ON st.id = r.to_id
2763
3773
  WHERE r.call_type = 'import'
3774
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
2764
3775
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
2765
- GROUP BY r.from_id, r.to_name`
3776
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
2766
3777
  ).all(...pkgFilePaths);
2767
3778
  for (const r of importRows) {
2768
3779
  const fromFile = symToFile.get(r.from_id);
2769
3780
  if (!fromFile || !localFiles.has(fromFile)) continue;
2770
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
3781
+ const toFile = r.to_file;
2771
3782
  if (!toFile || fromFile === toFile) continue;
3783
+ if (!fileStats.has(toFile)) {
3784
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
3785
+ }
2772
3786
  ensureFileNode(fromFile);
2773
3787
  ensureFileNode(toFile);
2774
3788
  const n = Number(r.n) || 0;
@@ -2818,7 +3832,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
2818
3832
  ).all(...missingIds);
2819
3833
  for (const s of extras) symById.set(s.id, s);
2820
3834
  }
2821
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
3835
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
2822
3836
  return { nodes, edges };
2823
3837
  }
2824
3838
 
@@ -2840,7 +3854,7 @@ function assignRefsToSymbols(refs, symbols) {
2840
3854
  }
2841
3855
  if (!owner && ref.callType === "import") owner = ordered[0];
2842
3856
  if (!owner || owner.id <= 0) continue;
2843
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3857
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
2844
3858
  if (seen.has(key)) continue;
2845
3859
  seen.add(key);
2846
3860
  assigned.push({ ...ref, fromId: owner.id });
@@ -2882,7 +3896,11 @@ var CORE_TABLES_SQL = `
2882
3896
  lang TEXT NOT NULL,
2883
3897
  mtime_ms INTEGER NOT NULL,
2884
3898
  symbol_count INTEGER NOT NULL DEFAULT 0,
2885
- last_indexed INTEGER NOT NULL
3899
+ last_indexed INTEGER NOT NULL,
3900
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
3901
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
3902
+ -- re-derived per query because the evidence lives on disk, not in the DB.
3903
+ package TEXT NOT NULL DEFAULT ''
2886
3904
  );
2887
3905
  CREATE TABLE IF NOT EXISTS symbols (
2888
3906
  id INTEGER PRIMARY KEY,
@@ -2899,6 +3917,9 @@ var CORE_TABLES_SQL = `
2899
3917
  file_fk TEXT NOT NULL
2900
3918
  );
2901
3919
  `;
3920
+ var FILE_INDEX_SQL = [
3921
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
3922
+ ];
2902
3923
  var SYMBOL_INDEX_SQL = [
2903
3924
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
2904
3925
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -2915,15 +3936,32 @@ var REFS_TABLE_SQL = `
2915
3936
  to_name TEXT NOT NULL,
2916
3937
  to_id INTEGER,
2917
3938
  call_type TEXT NOT NULL,
2918
- line INTEGER NOT NULL
3939
+ line INTEGER NOT NULL,
3940
+ lang TEXT NOT NULL DEFAULT '',
3941
+ module TEXT,
3942
+ to_file TEXT
2919
3943
  );
2920
3944
  `;
2921
3945
  var REFS_INDEX_SQL = [
2922
3946
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
2923
3947
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
2924
3948
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
2925
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
3949
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
3950
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
3951
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
3952
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
3953
+ // The post-index module resolution pass groups unresolved import refs by
3954
+ // (module, lang); graph readers then read to_file back.
3955
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
3956
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
2926
3957
  ];
3958
+ var LANG_FAMILY_TABLE_SQL = `
3959
+ CREATE TABLE IF NOT EXISTS lang_family (
3960
+ lang TEXT PRIMARY KEY,
3961
+ family TEXT NOT NULL
3962
+ );
3963
+ `;
3964
+ var LANG_FAMILY_WILDCARD = "*";
2927
3965
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2928
3966
 
2929
3967
  // src/codebase-index/writer-search-helpers.ts
@@ -3144,6 +4182,60 @@ var IndexStore = class _IndexStore {
3144
4182
  runWithRetry(fn) {
3145
4183
  return runSqliteWithRetry(fn);
3146
4184
  }
4185
+ /**
4186
+ * Mirror the in-process language→family map into SQLite.
4187
+ *
4188
+ * Rewritten on every open rather than only on schema bumps: the mapping is
4189
+ * static lookup data, so a code-side change (a new language, a language
4190
+ * moving families) must take effect without forcing a full reindex.
4191
+ */
4192
+ seedLangFamilies() {
4193
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
4194
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
4195
+ insert.run("", LANG_FAMILY_WILDCARD);
4196
+ }
4197
+ /**
4198
+ * Add any column the current schema expects but the on-disk table lacks.
4199
+ *
4200
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
4201
+ * and the version check above only rebuilds on a version *mismatch*. That
4202
+ * leaves a real gap: several wstack processes share this database, and while
4203
+ * a version upgrade is rolling out one of them may still be running the
4204
+ * previous build. That older process sees the newer version number, drops the
4205
+ * tables, and recreates them from *its* DDL — without the newer columns —
4206
+ * while the metadata row still reads the new version. Every later query for
4207
+ * one of those columns then fails with `no such column`, and no amount of
4208
+ * reindexing fixes it, because the version numbers already agree.
4209
+ *
4210
+ * Repairing column-by-column makes the schema self-healing from any of those
4211
+ * states. Table and column names are compile-time literals from this module,
4212
+ * never user input.
4213
+ */
4214
+ repairMissingColumns() {
4215
+ const expected = [
4216
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
4217
+ {
4218
+ table: "refs",
4219
+ columns: [
4220
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
4221
+ ["module", "TEXT"],
4222
+ ["to_file", "TEXT"]
4223
+ ]
4224
+ }
4225
+ ];
4226
+ for (const { table, columns } of expected) {
4227
+ const present = new Set(
4228
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
4229
+ (row) => typeof row.name === "string" ? [row.name] : []
4230
+ )
4231
+ );
4232
+ if (present.size === 0) continue;
4233
+ for (const [name, type] of columns) {
4234
+ if (present.has(name)) continue;
4235
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
4236
+ }
4237
+ }
4238
+ }
3147
4239
  initSchema() {
3148
4240
  this.db.exec(METADATA_TABLE_SQL);
3149
4241
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -3166,9 +4258,13 @@ var IndexStore = class _IndexStore {
3166
4258
  );
3167
4259
  }
3168
4260
  this.db.exec(CORE_TABLES_SQL);
3169
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3170
4261
  this.db.exec(REFS_TABLE_SQL);
4262
+ this.repairMissingColumns();
4263
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
4264
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
3171
4265
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
4266
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
4267
+ this.seedLangFamilies();
3172
4268
  try {
3173
4269
  this.db.exec(SYMBOLS_FTS_SQL);
3174
4270
  this.ftsAvailable = true;
@@ -3203,6 +4299,18 @@ var IndexStore = class _IndexStore {
3203
4299
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
3204
4300
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
3205
4301
  static MAX_SQL_VARS = 900;
4302
+ /**
4303
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
4304
+ * `sym` belong to the same language family — or the ref carries no language,
4305
+ * in which case the wildcard bind matches everything.
4306
+ *
4307
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
4308
+ */
4309
+ static FAMILY_MATCH_SQL = `(
4310
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
4311
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
4312
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
4313
+ )`;
3206
4314
  /**
3207
4315
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
3208
4316
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -3266,9 +4374,12 @@ var IndexStore = class _IndexStore {
3266
4374
  const placeholders = chunk.map(() => "?").join(",");
3267
4375
  const result = this.stmt(
3268
4376
  `UPDATE refs
3269
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
4377
+ SET to_id = (
4378
+ SELECT MIN(sym.id) FROM symbols sym
4379
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
4380
+ )
3270
4381
  WHERE to_name IN (${placeholders})`
3271
- ).run(...chunk);
4382
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
3272
4383
  changes += result.changes ?? 0;
3273
4384
  }
3274
4385
  return changes;
@@ -3395,6 +4506,115 @@ var IndexStore = class _IndexStore {
3395
4506
  getAllFileMetas() {
3396
4507
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
3397
4508
  }
4509
+ // ─── Project structure & module resolution ──────────────────────────────────
4510
+ /** Store the Code Atlas grouping label for each indexed file. */
4511
+ setFilePackages(entries) {
4512
+ if (entries.size === 0) return;
4513
+ this.runWithRetry(() => {
4514
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
4515
+ for (const [file, label] of entries) update.run(label, file);
4516
+ });
4517
+ }
4518
+ /**
4519
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
4520
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
4521
+ * Ordered so the resolver's choice among duplicate declarations is stable.
4522
+ */
4523
+ getNamespaceDeclarations() {
4524
+ return this.stmt(
4525
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
4526
+ ).all();
4527
+ }
4528
+ /** `file → package` for every indexed file that has a label. */
4529
+ getFilePackages() {
4530
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
4531
+ return new Map(rows.map((row) => [row.file, row.package]));
4532
+ }
4533
+ /**
4534
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
4535
+ *
4536
+ * Distinct rather than per-ref because resolution depends only on these three
4537
+ * values: a file importing the same module twenty times resolves it once.
4538
+ */
4539
+ getUnresolvedImports(onlyFiles) {
4540
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
4541
+ FROM refs r
4542
+ JOIN symbols s ON s.id = r.from_id
4543
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
4544
+ if (!onlyFiles?.length) {
4545
+ return this.stmt(base).all();
4546
+ }
4547
+ const out = [];
4548
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
4549
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
4550
+ const placeholders = chunk.map(() => "?").join(",");
4551
+ out.push(
4552
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
4553
+ );
4554
+ }
4555
+ return out;
4556
+ }
4557
+ /**
4558
+ * Write resolved import targets back onto `refs.to_file`.
4559
+ *
4560
+ * Applied through a temp table and a single UPDATE: one statement per
4561
+ * resolution would mean thousands of round-trips on a first index.
4562
+ */
4563
+ applyImportResolutions(resolutions) {
4564
+ if (resolutions.length === 0) return 0;
4565
+ return this.runWithRetry(() => {
4566
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4567
+ this.db.exec(
4568
+ `CREATE TEMP TABLE import_resolution (
4569
+ from_file TEXT NOT NULL,
4570
+ lang TEXT NOT NULL,
4571
+ module TEXT NOT NULL,
4572
+ to_file TEXT NOT NULL
4573
+ )`
4574
+ );
4575
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
4576
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
4577
+ const chunk = resolutions.slice(i, i + chunkSize);
4578
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
4579
+ const binds = [];
4580
+ for (const entry of chunk) {
4581
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
4582
+ }
4583
+ this.stmt(
4584
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
4585
+ VALUES ${placeholders}`
4586
+ ).run(...binds);
4587
+ }
4588
+ this.db.exec(
4589
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
4590
+ ON import_resolution(module, lang, from_file)`
4591
+ );
4592
+ const result = this.stmt(
4593
+ `UPDATE refs
4594
+ SET to_file = (
4595
+ SELECT ir.to_file
4596
+ FROM temp.import_resolution ir
4597
+ JOIN symbols s ON s.id = refs.from_id
4598
+ WHERE ir.module = refs.module
4599
+ AND ir.lang = refs.lang
4600
+ AND ir.from_file = s.file
4601
+ LIMIT 1
4602
+ )
4603
+ WHERE refs.call_type = 'import'
4604
+ AND refs.module IS NOT NULL
4605
+ AND EXISTS (
4606
+ SELECT 1
4607
+ FROM temp.import_resolution ir
4608
+ JOIN symbols s ON s.id = refs.from_id
4609
+ WHERE ir.module = refs.module
4610
+ AND ir.lang = refs.lang
4611
+ AND ir.from_file = s.file
4612
+ )`
4613
+ ).run();
4614
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
4615
+ return result.changes ?? 0;
4616
+ });
4617
+ }
3398
4618
  // ─── Search ──────────────────────────────────────────────────────────────────
3399
4619
  search(query, filter, opts) {
3400
4620
  const built = this.buildSearchWhere(query, filter);
@@ -3781,9 +5001,12 @@ var IndexStore = class _IndexStore {
3781
5001
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
3782
5002
  * Call this after all symbols have been inserted to fill in cross-references.
3783
5003
  *
3784
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
3785
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
3786
- * that found a targetmatching the previous per-row loop's return value.
5004
+ * A match additionally requires the referencing ref and the target symbol to
5005
+ * be in the same {@link LangFamily}. Without that guard a name match is a
5006
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
5007
+ * `Config` are declared in most languages at once, and each collision draws a
5008
+ * Code Atlas edge between files that never reference each other. Refs stored
5009
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
3787
5010
  */
3788
5011
  resolveRefs() {
3789
5012
  return this.runWithRetry(() => {
@@ -3792,20 +5015,35 @@ var IndexStore = class _IndexStore {
3792
5015
  `UPDATE refs
3793
5016
  SET to_id = s.id
3794
5017
  FROM (
3795
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
3796
- ) AS s
5018
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5019
+ FROM symbols sym
5020
+ JOIN lang_family lf ON lf.lang = sym.lang
5021
+ GROUP BY sym.name, lf.family
5022
+ UNION ALL
5023
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5024
+ FROM symbols sym
5025
+ GROUP BY sym.name
5026
+ ) AS s,
5027
+ lang_family AS rf
3797
5028
  WHERE refs.to_id IS NULL
3798
5029
  AND refs.to_name IS NOT NULL
3799
- AND refs.to_name = s.name`
5030
+ AND rf.lang = refs.lang
5031
+ AND s.name = refs.to_name
5032
+ AND s.family = rf.family`
3800
5033
  ).run();
3801
5034
  return result.changes ?? 0;
3802
5035
  } catch {
3803
5036
  const result = this.stmt(
3804
5037
  `UPDATE refs SET to_id = (
3805
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
5038
+ SELECT sym.id FROM symbols sym
5039
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5040
+ ORDER BY sym.id LIMIT 1
3806
5041
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
3807
- AND to_name IN (SELECT name FROM symbols)`
3808
- ).run();
5042
+ AND EXISTS (
5043
+ SELECT 1 FROM symbols sym
5044
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5045
+ )`
5046
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
3809
5047
  return result.changes ?? 0;
3810
5048
  }
3811
5049
  });
@@ -3889,6 +5127,20 @@ var IndexStore = class _IndexStore {
3889
5127
  return false;
3890
5128
  }
3891
5129
  }
5130
+ /**
5131
+ * Find all symbols that reference the named target symbol (incoming callers).
5132
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
5133
+ */
5134
+ findIncomingCallsByName(symbolName, file, limit = 100) {
5135
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
5136
+ }
5137
+ /**
5138
+ * Find all symbols that the named source symbol references (outgoing callees).
5139
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
5140
+ */
5141
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
5142
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
5143
+ }
3892
5144
  /**
3893
5145
  * Find all references TO a given symbol (who calls / uses this symbol?).
3894
5146
  */
@@ -4009,7 +5261,7 @@ function normalizeComparablePath(value) {
4009
5261
  }
4010
5262
  function gitOutput(projectRoot, args) {
4011
5263
  return new Promise((resolve2, reject) => {
4012
- execFile2(
5264
+ execFile(
4013
5265
  "git",
4014
5266
  ["-C", projectRoot, ...args],
4015
5267
  {
@@ -4140,13 +5392,40 @@ function assignRefsToSymbols2(refs, symbols) {
4140
5392
  }
4141
5393
  if (!owner && ref.callType === "import") owner = ordered[0];
4142
5394
  if (!owner || owner.id <= 0) continue;
4143
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
5395
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4144
5396
  if (seen.has(key)) continue;
4145
5397
  seen.add(key);
4146
5398
  assigned.push({ ...ref, fromId: owner.id });
4147
5399
  }
4148
5400
  return assigned;
4149
5401
  }
5402
+ async function resolveProjectRelations(store, projectRoot, opts) {
5403
+ if (opts.signal?.aborted) return;
5404
+ try {
5405
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
5406
+ if (indexedFiles.length === 0) return;
5407
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
5408
+ if (opts.signal?.aborted) return;
5409
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
5410
+ const resolver = new ModuleResolver(
5411
+ structure,
5412
+ indexedFiles,
5413
+ store.getNamespaceDeclarations()
5414
+ );
5415
+ const pending = store.getUnresolvedImports(opts.onlyFiles);
5416
+ const resolutions = [];
5417
+ for (const entry of pending) {
5418
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
5419
+ if (toFile && toFile !== entry.fromFile) {
5420
+ resolutions.push({ ...entry, toFile });
5421
+ }
5422
+ }
5423
+ if (opts.signal?.aborted) return;
5424
+ store.applyImportResolutions(resolutions);
5425
+ } catch (err) {
5426
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
5427
+ }
5428
+ }
4150
5429
  async function runIndexerWithStore(store, opts) {
4151
5430
  const { projectRoot, langs, ignore = [], signal } = opts;
4152
5431
  const relationGraphVersion = "2";
@@ -4391,6 +5670,14 @@ async function runIndexerWithStore(store, opts) {
4391
5670
  }
4392
5671
  }
4393
5672
  if (needsFullRefResolution) store.resolveRefs();
5673
+ await resolveProjectRelations(store, projectRoot, {
5674
+ // A watcher run re-resolves only what it touched; a full run (or a contract
5675
+ // bump) re-resolves everything, because a newly indexed file can be the
5676
+ // target of imports written long before it.
5677
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
5678
+ errors,
5679
+ signal
5680
+ });
4394
5681
  store.setMetadata("ref_resolution_version", refResolutionVersion);
4395
5682
  store.setMetadata("relation_graph_version", relationGraphVersion);
4396
5683
  if (!opts.files || filesIndexed >= 50) store.optimize();
@@ -4473,6 +5760,22 @@ function symbolGraphService(args) {
4473
5760
  indexStorePool.release(store);
4474
5761
  }
4475
5762
  }
5763
+ function incomingCallsService(args) {
5764
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5765
+ try {
5766
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
5767
+ } finally {
5768
+ indexStorePool.release(store);
5769
+ }
5770
+ }
5771
+ function outgoingCallsService(args) {
5772
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5773
+ try {
5774
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
5775
+ } finally {
5776
+ indexStorePool.release(store);
5777
+ }
5778
+ }
4476
5779
 
4477
5780
  // src/codebase-index/worker.ts
4478
5781
  if (!parentPort) throw new Error("codebase-index worker must be started as a worker thread");
@@ -4481,7 +5784,7 @@ var inFlight = /* @__PURE__ */ new Map();
4481
5784
  function post(msg) {
4482
5785
  port.postMessage(msg);
4483
5786
  }
4484
- async function dispatch(msg) {
5787
+ async function dispatch2(msg) {
4485
5788
  switch (msg.op) {
4486
5789
  case "index": {
4487
5790
  const ac = new AbortController();
@@ -4505,6 +5808,10 @@ async function dispatch(msg) {
4505
5808
  return fileGraphService(msg.args);
4506
5809
  case "symbolGraph":
4507
5810
  return symbolGraphService(msg.args);
5811
+ case "incomingCalls":
5812
+ return incomingCallsService(msg.args);
5813
+ case "outgoingCalls":
5814
+ return outgoingCallsService(msg.args);
4508
5815
  default:
4509
5816
  throw new Error(`unknown index op: ${msg.op}`);
4510
5817
  }
@@ -4514,7 +5821,7 @@ port.on("message", (msg) => {
4514
5821
  inFlight.get(msg.id)?.abort(new Error("Indexing cancelled"));
4515
5822
  return;
4516
5823
  }
4517
- void dispatch(msg).then(
5824
+ void dispatch2(msg).then(
4518
5825
  (result) => post({ type: "response", id: msg.id, ok: true, result }),
4519
5826
  (err) => {
4520
5827
  try {