@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
package/dist/builtin.js CHANGED
@@ -721,7 +721,7 @@ var init_process_registry = __esm({
721
721
  const p = this.processes.get(pid);
722
722
  if (!p) return false;
723
723
  if (p.killed) return true;
724
- if (p.protected) return false;
724
+ if (p.protected && opts.includeProtected !== true) return false;
725
725
  if (opts.preserveBackground && p.background) return false;
726
726
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
727
727
  const isWin5 = os.platform() === "win32";
@@ -772,9 +772,13 @@ var init_process_registry = __esm({
772
772
  killAll(opts = {}) {
773
773
  const pids = Array.from(this.processes.keys());
774
774
  const killed = [];
775
+ const includeProtected = opts.includeProtected === true;
775
776
  for (const pid of pids) {
776
777
  const p = this.processes.get(pid);
777
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
778
+ if (!p) continue;
779
+ if (p.protected && !includeProtected) continue;
780
+ if (opts.preserveBackground && p.background) continue;
781
+ if (this.kill(pid, opts)) killed.push(pid);
778
782
  }
779
783
  return killed;
780
784
  }
@@ -1138,18 +1142,18 @@ async function resolveRealInsideRoot(absPath, ctx) {
1138
1142
  const realRoots = await Promise.all(
1139
1143
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1140
1144
  );
1141
- let probe2 = absPath;
1145
+ let probe = absPath;
1142
1146
  const pendingTail = [];
1143
1147
  for (; ; ) {
1144
1148
  let real;
1145
1149
  try {
1146
- real = await fsp2.realpath(probe2);
1150
+ real = await fsp2.realpath(probe);
1147
1151
  } catch (err) {
1148
1152
  if (err.code === "ENOENT") {
1149
- const parent = path3.dirname(probe2);
1150
- if (parent === probe2) return absPath;
1151
- pendingTail.unshift(path3.basename(probe2));
1152
- probe2 = parent;
1153
+ const parent = path3.dirname(probe);
1154
+ if (parent === probe) return absPath;
1155
+ pendingTail.unshift(path3.basename(probe));
1156
+ probe = parent;
1153
1157
  continue;
1154
1158
  }
1155
1159
  throw err;
@@ -4968,20 +4972,23 @@ var init_legacy_bridge = __esm({
4968
4972
  });
4969
4973
 
4970
4974
  // src/codebase-index/languages.ts
4971
- import * as path18 from "node:path";
4975
+ import * as path13 from "node:path";
4972
4976
  function detectLang(file) {
4973
- const base = path18.basename(file);
4977
+ const base = path13.basename(file);
4974
4978
  const lowerBase = base.toLowerCase();
4975
4979
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4976
4980
  return "ts";
4977
4981
  }
4978
4982
  const special = SPECIAL_FILENAMES[lowerBase];
4979
4983
  if (special) return special;
4980
- const ext = path18.extname(base).toLowerCase();
4984
+ const ext = path13.extname(base).toLowerCase();
4981
4985
  if (!ext) return null;
4982
4986
  return EXT_TO_LANG[ext] ?? null;
4983
4987
  }
4984
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
4988
+ function languageFamily(lang) {
4989
+ return LANG_FAMILY[lang] ?? "other";
4990
+ }
4991
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
4985
4992
  var init_languages2 = __esm({
4986
4993
  "src/codebase-index/languages.ts"() {
4987
4994
  "use strict";
@@ -5072,6 +5079,52 @@ var init_languages2 = __esm({
5072
5079
  procfile: "other",
5073
5080
  justfile: "other"
5074
5081
  };
5082
+ LANG_FAMILY = {
5083
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
5084
+ // imports from — and is imported by — plain .ts files.
5085
+ ts: "js",
5086
+ tsx: "js",
5087
+ js: "js",
5088
+ jsx: "js",
5089
+ vue: "js",
5090
+ svelte: "js",
5091
+ go: "go",
5092
+ py: "py",
5093
+ rs: "rs",
5094
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
5095
+ java: "jvm",
5096
+ kotlin: "jvm",
5097
+ scala: "jvm",
5098
+ csharp: "dotnet",
5099
+ // A .h header is consumed by both C and C++ translation units.
5100
+ c: "c",
5101
+ cpp: "c",
5102
+ ruby: "ruby",
5103
+ php: "php",
5104
+ swift: "swift",
5105
+ dart: "dart",
5106
+ elixir: "elixir",
5107
+ haskell: "haskell",
5108
+ zig: "zig",
5109
+ lua: "lua",
5110
+ r: "r",
5111
+ shell: "shell",
5112
+ sql: "sql",
5113
+ json: "data",
5114
+ yaml: "data",
5115
+ toml: "data",
5116
+ html: "web",
5117
+ css: "web",
5118
+ proto: "proto",
5119
+ graphql: "graphql",
5120
+ md: "other",
5121
+ other: "other"
5122
+ };
5123
+ LANG_FAMILY_ENTRIES = Object.freeze(
5124
+ Object.entries(LANG_FAMILY).map(
5125
+ ([lang, family]) => Object.freeze([lang, family])
5126
+ )
5127
+ );
5075
5128
  }
5076
5129
  });
5077
5130
 
@@ -5236,7 +5289,7 @@ function getTypeName(name) {
5236
5289
  function deduplicateRefs(refs) {
5237
5290
  const seen = /* @__PURE__ */ new Set();
5238
5291
  return refs.filter((r) => {
5239
- const key = `${r.toName}:${r.callType}:${r.line}`;
5292
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
5240
5293
  if (seen.has(key)) return false;
5241
5294
  seen.add(key);
5242
5295
  return true;
@@ -5246,10 +5299,16 @@ function getImportSpecifierName(spec) {
5246
5299
  return spec.propertyName?.text ?? spec.name.text;
5247
5300
  }
5248
5301
  function emitImportSpecifierRefs(node, refs, lineNum) {
5302
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5249
5303
  const clause = node.importClause;
5250
- if (!clause) return;
5304
+ if (!clause) {
5305
+ if (module) {
5306
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5307
+ }
5308
+ return;
5309
+ }
5251
5310
  if (clause.name) {
5252
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5311
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5253
5312
  }
5254
5313
  const bindings = clause.namedBindings;
5255
5314
  if (!bindings) return;
@@ -5259,26 +5318,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
5259
5318
  fromId: 0,
5260
5319
  toName: getImportSpecifierName(element),
5261
5320
  callType: "import",
5262
- line: lineNum
5321
+ line: lineNum,
5322
+ module
5263
5323
  });
5264
5324
  }
5265
5325
  } else if (ts.isNamespaceImport(bindings)) {
5266
- refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
5326
+ refs.push({
5327
+ fromId: 0,
5328
+ toName: bindings.name.text,
5329
+ callType: "import",
5330
+ line: lineNum,
5331
+ module
5332
+ });
5267
5333
  }
5268
5334
  }
5335
+ function moduleSpecifierOf(node) {
5336
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
5337
+ }
5269
5338
  function emitExportSpecifierRefs(node, refs, lineNum) {
5339
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5270
5340
  const clause = node.exportClause;
5271
5341
  if (clause && ts.isNamespaceExport(clause)) {
5272
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5342
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5273
5343
  return;
5274
5344
  }
5275
5345
  if (clause && ts.isNamedExports(clause)) {
5276
5346
  for (const element of clause.elements) {
5277
5347
  const originalName = element.propertyName?.text ?? element.name.text;
5278
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
5348
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
5279
5349
  }
5280
5350
  return;
5281
5351
  }
5352
+ if (module) {
5353
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5354
+ }
5282
5355
  }
5283
5356
  var ts, tsLoad, kindMapCache;
5284
5357
  var init_ts_parser = __esm({
@@ -5290,6 +5363,82 @@ var init_ts_parser = __esm({
5290
5363
  }
5291
5364
  });
5292
5365
 
5366
+ // src/codebase-index/parser-output.ts
5367
+ function coerceSymbols(value) {
5368
+ if (!Array.isArray(value)) return [];
5369
+ return value.flatMap((entry) => {
5370
+ const candidate = entry;
5371
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
5372
+ return [
5373
+ {
5374
+ name: candidate.name,
5375
+ kind: candidate.kind,
5376
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5377
+ col: typeof candidate.col === "number" ? candidate.col : 0,
5378
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
5379
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
5380
+ }
5381
+ ];
5382
+ });
5383
+ }
5384
+ function coerceRefs(value, lang) {
5385
+ if (!Array.isArray(value)) return [];
5386
+ return value.flatMap((entry) => {
5387
+ const candidate = entry;
5388
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
5389
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
5390
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
5391
+ return [
5392
+ {
5393
+ fromId: 0,
5394
+ toName: candidate.toName,
5395
+ callType: candidate.callType,
5396
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5397
+ lang,
5398
+ module
5399
+ }
5400
+ ];
5401
+ });
5402
+ }
5403
+ function parseParserOutput(stdout, lang) {
5404
+ const trimmed = stdout.trim();
5405
+ if (!trimmed) return { symbols: [], refs: [] };
5406
+ let parsed;
5407
+ try {
5408
+ parsed = JSON.parse(trimmed);
5409
+ } catch {
5410
+ return { symbols: [], refs: [] };
5411
+ }
5412
+ if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
5413
+ const record = parsed;
5414
+ return {
5415
+ symbols: coerceSymbols(record.symbols),
5416
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
5417
+ };
5418
+ }
5419
+ function dedupeRefs(refs) {
5420
+ const seen = /* @__PURE__ */ new Set();
5421
+ return refs.filter((ref) => {
5422
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
5423
+ if (seen.has(key)) return false;
5424
+ seen.add(key);
5425
+ return true;
5426
+ });
5427
+ }
5428
+ var CALL_TYPES;
5429
+ var init_parser_output = __esm({
5430
+ "src/codebase-index/parser-output.ts"() {
5431
+ "use strict";
5432
+ CALL_TYPES = /* @__PURE__ */ new Set([
5433
+ "call",
5434
+ "type_ref",
5435
+ "inherit",
5436
+ "implement",
5437
+ "import"
5438
+ ]);
5439
+ }
5440
+ });
5441
+
5293
5442
  // src/codebase-index/spawn-gate.ts
5294
5443
  function withSpawnGate(fn) {
5295
5444
  const run = chain.then(fn, fn);
@@ -5315,8 +5464,8 @@ __export(go_parser_exports, {
5315
5464
  });
5316
5465
  import { spawn as spawn5 } from "node:child_process";
5317
5466
  import * as os6 from "node:os";
5318
- import * as path19 from "node:path";
5319
- import * as fs14 from "node:fs/promises";
5467
+ import * as path20 from "node:path";
5468
+ import * as fs15 from "node:fs/promises";
5320
5469
  async function parseSymbols2(opts) {
5321
5470
  const { file, content, lang } = opts;
5322
5471
  try {
@@ -5324,7 +5473,8 @@ async function parseSymbols2(opts) {
5324
5473
  if (parsed.symbols.length > 0) {
5325
5474
  return parsed;
5326
5475
  }
5327
- return fallbackParse(file, content, lang);
5476
+ const fallback = fallbackParse(file, content, lang);
5477
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
5328
5478
  } catch {
5329
5479
  return fallbackParse(file, content, lang);
5330
5480
  }
@@ -5388,9 +5538,9 @@ async function syncGoParse(filePath, content, lang) {
5388
5538
  try {
5389
5539
  let scriptPath = _cachedGoScriptPath;
5390
5540
  if (!scriptPath) {
5391
- const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5392
- scriptPath = path19.join(tmpDir, "parse.go");
5393
- await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5541
+ const tmpDir = await fs15.mkdtemp(path20.join(os6.tmpdir(), "ws-go-parse-"));
5542
+ scriptPath = path20.join(tmpDir, "parse.go");
5543
+ await fs15.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5394
5544
  _cachedGoScriptPath = scriptPath;
5395
5545
  }
5396
5546
  const goBinary = resolveWin32Command("go");
@@ -5432,8 +5582,8 @@ async function syncGoParse(filePath, content, lang) {
5432
5582
  if (code !== 0 || !stdout.trim()) {
5433
5583
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5434
5584
  }
5435
- const raw = JSON.parse(stdout.trim());
5436
- const symbols = raw.map((s) => ({
5585
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
5586
+ const symbols = rawSymbols.map((s) => ({
5437
5587
  id: 0,
5438
5588
  lang,
5439
5589
  kind: s.kind,
@@ -5446,7 +5596,7 @@ async function syncGoParse(filePath, content, lang) {
5446
5596
  scope: s.scope ?? "",
5447
5597
  text: `${s.name} ${s.signature ?? ""}`.trim()
5448
5598
  }));
5449
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
5599
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
5450
5600
  } catch {
5451
5601
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5452
5602
  }
@@ -5456,6 +5606,7 @@ var init_go_parser = __esm({
5456
5606
  "src/codebase-index/go-parser.ts"() {
5457
5607
  "use strict";
5458
5608
  init_win32_resolve();
5609
+ init_parser_output();
5459
5610
  init_spawn_gate();
5460
5611
  init_languages2();
5461
5612
  GO_PARSE_SCRIPT = `
@@ -5469,6 +5620,7 @@ import (
5469
5620
  "go/token"
5470
5621
  "io"
5471
5622
  "os"
5623
+ "strconv"
5472
5624
  "strings"
5473
5625
  )
5474
5626
 
@@ -5481,16 +5633,34 @@ type Sym struct {
5481
5633
  Scope string \`json:"scope"\`
5482
5634
  }
5483
5635
 
5636
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
5637
+ // yields both. Module is the import path for CallType "import", else empty.
5638
+ type Ref struct {
5639
+ ToName string \`json:"toName"\`
5640
+ CallType string \`json:"callType"\`
5641
+ Line int \`json:"line"\`
5642
+ Module string \`json:"module"\`
5643
+ }
5644
+
5645
+ type Result struct {
5646
+ Symbols []Sym \`json:"symbols"\`
5647
+ Refs []Ref \`json:"refs"\`
5648
+ }
5649
+
5650
+ func emptyResult() string {
5651
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
5652
+ }
5653
+
5484
5654
  func main() {
5485
5655
  src, err := io.ReadAll(os.Stdin)
5486
5656
  if err != nil {
5487
- fmt.Print("[]")
5657
+ fmt.Print(emptyResult())
5488
5658
  return
5489
5659
  }
5490
5660
  fset := token.NewFileSet()
5491
5661
  node, err := parser.ParseFile(fset, "src.go", src, 0)
5492
5662
  if err != nil {
5493
- fmt.Print("[]")
5663
+ fmt.Print(emptyResult())
5494
5664
  return
5495
5665
  }
5496
5666
 
@@ -5554,9 +5724,43 @@ func main() {
5554
5724
  }
5555
5725
  }
5556
5726
 
5557
- data, err := json.Marshal(syms)
5727
+ refs := []Ref{}
5728
+ ast.Inspect(node, func(n ast.Node) bool {
5729
+ switch expr := n.(type) {
5730
+ case *ast.CallExpr:
5731
+ line := fset.Position(expr.Pos()).Line
5732
+ switch fun := expr.Fun.(type) {
5733
+ case *ast.Ident:
5734
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
5735
+ case *ast.SelectorExpr:
5736
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
5737
+ // declared symbol name, so it resolves the same way the TypeScript
5738
+ // and Python extractors' call refs do.
5739
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
5740
+ }
5741
+ case *ast.ImportSpec:
5742
+ if expr.Path != nil {
5743
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
5744
+ line := fset.Position(expr.Pos()).Line
5745
+ // A Go import names a package, not a symbol; the package's
5746
+ // last path segment is the name it is referenced by.
5747
+ name := importPath
5748
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
5749
+ name = importPath[idx+1:]
5750
+ }
5751
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
5752
+ }
5753
+ }
5754
+ }
5755
+ return true
5756
+ })
5757
+
5758
+ if syms == nil {
5759
+ syms = []Sym{}
5760
+ }
5761
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
5558
5762
  if err != nil {
5559
- fmt.Print("[]")
5763
+ fmt.Print(emptyResult())
5560
5764
  return
5561
5765
  }
5562
5766
  fmt.Print(string(data))
@@ -5908,9 +6112,13 @@ var init_generic_parser = __esm({
5908
6112
  ],
5909
6113
  elixir: [
5910
6114
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
5911
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
6115
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
6116
+ // against this symbol, and a `Foo`-only capture never matches it.
6117
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
5912
6118
  ],
5913
6119
  haskell: [
6120
+ // Target of `import Data.List`.
6121
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
5914
6122
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
5915
6123
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
5916
6124
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -6001,9 +6209,9 @@ __export(py_parser_exports, {
6001
6209
  parseSymbols: () => parseSymbols4
6002
6210
  });
6003
6211
  import { spawn as spawn6 } from "node:child_process";
6004
- import * as fs15 from "node:fs/promises";
6212
+ import * as fs16 from "node:fs/promises";
6005
6213
  import * as os7 from "node:os";
6006
- import * as path20 from "node:path";
6214
+ import * as path21 from "node:path";
6007
6215
  async function parseSymbols4(opts) {
6008
6216
  const { file, content, lang } = opts;
6009
6217
  try {
@@ -6081,10 +6289,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6081
6289
  async function syncPyParse(filePath, content, lang) {
6082
6290
  try {
6083
6291
  if (!_cachedScriptPath) {
6084
- const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6085
- await fs15.mkdir(tmpDir, { recursive: true });
6086
- _cachedScriptPath = path20.join(tmpDir, "parse.py");
6087
- await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6292
+ const tmpDir = path21.join(os7.tmpdir(), "ws-py-parse");
6293
+ await fs16.mkdir(tmpDir, { recursive: true });
6294
+ _cachedScriptPath = path21.join(tmpDir, "parse.py");
6295
+ await fs16.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6088
6296
  }
6089
6297
  cachedPyBinary ??= resolvePython();
6090
6298
  const pyBinary = await cachedPyBinary;
@@ -6098,7 +6306,7 @@ async function syncPyParse(filePath, content, lang) {
6098
6306
  if (code !== 0 || !stdout.trim()) {
6099
6307
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
6100
6308
  }
6101
- const raw = JSON.parse(stdout.trim());
6309
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
6102
6310
  const symbols = raw.map((s) => ({
6103
6311
  id: 0,
6104
6312
  lang,
@@ -6112,7 +6320,7 @@ async function syncPyParse(filePath, content, lang) {
6112
6320
  scope: s.scope ?? "",
6113
6321
  text: `${s.name} ${s.signature ?? ""}`.trim()
6114
6322
  }));
6115
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
6323
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
6116
6324
  } catch {
6117
6325
  return null;
6118
6326
  }
@@ -6123,6 +6331,7 @@ var init_py_parser = __esm({
6123
6331
  "use strict";
6124
6332
  init_win32_resolve();
6125
6333
  init_generic_parser();
6334
+ init_parser_output();
6126
6335
  init_spawn_gate();
6127
6336
  init_languages2();
6128
6337
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -6184,7 +6393,18 @@ class Sym:
6184
6393
  def is_private(name):
6185
6394
  return name.startswith("__") and not name.endswith("__")
6186
6395
 
6396
+ def leaf_name(node):
6397
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
6398
+ # TypeScript and Go extractors record call refs, so resolution behaves the
6399
+ # same across languages.
6400
+ if isinstance(node, ast.Attribute):
6401
+ return node.attr
6402
+ if isinstance(node, ast.Name):
6403
+ return node.id
6404
+ return get_name(node).split(".")[-1]
6405
+
6187
6406
  syms = []
6407
+ refs = []
6188
6408
  errors = []
6189
6409
 
6190
6410
  try:
@@ -6192,7 +6412,7 @@ try:
6192
6412
  tree = ast.parse(source, filename=sys.argv[1])
6193
6413
  except Exception as e:
6194
6414
  errors.append(str(e))
6195
- print("[]")
6415
+ print(json.dumps({"symbols": [], "refs": []}))
6196
6416
  sys.exit(0)
6197
6417
 
6198
6418
  # Module-level scope
@@ -6326,7 +6546,42 @@ class ModuleVisitor(ast.NodeVisitor):
6326
6546
  visitor = ModuleVisitor()
6327
6547
  visitor.visit(tree)
6328
6548
 
6329
- print(json.dumps([s.to_dict() for s in syms]))
6549
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
6550
+ # into function bodies (it would index locals as symbols), but that is exactly
6551
+ # where the calls are.
6552
+ for node in ast.walk(tree):
6553
+ if isinstance(node, ast.Call):
6554
+ name = leaf_name(node.func)
6555
+ if name:
6556
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
6557
+ elif isinstance(node, ast.Import):
6558
+ for alias in node.names:
6559
+ refs.append({
6560
+ "toName": alias.name.split(".")[-1],
6561
+ "callType": "import",
6562
+ "line": node.lineno,
6563
+ "module": alias.name,
6564
+ })
6565
+ elif isinstance(node, ast.ImportFrom):
6566
+ # PEP 328: node.level is the number of leading dots. Preserving them is
6567
+ # what lets the resolver walk up from the importing file's package \u2014
6568
+ # dropping them made \`from .foo import X\` indistinguishable from an
6569
+ # absolute \`foo\`.
6570
+ module = ("." * (node.level or 0)) + (node.module or "")
6571
+ for alias in node.names:
6572
+ refs.append({
6573
+ "toName": alias.name,
6574
+ "callType": "import",
6575
+ "line": node.lineno,
6576
+ "module": module,
6577
+ })
6578
+ elif isinstance(node, ast.ClassDef):
6579
+ for base in node.bases:
6580
+ name = leaf_name(base)
6581
+ if name:
6582
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
6583
+
6584
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
6330
6585
  `;
6331
6586
  _cachedScriptPath = null;
6332
6587
  }
@@ -6339,107 +6594,10 @@ __export(rs_parser_exports, {
6339
6594
  parseSymbols: () => parseSymbols5
6340
6595
  });
6341
6596
  import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6342
- import { execFile, spawn as spawn7 } from "node:child_process";
6343
- import * as fs16 from "node:fs/promises";
6344
- import * as path21 from "node:path";
6345
6597
  async function parseSymbols5(opts) {
6346
6598
  const { file, content, lang } = opts;
6347
- const nativeAvailable = await checkNativeParser();
6348
- if (nativeAvailable) {
6349
- const result = await withSpawnGate(() => tryNativeParse(file, content));
6350
- if (result) return result;
6351
- }
6352
6599
  return regexParse({ file, content, lang });
6353
6600
  }
6354
- function probe(command, args) {
6355
- return new Promise((resolve16, reject) => {
6356
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
6357
- if (error) reject(error);
6358
- else resolve16();
6359
- });
6360
- });
6361
- }
6362
- function checkNativeParser() {
6363
- nativeParserAvailability ??= (async () => {
6364
- try {
6365
- await probe("rustc", ["--version"]);
6366
- const toolsDir = path21.join(process.cwd(), "tools");
6367
- await probe(
6368
- "cargo",
6369
- [
6370
- "metadata",
6371
- "--no-deps",
6372
- "--format-version",
6373
- "1",
6374
- "--manifest-path",
6375
- path21.join(toolsDir, "Cargo.toml")
6376
- ]
6377
- );
6378
- return true;
6379
- } catch {
6380
- return false;
6381
- }
6382
- })();
6383
- return nativeParserAvailability;
6384
- }
6385
- async function tryNativeParse(file, content) {
6386
- try {
6387
- const toolsDir = path21.join(process.cwd(), "tools");
6388
- const crateDir = path21.join(toolsDir, "syn-parser");
6389
- const tmpFile = path21.join(crateDir, "src", "input.rs");
6390
- await fs16.writeFile(tmpFile, content, "utf8");
6391
- const cargoBinary = resolveWin32Command("cargo");
6392
- const result = await new Promise(
6393
- (resolve16, reject) => {
6394
- let settled = false;
6395
- const proc = spawn7(
6396
- cargoBinary,
6397
- ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6398
- {
6399
- cwd: process.cwd(),
6400
- stdio: ["pipe", "pipe", "pipe"],
6401
- windowsHide: true
6402
- }
6403
- );
6404
- proc.on("error", (err) => {
6405
- if (settled) return;
6406
- settled = true;
6407
- reject(err);
6408
- });
6409
- let stdout2 = "";
6410
- proc.stdout?.on("data", (chunk) => {
6411
- stdout2 += chunk.toString();
6412
- });
6413
- proc.stderr?.resume();
6414
- const timer = setTimeout(() => {
6415
- if (settled) return;
6416
- settled = true;
6417
- proc.kill("SIGKILL");
6418
- reject(new Error("timeout"));
6419
- }, 15e3);
6420
- timer.unref?.();
6421
- proc.on("close", (c) => {
6422
- if (settled) return;
6423
- settled = true;
6424
- clearTimeout(timer);
6425
- resolve16({ code: c, stdout: stdout2 });
6426
- });
6427
- }
6428
- );
6429
- const { code, stdout } = result;
6430
- if (code === 0 && stdout.trim()) {
6431
- const symbols = JSON.parse(stdout.trim());
6432
- return {
6433
- file,
6434
- lang: "rs",
6435
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
6436
- mtimeMs: Date.now()
6437
- };
6438
- }
6439
- } catch {
6440
- }
6441
- return null;
6442
- }
6443
6601
  function regexParse(opts) {
6444
6602
  const { file, content, lang } = opts;
6445
6603
  const symbols = [];
@@ -6495,12 +6653,10 @@ function regexParse(opts) {
6495
6653
  });
6496
6654
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
6497
6655
  }
6498
- var nativeParserAvailability, RS_PATTERNS;
6656
+ var RS_PATTERNS;
6499
6657
  var init_rs_parser = __esm({
6500
6658
  "src/codebase-index/rs-parser.ts"() {
6501
6659
  "use strict";
6502
- init_win32_resolve();
6503
- init_spawn_gate();
6504
6660
  init_languages2();
6505
6661
  RS_PATTERNS = [
6506
6662
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -7082,6 +7238,10 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
7082
7238
  const start = Date.now();
7083
7239
  const pidStr = String(process.pid);
7084
7240
  const hostStr = os2.hostname();
7241
+ try {
7242
+ await fs6.mkdir(path9.dirname(lockfilePath), { recursive: true });
7243
+ } catch {
7244
+ }
7085
7245
  while (Date.now() - start < timeoutMs) {
7086
7246
  try {
7087
7247
  await fs6.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
@@ -7747,6 +7907,16 @@ function parseKillCommand(command) {
7747
7907
  if (pgrepMatch) {
7748
7908
  return null;
7749
7909
  }
7910
+ const posixScriptMatch = normalized.match(SCRIPT_KILL_RE_POSIX);
7911
+ if (posixScriptMatch) {
7912
+ return {
7913
+ name: "kill-script",
7914
+ signal: "FORCE",
7915
+ isGroupKill: false,
7916
+ isAllKill: false,
7917
+ originalCommand: command
7918
+ };
7919
+ }
7750
7920
  return null;
7751
7921
  }
7752
7922
  async function getProtectedEntries() {
@@ -9940,7 +10110,7 @@ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
9940
10110
 
9941
10111
  // src/codebase-index/project-server-client.ts
9942
10112
  import { spawn as spawn4 } from "node:child_process";
9943
- import * as fs12 from "node:fs";
10113
+ import * as fs13 from "node:fs";
9944
10114
  import * as net3 from "node:net";
9945
10115
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9946
10116
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
@@ -10027,16 +10197,16 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
10027
10197
 
10028
10198
  // src/codebase-index/project-server-endpoint.ts
10029
10199
  import { createHash as createHash4 } from "node:crypto";
10030
- import * as fs11 from "node:fs";
10200
+ import * as fs12 from "node:fs";
10031
10201
  import * as os5 from "node:os";
10032
- import * as path16 from "node:path";
10202
+ import * as path17 from "node:path";
10033
10203
  import { fileURLToPath } from "node:url";
10034
10204
  import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
10035
10205
 
10036
10206
  // src/codebase-index/writer.ts
10037
10207
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
10038
- import * as fs10 from "node:fs";
10039
- import * as path15 from "node:path";
10208
+ import * as fs11 from "node:fs";
10209
+ import * as path16 from "node:path";
10040
10210
 
10041
10211
  // src/codebase-index/bm25.ts
10042
10212
  var K1 = 1.5;
@@ -10127,6 +10297,9 @@ var Bm25Index = class {
10127
10297
  }
10128
10298
  };
10129
10299
 
10300
+ // src/codebase-index/writer.ts
10301
+ init_languages2();
10302
+
10130
10303
  // src/codebase-index/lsp-kind.ts
10131
10304
  function lspKindToInternalKind(k) {
10132
10305
  switch (k) {
@@ -10161,7 +10334,7 @@ function lspKindToInternalKind(k) {
10161
10334
  }
10162
10335
 
10163
10336
  // src/codebase-index/schema.ts
10164
- var SCHEMA_VERSION = 3;
10337
+ var SCHEMA_VERSION = 4;
10165
10338
 
10166
10339
  // src/codebase-index/sqlite-runtime.ts
10167
10340
  import { createRequire } from "node:module";
@@ -10232,7 +10405,7 @@ function runSqliteWithRetry(fn) {
10232
10405
 
10233
10406
  // src/codebase-index/writer-admin.ts
10234
10407
  import * as fs9 from "node:fs";
10235
- import * as path13 from "node:path";
10408
+ import * as path14 from "node:path";
10236
10409
  var DB_FILE = "index.db";
10237
10410
  function getAllIndexableWithStatement(stmt) {
10238
10411
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10291,7 +10464,7 @@ function getAllFileMetasWithStatement(stmt) {
10291
10464
  }
10292
10465
  function getIndexDbSizeBytes(indexDir) {
10293
10466
  try {
10294
- return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10467
+ return fs9.statSync(path14.join(indexDir, DB_FILE)).size;
10295
10468
  } catch {
10296
10469
  return 0;
10297
10470
  }
@@ -10342,49 +10515,345 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
10342
10515
  }
10343
10516
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10344
10517
  if (refs.length === 0) return;
10345
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
10518
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
10346
10519
  for (let i = 0; i < refs.length; i += chunkSize) {
10347
10520
  const chunk = refs.slice(i, i + chunkSize);
10348
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
10521
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
10349
10522
  const insert = stmt(
10350
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
10523
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
10524
+ VALUES ${placeholders}`
10351
10525
  );
10352
10526
  const binds = [];
10353
10527
  for (const ref of chunk) {
10354
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
10528
+ binds.push(
10529
+ ref.fromId,
10530
+ ref.toName,
10531
+ ref.toId ?? null,
10532
+ ref.callType,
10533
+ ref.line,
10534
+ ref.lang ?? "",
10535
+ ref.module ?? null,
10536
+ ref.toFile ?? null
10537
+ );
10355
10538
  }
10356
10539
  insert.run(...binds);
10357
10540
  }
10358
10541
  }
10359
10542
 
10360
- // src/codebase-index/writer-graph-helpers.ts
10361
- import * as path14 from "node:path";
10362
- function derivePackage(filePath) {
10363
- const f = filePath.replace(/\\/g, "/");
10364
- const pkgsIdx = f.indexOf("/packages/");
10365
- if (pkgsIdx !== -1) {
10366
- const rest = f.slice(pkgsIdx + "/packages/".length);
10367
- const seg = rest.split("/")[0];
10368
- return seg ? `@wrongstack/${seg}` : void 0;
10369
- }
10370
- const appsIdx = f.indexOf("/apps/");
10543
+ // src/codebase-index/writer-graph-reader.ts
10544
+ init_languages2();
10545
+
10546
+ // src/codebase-index/module-roots.ts
10547
+ init_languages2();
10548
+ import * as fs10 from "node:fs/promises";
10549
+ import * as path15 from "node:path";
10550
+ function toPortablePath(file) {
10551
+ return file.replace(/\\/g, "/");
10552
+ }
10553
+ async function readTextIfPresent(file) {
10554
+ try {
10555
+ return await fs10.readFile(file, "utf8");
10556
+ } catch {
10557
+ return void 0;
10558
+ }
10559
+ }
10560
+ function parsePackageJsonName(source) {
10561
+ try {
10562
+ const parsed = JSON.parse(source);
10563
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
10564
+ } catch {
10565
+ return void 0;
10566
+ }
10567
+ }
10568
+ function parseGoModulePath(source) {
10569
+ for (const rawLine of source.split(/\r?\n/)) {
10570
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
10571
+ const match = /^module\s+(\S+)/.exec(line);
10572
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
10573
+ }
10574
+ return void 0;
10575
+ }
10576
+ function parseTomlTableName(source, tables) {
10577
+ let current = "";
10578
+ for (const rawLine of source.split(/\r?\n/)) {
10579
+ const line = rawLine.replace(/#.*$/, "").trim();
10580
+ if (line.startsWith("[[")) {
10581
+ current = "\0";
10582
+ continue;
10583
+ }
10584
+ const table = /^\[([^\]]+)\]$/.exec(line);
10585
+ if (table?.[1]) {
10586
+ current = table[1].trim();
10587
+ continue;
10588
+ }
10589
+ if (!tables.includes(current)) continue;
10590
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
10591
+ if (match?.[1]) return match[1];
10592
+ }
10593
+ return void 0;
10594
+ }
10595
+ function parsePomArtifactId(source) {
10596
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
10597
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
10598
+ }
10599
+ var LANGS_BY_KIND = {
10600
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
10601
+ cargo: ["rs"],
10602
+ go: ["go"],
10603
+ python: ["py"],
10604
+ maven: ["java", "kotlin", "scala"],
10605
+ gradle: ["java", "kotlin", "scala"],
10606
+ dotnet: ["csharp"]
10607
+ };
10608
+ function ancestorsOf(dir, stopAt) {
10609
+ const out = [];
10610
+ let current = dir;
10611
+ for (; ; ) {
10612
+ out.push(current);
10613
+ if (current === stopAt || current.length <= stopAt.length) break;
10614
+ const parent = path15.posix.dirname(current);
10615
+ if (parent === current) break;
10616
+ current = parent;
10617
+ }
10618
+ return out;
10619
+ }
10620
+ var MARKER_PROBES = [
10621
+ {
10622
+ kind: "npm",
10623
+ file: "package.json",
10624
+ build: (dir, source) => {
10625
+ const name = parsePackageJsonName(source) ?? path15.posix.basename(dir);
10626
+ return { name, importPath: name, sourceRoots: [dir] };
10627
+ }
10628
+ },
10629
+ {
10630
+ kind: "cargo",
10631
+ file: "Cargo.toml",
10632
+ build: (dir, source) => {
10633
+ const name = parseTomlTableName(source, ["package"]);
10634
+ if (!name) return void 0;
10635
+ return {
10636
+ name: `crate:${name}`,
10637
+ // Rust paths use underscores where crate names often use dashes.
10638
+ importPath: name.replace(/-/g, "_"),
10639
+ sourceRoots: [path15.posix.join(dir, "src")]
10640
+ };
10641
+ }
10642
+ },
10643
+ {
10644
+ kind: "go",
10645
+ file: "go.mod",
10646
+ build: (dir, source) => {
10647
+ const modulePath = parseGoModulePath(source);
10648
+ if (!modulePath) return void 0;
10649
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
10650
+ }
10651
+ },
10652
+ {
10653
+ kind: "python",
10654
+ file: "pyproject.toml",
10655
+ build: (dir, source) => {
10656
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path15.posix.basename(dir);
10657
+ return {
10658
+ name: `py:${name}`,
10659
+ importPath: void 0,
10660
+ // `src/` layout is the packaging-guide default; the root itself covers
10661
+ // the flat layout. Both are probed, missing ones simply never match.
10662
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
10663
+ };
10664
+ }
10665
+ },
10666
+ {
10667
+ kind: "python",
10668
+ file: "setup.py",
10669
+ build: (dir) => ({
10670
+ name: `py:${path15.posix.basename(dir)}`,
10671
+ importPath: void 0,
10672
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
10673
+ })
10674
+ },
10675
+ {
10676
+ kind: "maven",
10677
+ file: "pom.xml",
10678
+ build: (dir, source) => {
10679
+ const artifactId = parsePomArtifactId(source) ?? path15.posix.basename(dir);
10680
+ return {
10681
+ name: `mvn:${artifactId}`,
10682
+ importPath: void 0,
10683
+ sourceRoots: [
10684
+ path15.posix.join(dir, "src/main/java"),
10685
+ path15.posix.join(dir, "src/main/kotlin"),
10686
+ path15.posix.join(dir, "src/main/scala"),
10687
+ path15.posix.join(dir, "src/test/java")
10688
+ ]
10689
+ };
10690
+ }
10691
+ },
10692
+ {
10693
+ kind: "gradle",
10694
+ file: "build.gradle",
10695
+ build: (dir) => buildGradleRoot(dir)
10696
+ },
10697
+ {
10698
+ kind: "gradle",
10699
+ file: "build.gradle.kts",
10700
+ build: (dir) => buildGradleRoot(dir)
10701
+ }
10702
+ ];
10703
+ function buildGradleRoot(dir) {
10704
+ return {
10705
+ name: `gradle:${path15.posix.basename(dir)}`,
10706
+ importPath: void 0,
10707
+ sourceRoots: [
10708
+ path15.posix.join(dir, "src/main/java"),
10709
+ path15.posix.join(dir, "src/main/kotlin"),
10710
+ path15.posix.join(dir, "src/main/scala")
10711
+ ]
10712
+ };
10713
+ }
10714
+ async function probeDotnetRoot(dir) {
10715
+ let entries;
10716
+ try {
10717
+ entries = await fs10.readdir(dir);
10718
+ } catch {
10719
+ return void 0;
10720
+ }
10721
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
10722
+ if (!project) return void 0;
10723
+ const name = project.slice(0, -".csproj".length);
10724
+ return {
10725
+ dir,
10726
+ kind: "dotnet",
10727
+ name: `csproj:${name}`,
10728
+ importPath: void 0,
10729
+ sourceRoots: [dir]
10730
+ };
10731
+ }
10732
+ async function detectModuleRoots(projectRoot, files) {
10733
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
10734
+ const langsByDir = /* @__PURE__ */ new Map();
10735
+ for (const file of files) {
10736
+ const portable = toPortablePath(file);
10737
+ const lang = detectLang(portable);
10738
+ if (!lang) continue;
10739
+ const dir = path15.posix.dirname(portable);
10740
+ let langs = langsByDir.get(dir);
10741
+ if (!langs) {
10742
+ langs = /* @__PURE__ */ new Set();
10743
+ langsByDir.set(dir, langs);
10744
+ }
10745
+ langs.add(lang);
10746
+ }
10747
+ const candidates = /* @__PURE__ */ new Map();
10748
+ for (const [dir, langs] of langsByDir) {
10749
+ for (const ancestor of ancestorsOf(dir, root)) {
10750
+ let merged = candidates.get(ancestor);
10751
+ if (!merged) {
10752
+ merged = /* @__PURE__ */ new Set();
10753
+ candidates.set(ancestor, merged);
10754
+ }
10755
+ for (const lang of langs) merged.add(lang);
10756
+ }
10757
+ }
10758
+ const roots = [];
10759
+ await Promise.all(
10760
+ [...candidates].map(async ([dir, langs]) => {
10761
+ for (const probe of MARKER_PROBES) {
10762
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
10763
+ const source = await readTextIfPresent(path15.posix.join(dir, probe.file));
10764
+ if (source === void 0) continue;
10765
+ const built = probe.build(dir, source);
10766
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
10767
+ }
10768
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
10769
+ const dotnet = await probeDotnetRoot(dir);
10770
+ if (dotnet) roots.push(dotnet);
10771
+ }
10772
+ })
10773
+ );
10774
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
10775
+ return { projectRoot: root, roots };
10776
+ }
10777
+ function findOwningRoot(structure, file, kinds) {
10778
+ const portable = toPortablePath(file);
10779
+ for (const root of structure.roots) {
10780
+ if (kinds && !kinds.includes(root.kind)) continue;
10781
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
10782
+ }
10783
+ return void 0;
10784
+ }
10785
+ function derivePackageFromLayout(filePath) {
10786
+ const portable = toPortablePath(filePath);
10787
+ const packagesIdx = portable.indexOf("/packages/");
10788
+ if (packagesIdx !== -1) {
10789
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
10790
+ if (segment) return `@wrongstack/${segment}`;
10791
+ }
10792
+ const appsIdx = portable.indexOf("/apps/");
10371
10793
  if (appsIdx !== -1) {
10372
- const rest = f.slice(appsIdx + "/apps/".length);
10373
- const seg = rest.split("/")[0];
10374
- return seg ? `app:${seg}` : void 0;
10794
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
10795
+ if (segment) return `app:${segment}`;
10375
10796
  }
10376
10797
  return void 0;
10377
10798
  }
10378
- function packageFromImport(moduleName) {
10379
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
10380
- const parts = moduleName.split("/");
10381
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
10799
+ function pythonPackageLabel(structure, file, initDirs) {
10800
+ const portable = toPortablePath(file);
10801
+ const dir = path15.posix.dirname(portable);
10802
+ if (!initDirs.has(dir)) return void 0;
10803
+ const segments = [];
10804
+ let current = dir;
10805
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
10806
+ segments.unshift(path15.posix.basename(current));
10807
+ current = path15.posix.dirname(current);
10808
+ }
10809
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
10382
10810
  }
10383
- function buildPackageGraphNodes(fileCounts, files) {
10811
+ function assignPackageLabels(structure, files) {
10812
+ const initDirs = /* @__PURE__ */ new Set();
10813
+ for (const file of files) {
10814
+ const portable = toPortablePath(file);
10815
+ if (path15.posix.basename(portable) === "__init__.py") {
10816
+ initDirs.add(path15.posix.dirname(portable));
10817
+ }
10818
+ }
10819
+ const labels = /* @__PURE__ */ new Map();
10820
+ for (const file of files) {
10821
+ const portable = toPortablePath(file);
10822
+ const lang = detectLang(portable);
10823
+ if (lang === "go") {
10824
+ const owner3 = findOwningRoot(structure, portable, ["go"]);
10825
+ const dir = path15.posix.dirname(portable);
10826
+ if (owner3?.importPath) {
10827
+ const relative12 = path15.posix.relative(owner3.dir, dir);
10828
+ labels.set(file, relative12 ? `${owner3.importPath}/${relative12}` : owner3.importPath);
10829
+ } else {
10830
+ labels.set(file, `go:${path15.posix.relative(structure.projectRoot, dir) || "."}`);
10831
+ }
10832
+ continue;
10833
+ }
10834
+ if (lang === "py") {
10835
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
10836
+ if (dotted) {
10837
+ labels.set(file, dotted);
10838
+ continue;
10839
+ }
10840
+ }
10841
+ const owner2 = findOwningRoot(structure, portable);
10842
+ const label = owner2?.name ?? derivePackageFromLayout(portable) ?? "(root)";
10843
+ labels.set(file, label);
10844
+ }
10845
+ return labels;
10846
+ }
10847
+
10848
+ // src/codebase-index/writer-graph-helpers.ts
10849
+ function createPackageLabeller(stored) {
10850
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
10851
+ }
10852
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
10384
10853
  const pkgNodes = /* @__PURE__ */ new Map();
10385
10854
  const fileToPkg = /* @__PURE__ */ new Map();
10386
10855
  for (const { file, n } of fileCounts) {
10387
- const pkg = derivePackage(file) ?? "(root)";
10856
+ const pkg = packageOf(file);
10388
10857
  fileToPkg.set(file, pkg);
10389
10858
  const node = pkgNodes.get(pkg);
10390
10859
  if (node) {
@@ -10401,7 +10870,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10401
10870
  }
10402
10871
  }
10403
10872
  for (const { file } of files) {
10404
- const pkg = derivePackage(file) ?? "(root)";
10873
+ const pkg = packageOf(file);
10405
10874
  fileToPkg.set(file, pkg);
10406
10875
  const node = pkgNodes.get(pkg);
10407
10876
  if (node) {
@@ -10419,7 +10888,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10419
10888
  }
10420
10889
  return { pkgNodes, fileToPkg };
10421
10890
  }
10422
- function buildFileGraphNodeState(pkgSyms, localFiles) {
10891
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
10423
10892
  const fileNodes = /* @__PURE__ */ new Map();
10424
10893
  const symToFile = /* @__PURE__ */ new Map();
10425
10894
  const fileStats = /* @__PURE__ */ new Map();
@@ -10438,7 +10907,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10438
10907
  id: `file:${file}`,
10439
10908
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
10440
10909
  kind: "file",
10441
- package: derivePackage(file) ?? "(root)",
10910
+ package: packageOf(file),
10442
10911
  file,
10443
10912
  symbolCount: stats?.count ?? 0,
10444
10913
  lang: stats?.lang,
@@ -10450,7 +10919,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10450
10919
  }
10451
10920
  return { fileNodes, symToFile, fileStats, ensureFileNode };
10452
10921
  }
10453
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10922
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
10454
10923
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
10455
10924
  const aExternal = a.file === fileFilter ? 0 : 1;
10456
10925
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -10462,7 +10931,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10462
10931
  symbolId: s.id,
10463
10932
  symbolKind: s.kind,
10464
10933
  file: s.file,
10465
- package: derivePackage(s.file) ?? "(root)",
10934
+ package: packageOf(s.file),
10466
10935
  lang: s.lang,
10467
10936
  line: s.line,
10468
10937
  signature: s.signature,
@@ -10470,29 +10939,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10470
10939
  external: s.file !== fileFilter
10471
10940
  }));
10472
10941
  }
10473
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10474
- if (!moduleName.startsWith(".")) return void 0;
10475
- const normalizedFrom = fromFile.replace(/\\/g, "/");
10476
- const absolute = path14.posix.normalize(
10477
- path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10478
- );
10479
- const extension = path14.posix.extname(absolute);
10480
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
10481
- const candidates = [
10482
- absolute,
10483
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10484
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10485
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10486
- ];
10487
- const indexedByPortablePath = new Map(
10488
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
10489
- );
10490
- for (const candidate of candidates) {
10491
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
10492
- if (indexed) return indexed;
10493
- }
10494
- return void 0;
10495
- }
10496
10942
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
10497
10943
  const key = `${source}\0${target}`;
10498
10944
  let edge = edgeMap.get(key);
@@ -10533,11 +10979,146 @@ function mapWriterRefRow(row) {
10533
10979
  toName: row.to_name,
10534
10980
  toId: row.to_id ?? void 0,
10535
10981
  callType: row.call_type,
10536
- line: row.line
10982
+ line: row.line,
10983
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
10984
+ // queries select; `undefined` keeps those rows valid Refs.
10985
+ lang: row.lang || void 0,
10986
+ module: row.module ?? void 0,
10987
+ toFile: row.to_file ?? void 0
10537
10988
  };
10538
10989
  }
10539
10990
 
10540
10991
  // src/codebase-index/writer-graph-reader.ts
10992
+ var MAX_SQL_VARS = 900;
10993
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
10994
+ const results = [];
10995
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
10996
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
10997
+ const placeholders = chunk.map(() => "?").join(",");
10998
+ const sql = buildSql(placeholders);
10999
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
11000
+ }
11001
+ return results;
11002
+ }
11003
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
11004
+ let total = 0;
11005
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
11006
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
11007
+ const placeholders = chunk.map(() => "?").join(",");
11008
+ const sql = buildSql(placeholders);
11009
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
11010
+ total += rows[0]?.n ?? 0;
11011
+ }
11012
+ return total;
11013
+ }
11014
+ function mapCallSiteRow(row) {
11015
+ return {
11016
+ symbol: {
11017
+ id: row.sym_id,
11018
+ name: row.sym_name,
11019
+ kind: row.sym_kind,
11020
+ lang: row.sym_lang,
11021
+ file: row.sym_file,
11022
+ line: row.sym_line,
11023
+ signature: row.sym_signature
11024
+ },
11025
+ callType: row.call_type,
11026
+ line: row.ref_line
11027
+ };
11028
+ }
11029
+ function resolveSymbolIds(stmt, symbolName, file) {
11030
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
11031
+ const args = file ? [symbolName, file] : [symbolName];
11032
+ const rows = stmt(baseSql).all(...args);
11033
+ return rows.map((r) => r.id);
11034
+ }
11035
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
11036
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
11037
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
11038
+ let matchIds = targetIds;
11039
+ let ambiguous = false;
11040
+ if (file !== void 0) {
11041
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
11042
+ if (allNamedIds.length > targetIds.length) {
11043
+ matchIds = allNamedIds;
11044
+ ambiguous = true;
11045
+ }
11046
+ }
11047
+ const useFallback = !file;
11048
+ const rows = chunkedIdQuery(
11049
+ stmt,
11050
+ matchIds,
11051
+ (ph) => `SELECT
11052
+ s.id AS sym_id,
11053
+ s.name AS sym_name,
11054
+ s.kind AS sym_kind,
11055
+ s.lang AS sym_lang,
11056
+ s.file AS sym_file,
11057
+ s.line AS sym_line,
11058
+ s.signature AS sym_signature,
11059
+ r.call_type,
11060
+ r.line AS ref_line
11061
+ FROM refs r
11062
+ JOIN symbols s ON s.id = r.from_id
11063
+ WHERE r.to_id IN (${ph})
11064
+ ORDER BY r.line, r.id`,
11065
+ []
11066
+ );
11067
+ if (useFallback) {
11068
+ const fallbackRows = stmt(
11069
+ `SELECT
11070
+ s.id AS sym_id,
11071
+ s.name AS sym_name,
11072
+ s.kind AS sym_kind,
11073
+ s.lang AS sym_lang,
11074
+ s.file AS sym_file,
11075
+ s.line AS sym_line,
11076
+ s.signature AS sym_signature,
11077
+ r.call_type,
11078
+ r.line AS ref_line
11079
+ FROM refs r
11080
+ JOIN symbols s ON s.id = r.from_id
11081
+ WHERE r.to_id IS NULL AND r.to_name = ?
11082
+ ORDER BY r.line, r.id`
11083
+ ).all(symbolName);
11084
+ rows.push(...fallbackRows);
11085
+ }
11086
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
11087
+ const allCalls = rows.map(mapCallSiteRow);
11088
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
11089
+ }
11090
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
11091
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
11092
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
11093
+ const unresolvedCount = chunkedIdScalar(
11094
+ stmt,
11095
+ sourceIds,
11096
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
11097
+ );
11098
+ const rows = chunkedIdQuery(
11099
+ stmt,
11100
+ sourceIds,
11101
+ (ph) => `SELECT
11102
+ s.id AS sym_id,
11103
+ s.name AS sym_name,
11104
+ s.kind AS sym_kind,
11105
+ s.lang AS sym_lang,
11106
+ s.file AS sym_file,
11107
+ s.line AS sym_line,
11108
+ s.signature AS sym_signature,
11109
+ r.call_type,
11110
+ r.line AS ref_line
11111
+ FROM refs r
11112
+ JOIN symbols s ON s.id = r.to_id
11113
+ WHERE r.from_id IN (${ph})
11114
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
11115
+ ORDER BY r.line, r.id`,
11116
+ []
11117
+ );
11118
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
11119
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
11120
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
11121
+ }
10541
11122
  function findRefsToWithStatement(stmt, symbolId) {
10542
11123
  return stmt(
10543
11124
  "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 = ?)"
@@ -10551,7 +11132,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
10551
11132
  function getPackageGraphWithStatement(stmt) {
10552
11133
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
10553
11134
  const files = stmt("SELECT DISTINCT file FROM files").all();
10554
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
11135
+ const packageOf = readPackageLabeller(stmt);
11136
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
10555
11137
  const refRows = stmt(
10556
11138
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
10557
11139
  FROM refs r
@@ -10562,32 +11144,42 @@ function getPackageGraphWithStatement(stmt) {
10562
11144
  ).all();
10563
11145
  const edgeMap = /* @__PURE__ */ new Map();
10564
11146
  for (const r of refRows) {
10565
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
10566
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
11147
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11148
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
10567
11149
  if (fromPkg === toPkg) continue;
10568
11150
  const n = Number(r.n) || 0;
10569
11151
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
10570
11152
  }
10571
11153
  const importRows = stmt(
10572
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
11154
+ `SELECT s.file AS from_file,
11155
+ COALESCE(r.to_file, st.file) AS to_file,
11156
+ COUNT(*) AS n
10573
11157
  FROM refs r
10574
11158
  JOIN symbols s ON s.id = r.from_id
11159
+ LEFT JOIN symbols st ON st.id = r.to_id
10575
11160
  WHERE r.call_type = 'import'
10576
- GROUP BY r.to_name, s.file`
11161
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
11162
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
10577
11163
  ).all();
10578
11164
  for (const r of importRows) {
10579
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
10580
- const toPkg = packageFromImport(r.to_name);
10581
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
11165
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11166
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
11167
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
10582
11168
  const n = Number(r.n) || 0;
10583
11169
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
10584
11170
  }
10585
11171
  const edges = materializeWeightedEdges(edgeMap, "pkg");
10586
11172
  return { nodes: [...pkgNodes.values()], edges };
10587
11173
  }
11174
+ function readPackageLabeller(stmt) {
11175
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
11176
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
11177
+ }
10588
11178
  function getFileGraphWithStatement(stmt, packageFilter) {
10589
11179
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
10590
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
11180
+ const packageOf = readPackageLabeller(stmt);
11181
+ const langOf = (file) => detectLang(file) ?? "other";
11182
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
10591
11183
  const localFiles = new Set(pkgFilePaths);
10592
11184
  if (localFiles.size === 0) return { nodes: [], edges: [] };
10593
11185
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -10596,9 +11188,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10596
11188
  ).all(...pkgFilePaths);
10597
11189
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
10598
11190
  pkgSyms,
10599
- localFiles
11191
+ localFiles,
11192
+ packageOf
10600
11193
  );
10601
- const indexedFiles = new Set(allFiles.map((f) => f.file));
10602
11194
  const refRows = stmt(
10603
11195
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
10604
11196
  FROM refs r
@@ -10621,7 +11213,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10621
11213
  for (const x of extras) {
10622
11214
  symToFile.set(x.id, x.file);
10623
11215
  if (!fileStats.has(x.file)) {
10624
- fileStats.set(x.file, { count: 0, lang: "ts" });
11216
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
10625
11217
  }
10626
11218
  }
10627
11219
  }
@@ -10638,17 +11230,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10638
11230
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
10639
11231
  }
10640
11232
  const importRows = stmt(
10641
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
11233
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
10642
11234
  FROM refs r
11235
+ LEFT JOIN symbols st ON st.id = r.to_id
10643
11236
  WHERE r.call_type = 'import'
11237
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
10644
11238
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
10645
- GROUP BY r.from_id, r.to_name`
11239
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
10646
11240
  ).all(...pkgFilePaths);
10647
11241
  for (const r of importRows) {
10648
11242
  const fromFile = symToFile.get(r.from_id);
10649
11243
  if (!fromFile || !localFiles.has(fromFile)) continue;
10650
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
11244
+ const toFile = r.to_file;
10651
11245
  if (!toFile || fromFile === toFile) continue;
11246
+ if (!fileStats.has(toFile)) {
11247
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
11248
+ }
10652
11249
  ensureFileNode(fromFile);
10653
11250
  ensureFileNode(toFile);
10654
11251
  const n = Number(r.n) || 0;
@@ -10698,7 +11295,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
10698
11295
  ).all(...missingIds);
10699
11296
  for (const s of extras) symById.set(s.id, s);
10700
11297
  }
10701
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
11298
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
10702
11299
  return { nodes, edges };
10703
11300
  }
10704
11301
 
@@ -10720,7 +11317,7 @@ function assignRefsToSymbols(refs, symbols) {
10720
11317
  }
10721
11318
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10722
11319
  if (!owner2 || owner2.id <= 0) continue;
10723
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
11320
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
10724
11321
  if (seen.has(key)) continue;
10725
11322
  seen.add(key);
10726
11323
  assigned.push({ ...ref, fromId: owner2.id });
@@ -10766,7 +11363,11 @@ var CORE_TABLES_SQL = `
10766
11363
  lang TEXT NOT NULL,
10767
11364
  mtime_ms INTEGER NOT NULL,
10768
11365
  symbol_count INTEGER NOT NULL DEFAULT 0,
10769
- last_indexed INTEGER NOT NULL
11366
+ last_indexed INTEGER NOT NULL,
11367
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
11368
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
11369
+ -- re-derived per query because the evidence lives on disk, not in the DB.
11370
+ package TEXT NOT NULL DEFAULT ''
10770
11371
  );
10771
11372
  CREATE TABLE IF NOT EXISTS symbols (
10772
11373
  id INTEGER PRIMARY KEY,
@@ -10783,6 +11384,9 @@ var CORE_TABLES_SQL = `
10783
11384
  file_fk TEXT NOT NULL
10784
11385
  );
10785
11386
  `;
11387
+ var FILE_INDEX_SQL = [
11388
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
11389
+ ];
10786
11390
  var SYMBOL_INDEX_SQL = [
10787
11391
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10788
11392
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -10799,15 +11403,32 @@ var REFS_TABLE_SQL = `
10799
11403
  to_name TEXT NOT NULL,
10800
11404
  to_id INTEGER,
10801
11405
  call_type TEXT NOT NULL,
10802
- line INTEGER NOT NULL
11406
+ line INTEGER NOT NULL,
11407
+ lang TEXT NOT NULL DEFAULT '',
11408
+ module TEXT,
11409
+ to_file TEXT
10803
11410
  );
10804
11411
  `;
10805
11412
  var REFS_INDEX_SQL = [
10806
11413
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10807
11414
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10808
11415
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10809
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
11416
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
11417
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
11418
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
11419
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
11420
+ // The post-index module resolution pass groups unresolved import refs by
11421
+ // (module, lang); graph readers then read to_file back.
11422
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
11423
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
10810
11424
  ];
11425
+ var LANG_FAMILY_TABLE_SQL = `
11426
+ CREATE TABLE IF NOT EXISTS lang_family (
11427
+ lang TEXT PRIMARY KEY,
11428
+ family TEXT NOT NULL
11429
+ );
11430
+ `;
11431
+ var LANG_FAMILY_WILDCARD = "*";
10811
11432
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10812
11433
 
10813
11434
  // src/codebase-index/writer-search-helpers.ts
@@ -11019,15 +11640,69 @@ var IndexStore = class _IndexStore {
11019
11640
  }
11020
11641
  constructor(projectRoot, opts = {}) {
11021
11642
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
11022
- fs10.mkdirSync(this.indexDir, { recursive: true });
11643
+ fs11.mkdirSync(this.indexDir, { recursive: true });
11023
11644
  const Database = loadDatabaseSync();
11024
- this.db = new Database(path15.join(this.indexDir, DB_FILE2));
11645
+ this.db = new Database(path16.join(this.indexDir, DB_FILE2));
11025
11646
  applyIndexStorePragmas(this.db);
11026
11647
  this.initSchema();
11027
11648
  }
11028
11649
  runWithRetry(fn) {
11029
11650
  return runSqliteWithRetry(fn);
11030
11651
  }
11652
+ /**
11653
+ * Mirror the in-process language→family map into SQLite.
11654
+ *
11655
+ * Rewritten on every open rather than only on schema bumps: the mapping is
11656
+ * static lookup data, so a code-side change (a new language, a language
11657
+ * moving families) must take effect without forcing a full reindex.
11658
+ */
11659
+ seedLangFamilies() {
11660
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
11661
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
11662
+ insert.run("", LANG_FAMILY_WILDCARD);
11663
+ }
11664
+ /**
11665
+ * Add any column the current schema expects but the on-disk table lacks.
11666
+ *
11667
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
11668
+ * and the version check above only rebuilds on a version *mismatch*. That
11669
+ * leaves a real gap: several wstack processes share this database, and while
11670
+ * a version upgrade is rolling out one of them may still be running the
11671
+ * previous build. That older process sees the newer version number, drops the
11672
+ * tables, and recreates them from *its* DDL — without the newer columns —
11673
+ * while the metadata row still reads the new version. Every later query for
11674
+ * one of those columns then fails with `no such column`, and no amount of
11675
+ * reindexing fixes it, because the version numbers already agree.
11676
+ *
11677
+ * Repairing column-by-column makes the schema self-healing from any of those
11678
+ * states. Table and column names are compile-time literals from this module,
11679
+ * never user input.
11680
+ */
11681
+ repairMissingColumns() {
11682
+ const expected = [
11683
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
11684
+ {
11685
+ table: "refs",
11686
+ columns: [
11687
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
11688
+ ["module", "TEXT"],
11689
+ ["to_file", "TEXT"]
11690
+ ]
11691
+ }
11692
+ ];
11693
+ for (const { table, columns } of expected) {
11694
+ const present = new Set(
11695
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
11696
+ (row) => typeof row.name === "string" ? [row.name] : []
11697
+ )
11698
+ );
11699
+ if (present.size === 0) continue;
11700
+ for (const [name, type] of columns) {
11701
+ if (present.has(name)) continue;
11702
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
11703
+ }
11704
+ }
11705
+ }
11031
11706
  initSchema() {
11032
11707
  this.db.exec(METADATA_TABLE_SQL);
11033
11708
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -11050,9 +11725,13 @@ var IndexStore = class _IndexStore {
11050
11725
  );
11051
11726
  }
11052
11727
  this.db.exec(CORE_TABLES_SQL);
11053
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11054
11728
  this.db.exec(REFS_TABLE_SQL);
11729
+ this.repairMissingColumns();
11730
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
11731
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11055
11732
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
11733
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
11734
+ this.seedLangFamilies();
11056
11735
  try {
11057
11736
  this.db.exec(SYMBOLS_FTS_SQL);
11058
11737
  this.ftsAvailable = true;
@@ -11087,6 +11766,18 @@ var IndexStore = class _IndexStore {
11087
11766
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
11088
11767
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
11089
11768
  static MAX_SQL_VARS = 900;
11769
+ /**
11770
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
11771
+ * `sym` belong to the same language family — or the ref carries no language,
11772
+ * in which case the wildcard bind matches everything.
11773
+ *
11774
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
11775
+ */
11776
+ static FAMILY_MATCH_SQL = `(
11777
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
11778
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
11779
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
11780
+ )`;
11090
11781
  /**
11091
11782
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
11092
11783
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -11150,9 +11841,12 @@ var IndexStore = class _IndexStore {
11150
11841
  const placeholders = chunk.map(() => "?").join(",");
11151
11842
  const result = this.stmt(
11152
11843
  `UPDATE refs
11153
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
11844
+ SET to_id = (
11845
+ SELECT MIN(sym.id) FROM symbols sym
11846
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
11847
+ )
11154
11848
  WHERE to_name IN (${placeholders})`
11155
- ).run(...chunk);
11849
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
11156
11850
  changes += result.changes ?? 0;
11157
11851
  }
11158
11852
  return changes;
@@ -11279,6 +11973,115 @@ var IndexStore = class _IndexStore {
11279
11973
  getAllFileMetas() {
11280
11974
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
11281
11975
  }
11976
+ // ─── Project structure & module resolution ──────────────────────────────────
11977
+ /** Store the Code Atlas grouping label for each indexed file. */
11978
+ setFilePackages(entries) {
11979
+ if (entries.size === 0) return;
11980
+ this.runWithRetry(() => {
11981
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
11982
+ for (const [file, label] of entries) update.run(label, file);
11983
+ });
11984
+ }
11985
+ /**
11986
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
11987
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
11988
+ * Ordered so the resolver's choice among duplicate declarations is stable.
11989
+ */
11990
+ getNamespaceDeclarations() {
11991
+ return this.stmt(
11992
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
11993
+ ).all();
11994
+ }
11995
+ /** `file → package` for every indexed file that has a label. */
11996
+ getFilePackages() {
11997
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
11998
+ return new Map(rows.map((row) => [row.file, row.package]));
11999
+ }
12000
+ /**
12001
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
12002
+ *
12003
+ * Distinct rather than per-ref because resolution depends only on these three
12004
+ * values: a file importing the same module twenty times resolves it once.
12005
+ */
12006
+ getUnresolvedImports(onlyFiles) {
12007
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
12008
+ FROM refs r
12009
+ JOIN symbols s ON s.id = r.from_id
12010
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
12011
+ if (!onlyFiles?.length) {
12012
+ return this.stmt(base).all();
12013
+ }
12014
+ const out = [];
12015
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
12016
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
12017
+ const placeholders = chunk.map(() => "?").join(",");
12018
+ out.push(
12019
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
12020
+ );
12021
+ }
12022
+ return out;
12023
+ }
12024
+ /**
12025
+ * Write resolved import targets back onto `refs.to_file`.
12026
+ *
12027
+ * Applied through a temp table and a single UPDATE: one statement per
12028
+ * resolution would mean thousands of round-trips on a first index.
12029
+ */
12030
+ applyImportResolutions(resolutions) {
12031
+ if (resolutions.length === 0) return 0;
12032
+ return this.runWithRetry(() => {
12033
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12034
+ this.db.exec(
12035
+ `CREATE TEMP TABLE import_resolution (
12036
+ from_file TEXT NOT NULL,
12037
+ lang TEXT NOT NULL,
12038
+ module TEXT NOT NULL,
12039
+ to_file TEXT NOT NULL
12040
+ )`
12041
+ );
12042
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
12043
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
12044
+ const chunk = resolutions.slice(i, i + chunkSize);
12045
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
12046
+ const binds = [];
12047
+ for (const entry of chunk) {
12048
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
12049
+ }
12050
+ this.stmt(
12051
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
12052
+ VALUES ${placeholders}`
12053
+ ).run(...binds);
12054
+ }
12055
+ this.db.exec(
12056
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
12057
+ ON import_resolution(module, lang, from_file)`
12058
+ );
12059
+ const result = this.stmt(
12060
+ `UPDATE refs
12061
+ SET to_file = (
12062
+ SELECT ir.to_file
12063
+ FROM temp.import_resolution ir
12064
+ JOIN symbols s ON s.id = refs.from_id
12065
+ WHERE ir.module = refs.module
12066
+ AND ir.lang = refs.lang
12067
+ AND ir.from_file = s.file
12068
+ LIMIT 1
12069
+ )
12070
+ WHERE refs.call_type = 'import'
12071
+ AND refs.module IS NOT NULL
12072
+ AND EXISTS (
12073
+ SELECT 1
12074
+ FROM temp.import_resolution ir
12075
+ JOIN symbols s ON s.id = refs.from_id
12076
+ WHERE ir.module = refs.module
12077
+ AND ir.lang = refs.lang
12078
+ AND ir.from_file = s.file
12079
+ )`
12080
+ ).run();
12081
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12082
+ return result.changes ?? 0;
12083
+ });
12084
+ }
11282
12085
  // ─── Search ──────────────────────────────────────────────────────────────────
11283
12086
  search(query, filter, opts) {
11284
12087
  const built = this.buildSearchWhere(query, filter);
@@ -11665,9 +12468,12 @@ var IndexStore = class _IndexStore {
11665
12468
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
11666
12469
  * Call this after all symbols have been inserted to fill in cross-references.
11667
12470
  *
11668
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
11669
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
11670
- * that found a targetmatching the previous per-row loop's return value.
12471
+ * A match additionally requires the referencing ref and the target symbol to
12472
+ * be in the same {@link LangFamily}. Without that guard a name match is a
12473
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
12474
+ * `Config` are declared in most languages at once, and each collision draws a
12475
+ * Code Atlas edge between files that never reference each other. Refs stored
12476
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
11671
12477
  */
11672
12478
  resolveRefs() {
11673
12479
  return this.runWithRetry(() => {
@@ -11676,20 +12482,35 @@ var IndexStore = class _IndexStore {
11676
12482
  `UPDATE refs
11677
12483
  SET to_id = s.id
11678
12484
  FROM (
11679
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
11680
- ) AS s
12485
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
12486
+ FROM symbols sym
12487
+ JOIN lang_family lf ON lf.lang = sym.lang
12488
+ GROUP BY sym.name, lf.family
12489
+ UNION ALL
12490
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
12491
+ FROM symbols sym
12492
+ GROUP BY sym.name
12493
+ ) AS s,
12494
+ lang_family AS rf
11681
12495
  WHERE refs.to_id IS NULL
11682
12496
  AND refs.to_name IS NOT NULL
11683
- AND refs.to_name = s.name`
12497
+ AND rf.lang = refs.lang
12498
+ AND s.name = refs.to_name
12499
+ AND s.family = rf.family`
11684
12500
  ).run();
11685
12501
  return result.changes ?? 0;
11686
12502
  } catch {
11687
12503
  const result = this.stmt(
11688
12504
  `UPDATE refs SET to_id = (
11689
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
12505
+ SELECT sym.id FROM symbols sym
12506
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12507
+ ORDER BY sym.id LIMIT 1
11690
12508
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
11691
- AND to_name IN (SELECT name FROM symbols)`
11692
- ).run();
12509
+ AND EXISTS (
12510
+ SELECT 1 FROM symbols sym
12511
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12512
+ )`
12513
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
11693
12514
  return result.changes ?? 0;
11694
12515
  }
11695
12516
  });
@@ -11773,6 +12594,20 @@ var IndexStore = class _IndexStore {
11773
12594
  return false;
11774
12595
  }
11775
12596
  }
12597
+ /**
12598
+ * Find all symbols that reference the named target symbol (incoming callers).
12599
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
12600
+ */
12601
+ findIncomingCallsByName(symbolName, file, limit = 100) {
12602
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
12603
+ }
12604
+ /**
12605
+ * Find all symbols that the named source symbol references (outgoing callees).
12606
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
12607
+ */
12608
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
12609
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
12610
+ }
11776
12611
  /**
11777
12612
  * Find all references TO a given symbol (who calls / uses this symbol?).
11778
12613
  */
@@ -11864,21 +12699,21 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
11864
12699
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
11865
12700
  var buildIdCache;
11866
12701
  function projectIndexServerBuildId(entrypoint) {
11867
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
12702
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path17.resolve(entrypoint);
11868
12703
  try {
11869
- const stat18 = fs11.statSync(file);
12704
+ const stat18 = fs12.statSync(file);
11870
12705
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
11871
12706
  return buildIdCache.buildId;
11872
12707
  }
11873
- const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
12708
+ const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
11874
12709
  buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
11875
12710
  return buildId;
11876
12711
  } catch {
11877
- return `unreadable:${path16.basename(file)}`;
12712
+ return `unreadable:${path17.basename(file)}`;
11878
12713
  }
11879
12714
  }
11880
12715
  function normalizeLocalPath(value) {
11881
- const resolved = path16.resolve(value);
12716
+ const resolved = path17.resolve(value);
11882
12717
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11883
12718
  }
11884
12719
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -11890,11 +12725,11 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
11890
12725
  if (process.platform === "win32") {
11891
12726
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
11892
12727
  }
11893
- return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
12728
+ return path17.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
11894
12729
  }
11895
12730
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
11896
- return path16.join(
11897
- path16.resolve(resolveIndexDir(projectRoot, indexDir)),
12731
+ return path17.join(
12732
+ path17.resolve(resolveIndexDir(projectRoot, indexDir)),
11898
12733
  PROJECT_INDEX_SERVER_METADATA_FILE
11899
12734
  );
11900
12735
  }
@@ -11934,7 +12769,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
11934
12769
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
11935
12770
  try {
11936
12771
  const url = new URL(rel, import.meta.url);
11937
- if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
12772
+ if (url.protocol === "file:" && fs13.existsSync(fileURLToPath2(url))) {
11938
12773
  builtUrl = url;
11939
12774
  break;
11940
12775
  }
@@ -12191,7 +13026,7 @@ var ProjectServerConnection = class {
12191
13026
  currentAuthToken() {
12192
13027
  if (this.authToken === void 0) {
12193
13028
  try {
12194
- const raw = fs12.readFileSync(
13029
+ const raw = fs13.readFileSync(
12195
13030
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
12196
13031
  "utf8"
12197
13032
  );
@@ -12456,7 +13291,7 @@ var ProjectServerConnection = class {
12456
13291
  if (!url) throw new Error("built codebase-index project server is unavailable");
12457
13292
  if (process.platform !== "win32") {
12458
13293
  try {
12459
- fs12.rmSync(this.endpoint, { force: true });
13294
+ fs13.rmSync(this.endpoint, { force: true });
12460
13295
  } catch {
12461
13296
  }
12462
13297
  }
@@ -12480,8 +13315,8 @@ var ProjectServerConnection = class {
12480
13315
  process.kill(pid);
12481
13316
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12482
13317
  try {
12483
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
12484
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
13318
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
13319
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
12485
13320
  } catch {
12486
13321
  }
12487
13322
  return true;
@@ -12551,7 +13386,7 @@ import { Worker } from "node:worker_threads";
12551
13386
 
12552
13387
  // src/codebase-index/indexer.ts
12553
13388
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
12554
- import { execFile as execFile2 } from "node:child_process";
13389
+ import { execFile } from "node:child_process";
12555
13390
  import * as fs17 from "node:fs/promises";
12556
13391
  import { availableParallelism } from "node:os";
12557
13392
  import * as path23 from "node:path";
@@ -12562,8 +13397,8 @@ import {
12562
13397
  } from "@wrongstack/core/utils";
12563
13398
 
12564
13399
  // src/codebase-index/gitignore.ts
12565
- import * as fs13 from "node:fs/promises";
12566
- import * as path17 from "node:path";
13400
+ import * as fs14 from "node:fs/promises";
13401
+ import * as path18 from "node:path";
12567
13402
  import { compileGlob } from "@wrongstack/core/utils";
12568
13403
  function globBody(glob) {
12569
13404
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -12579,48 +13414,474 @@ function compileGitignore(lines) {
12579
13414
  negated = true;
12580
13415
  line = line.slice(1);
12581
13416
  }
12582
- let dirOnly = false;
12583
- if (line.endsWith("/")) {
12584
- dirOnly = true;
12585
- line = line.slice(0, -1);
13417
+ let dirOnly = false;
13418
+ if (line.endsWith("/")) {
13419
+ dirOnly = true;
13420
+ line = line.slice(0, -1);
13421
+ }
13422
+ if (!line) continue;
13423
+ const anchored = line.startsWith("/") || line.includes("/");
13424
+ if (line.startsWith("/")) line = line.slice(1);
13425
+ const body = globBody(line);
13426
+ const prefix = anchored ? "^" : "(?:^|.*/)";
13427
+ rules.push({
13428
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
13429
+ under: new RegExp(`${prefix}${body}/.*$`),
13430
+ negated,
13431
+ dirOnly
13432
+ });
13433
+ }
13434
+ return (relPath, isDir) => {
13435
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
13436
+ let ignored = false;
13437
+ for (const r of rules) {
13438
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
13439
+ if (re.test(p)) ignored = !r.negated;
13440
+ }
13441
+ return ignored;
13442
+ };
13443
+ }
13444
+ async function loadGitignoreMatcher(projectRoot) {
13445
+ let lines = [];
13446
+ try {
13447
+ const raw = await fs14.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
13448
+ lines = raw.split("\n");
13449
+ } catch {
13450
+ }
13451
+ return compileGitignore(lines);
13452
+ }
13453
+
13454
+ // src/codebase-index/indexer.ts
13455
+ init_languages2();
13456
+
13457
+ // src/codebase-index/module-resolver.ts
13458
+ init_languages2();
13459
+ import * as path19 from "node:path";
13460
+ var EXTENSIONS = {
13461
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
13462
+ py: [".py", ".pyi"],
13463
+ rs: [".rs"],
13464
+ jvm: [".java", ".kt", ".scala"],
13465
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
13466
+ ruby: [".rb"],
13467
+ go: [".go"]
13468
+ };
13469
+ var DIRECTORY_ENTRIES = {
13470
+ js: ["index"],
13471
+ py: ["__init__"],
13472
+ rs: ["mod"],
13473
+ ruby: ["index"]
13474
+ };
13475
+ function normalizeNamespace(value) {
13476
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
13477
+ }
13478
+ var ModuleResolver = class {
13479
+ structure;
13480
+ /** Lowercased portable path → the path as indexed (case is preserved). */
13481
+ byPath;
13482
+ /** Lowercased portable directory → files directly inside it, as indexed. */
13483
+ byDir;
13484
+ /** Normalized namespace → the file declaring it (first by path, stable). */
13485
+ byNamespace;
13486
+ constructor(structure, files, namespaces = []) {
13487
+ this.structure = structure;
13488
+ this.byPath = /* @__PURE__ */ new Map();
13489
+ this.byDir = /* @__PURE__ */ new Map();
13490
+ this.byNamespace = /* @__PURE__ */ new Map();
13491
+ const dirsByKey = /* @__PURE__ */ new Map();
13492
+ for (const file of files) {
13493
+ const portable = toPortablePath(file);
13494
+ const pathKey = portable.toLowerCase();
13495
+ const priorPath = this.byPath.get(pathKey);
13496
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
13497
+ else this.byPath.set(pathKey, file);
13498
+ const dir = path19.posix.dirname(portable);
13499
+ const dirKey = dir.toLowerCase();
13500
+ const knownDir = dirsByKey.get(dirKey);
13501
+ if (knownDir === void 0) {
13502
+ dirsByKey.set(dirKey, dir);
13503
+ this.byDir.set(dirKey, [file]);
13504
+ } else if (knownDir === dir) {
13505
+ this.byDir.get(dirKey)?.push(file);
13506
+ } else {
13507
+ dirsByKey.delete(dirKey);
13508
+ this.byDir.delete(dirKey);
13509
+ }
13510
+ }
13511
+ for (const { name, file } of namespaces) {
13512
+ const lang = detectLang(file);
13513
+ if (!lang) continue;
13514
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
13515
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
13516
+ this.byNamespace.set(key, file);
13517
+ }
13518
+ }
13519
+ }
13520
+ /**
13521
+ * Resolve `specifier` as written in `fromFile`.
13522
+ * Returns the indexed target path, or `undefined` when it is external or
13523
+ * cannot be located.
13524
+ */
13525
+ resolve(fromFile, lang, specifier) {
13526
+ const spec = specifier.trim().replace(/\\/g, "/");
13527
+ if (!spec) return void 0;
13528
+ const from = toPortablePath(fromFile);
13529
+ switch (languageFamily(lang)) {
13530
+ case "js":
13531
+ return this.resolveJs(from, spec);
13532
+ case "go":
13533
+ return this.resolveGo(spec);
13534
+ case "py":
13535
+ return this.resolvePython(from, spec);
13536
+ case "rs":
13537
+ return this.resolveRust(from, spec);
13538
+ case "jvm":
13539
+ return this.resolveJvm(spec);
13540
+ case "c":
13541
+ return this.resolveInclude(from, spec);
13542
+ case "ruby":
13543
+ return this.resolveRuby(from, spec);
13544
+ case "dotnet":
13545
+ case "php":
13546
+ case "elixir":
13547
+ case "haskell":
13548
+ return this.resolveNamespace(lang, spec);
13549
+ default:
13550
+ return void 0;
13551
+ }
13552
+ }
13553
+ /**
13554
+ * Resolve a namespace specifier to the file declaring it.
13555
+ *
13556
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
13557
+ * names a namespace outright, while PHP's `use App\Models\User` names a
13558
+ * *class* inside `App\Models`, so the prefix is what was declared.
13559
+ */
13560
+ resolveNamespace(lang, spec) {
13561
+ const family = languageFamily(lang);
13562
+ const normalized = normalizeNamespace(spec);
13563
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
13564
+ if (exact) return exact;
13565
+ const segments = normalized.split(".").filter(Boolean);
13566
+ if (segments.length < 2) return void 0;
13567
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
13568
+ }
13569
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
13570
+ lookup(candidate) {
13571
+ return this.byPath.get(path19.posix.normalize(candidate).toLowerCase());
13572
+ }
13573
+ /**
13574
+ * Try `base` verbatim, then `base` + each extension, then each directory
13575
+ * entry point inside `base`.
13576
+ */
13577
+ lookupWithExtensions(base, family) {
13578
+ const direct = this.lookup(base);
13579
+ if (direct) return direct;
13580
+ const extensions = EXTENSIONS[family] ?? [];
13581
+ const suffix = path19.posix.extname(base);
13582
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
13583
+ for (const ext of extensions) {
13584
+ const hit = this.lookup(`${stem}${ext}`);
13585
+ if (hit) return hit;
13586
+ }
13587
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
13588
+ for (const ext of extensions) {
13589
+ const hit = this.lookup(path19.posix.join(base, `${entry}${ext}`));
13590
+ if (hit) return hit;
13591
+ }
13592
+ }
13593
+ return void 0;
13594
+ }
13595
+ /**
13596
+ * A representative indexed file inside `dir`, for ecosystems whose import
13597
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
13598
+ *
13599
+ * The choice is deterministic — a file named after the directory, else the
13600
+ * first by name — so the same import always produces the same edge. Package
13601
+ * grouping is unaffected either way: every file in the directory carries the
13602
+ * same package label, so the package-level edge is exact regardless of which
13603
+ * member represents it.
13604
+ */
13605
+ representativeIn(dir, family) {
13606
+ const members = this.byDir.get(path19.posix.normalize(dir).toLowerCase());
13607
+ if (!members?.length) return void 0;
13608
+ const extensions = EXTENSIONS[family] ?? [];
13609
+ const eligible = members.filter((file) => extensions.includes(path19.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
13610
+ if (eligible.length === 0) return void 0;
13611
+ const base = path19.posix.basename(path19.posix.normalize(dir)).toLowerCase();
13612
+ const named = eligible.find(
13613
+ (file) => path19.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
13614
+ );
13615
+ return named ?? eligible[0];
13616
+ }
13617
+ // ─── Per-family resolution ──────────────────────────────────────────────────
13618
+ /** Relative specifiers, then workspace package names and their subpaths. */
13619
+ resolveJs(fromFile, spec) {
13620
+ if (spec.startsWith(".")) {
13621
+ const absolute = path19.posix.join(path19.posix.dirname(fromFile), spec);
13622
+ return this.lookupWithExtensions(absolute, "js");
13623
+ }
13624
+ const owner2 = this.structure.roots.find(
13625
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
13626
+ );
13627
+ if (!owner2?.importPath) return void 0;
13628
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
13629
+ if (!subpath) {
13630
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, "src/index"), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "index"), "js");
13631
+ }
13632
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, subpath), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "src", subpath), "js");
13633
+ }
13634
+ /** Go import paths are absolute module paths; a package is a directory. */
13635
+ resolveGo(spec) {
13636
+ const owner2 = this.structure.roots.find(
13637
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
13638
+ );
13639
+ if (!owner2?.importPath) return void 0;
13640
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
13641
+ return this.representativeIn(path19.posix.join(owner2.dir, subpath), "go");
13642
+ }
13643
+ /**
13644
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
13645
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
13646
+ */
13647
+ resolvePython(fromFile, spec) {
13648
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
13649
+ if (leadingDots > 0) {
13650
+ let base = path19.posix.dirname(fromFile);
13651
+ for (let i = 1; i < leadingDots; i++) base = path19.posix.dirname(base);
13652
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
13653
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest), "py");
13654
+ }
13655
+ const segments = spec.split(".").filter(Boolean);
13656
+ if (segments.length === 0) return void 0;
13657
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
13658
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
13659
+ const hit = this.lookupWithExtensions(path19.posix.join(base, ...segments), "py");
13660
+ if (hit) return hit;
13661
+ if (segments.length > 1) {
13662
+ const parent = this.lookupWithExtensions(
13663
+ path19.posix.join(base, ...segments.slice(0, -1)),
13664
+ "py"
13665
+ );
13666
+ if (parent) return parent;
13667
+ }
13668
+ }
13669
+ return void 0;
13670
+ }
13671
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
13672
+ resolveRust(fromFile, spec) {
13673
+ const segments = spec.split("::").filter(Boolean);
13674
+ if (segments.length === 0) return void 0;
13675
+ const head = segments[0];
13676
+ if (head === "self" || head === "super") {
13677
+ let base = path19.posix.dirname(fromFile);
13678
+ for (const segment of segments) {
13679
+ if (segment === "super") base = path19.posix.dirname(base);
13680
+ else if (segment !== "self") break;
13681
+ }
13682
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
13683
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest2), "rs");
13684
+ }
13685
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
13686
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
13687
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
13688
+ );
13689
+ if (!crate) {
13690
+ return this.lookupWithExtensions(
13691
+ path19.posix.join(path19.posix.dirname(fromFile), ...segments),
13692
+ "rs"
13693
+ );
12586
13694
  }
12587
- if (!line) continue;
12588
- const anchored = line.startsWith("/") || line.includes("/");
12589
- if (line.startsWith("/")) line = line.slice(1);
12590
- const body = globBody(line);
12591
- const prefix = anchored ? "^" : "(?:^|.*/)";
12592
- rules.push({
12593
- eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
12594
- under: new RegExp(`${prefix}${body}/.*$`),
12595
- negated,
12596
- dirOnly
12597
- });
13695
+ const rest = segments.slice(1);
13696
+ for (const base of crate.sourceRoots) {
13697
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path19.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
13698
+ const exact = this.lookupWithExtensions(path19.posix.join(base, ...rest), "rs");
13699
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path19.posix.join(base, "lib"), "rs");
13700
+ if (hit) return hit;
13701
+ }
13702
+ return void 0;
12598
13703
  }
12599
- return (relPath, isDir) => {
12600
- const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
12601
- let ignored = false;
12602
- for (const r of rules) {
12603
- const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
12604
- if (re.test(p)) ignored = !r.negated;
13704
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
13705
+ resolveJvm(spec) {
13706
+ const segments = spec.split(".").filter(Boolean);
13707
+ if (segments.length === 0) return void 0;
13708
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
13709
+ const wildcard = segments[segments.length - 1] === "*";
13710
+ const parts = wildcard ? segments.slice(0, -1) : segments;
13711
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
13712
+ const target = path19.posix.join(base, ...parts);
13713
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
13714
+ if (hit) return hit;
12605
13715
  }
12606
- return ignored;
12607
- };
13716
+ return void 0;
13717
+ }
13718
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
13719
+ resolveInclude(fromFile, spec) {
13720
+ const relative12 = this.lookupWithExtensions(
13721
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
13722
+ "c"
13723
+ );
13724
+ if (relative12) return relative12;
13725
+ for (const base of [
13726
+ path19.posix.join(this.structure.projectRoot, "include"),
13727
+ this.structure.projectRoot
13728
+ ]) {
13729
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "c");
13730
+ if (hit) return hit;
13731
+ }
13732
+ return void 0;
13733
+ }
13734
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
13735
+ resolveRuby(fromFile, spec) {
13736
+ const relative12 = this.lookupWithExtensions(
13737
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
13738
+ "ruby"
13739
+ );
13740
+ if (relative12) return relative12;
13741
+ for (const base of [
13742
+ path19.posix.join(this.structure.projectRoot, "lib"),
13743
+ this.structure.projectRoot
13744
+ ]) {
13745
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "ruby");
13746
+ if (hit) return hit;
13747
+ }
13748
+ return void 0;
13749
+ }
13750
+ };
13751
+
13752
+ // src/codebase-index/import-extractor.ts
13753
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
13754
+ var IMPORT_MAX_PER_FILE = 400;
13755
+ var DOTTED_IMPORT = [
13756
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
13757
+ ];
13758
+ var LANG_IMPORTS = {
13759
+ // Go and Python have real AST extractors; these patterns are the fallback for
13760
+ // machines with no Go toolchain or Python interpreter installed, where the
13761
+ // parser degrades to regex symbols and would otherwise contribute no edges.
13762
+ go: [
13763
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
13764
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
13765
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
13766
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
13767
+ ],
13768
+ py: [
13769
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
13770
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
13771
+ ],
13772
+ rs: [
13773
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
13774
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
13775
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
13776
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
13777
+ ],
13778
+ java: DOTTED_IMPORT,
13779
+ kotlin: DOTTED_IMPORT,
13780
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
13781
+ csharp: [
13782
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
13783
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
13784
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
13785
+ ],
13786
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
13787
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
13788
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
13789
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
13790
+ php: [
13791
+ // `use A\B\C` imports the class C, which is what the index has a symbol
13792
+ // for — the namespace symbol only covers the `A\B` prefix.
13793
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
13794
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
13795
+ ],
13796
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
13797
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
13798
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
13799
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
13800
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
13801
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
13802
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
13803
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
13804
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
13805
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
13806
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
13807
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
13808
+ html: [
13809
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
13810
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
13811
+ ],
13812
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
13813
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
13814
+ };
13815
+ function lastSegment(specifier) {
13816
+ const pathLike = /[/\\]|::/.test(specifier);
13817
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
13818
+ let last = segments[segments.length - 1] ?? specifier;
13819
+ if (last === "*" || last === "_") {
13820
+ last = segments[segments.length - 2] ?? specifier;
13821
+ }
13822
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
13823
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
13824
+ return dotted[dotted.length - 1] ?? last;
12608
13825
  }
12609
- async function loadGitignoreMatcher(projectRoot) {
12610
- let lines = [];
12611
- try {
12612
- const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
12613
- lines = raw.split("\n");
12614
- } catch {
13826
+ function newlineOffsets(content) {
13827
+ const offsets = [];
13828
+ for (let i = 0; i < content.length; i++) {
13829
+ if (content.charCodeAt(i) === 10) offsets.push(i);
12615
13830
  }
12616
- return compileGitignore(lines);
13831
+ return offsets;
13832
+ }
13833
+ function lineAt(offsets, index) {
13834
+ let low = 0;
13835
+ let high = offsets.length;
13836
+ while (low < high) {
13837
+ const mid = low + high >>> 1;
13838
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
13839
+ else high = mid;
13840
+ }
13841
+ return low + 1;
13842
+ }
13843
+ function hasImportPatterns(lang) {
13844
+ return LANG_IMPORTS[lang] !== void 0;
13845
+ }
13846
+ function extractImports(opts) {
13847
+ const patterns = LANG_IMPORTS[opts.lang];
13848
+ if (!patterns || !opts.content) return [];
13849
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
13850
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
13851
+ const refs = [];
13852
+ const seen = /* @__PURE__ */ new Set();
13853
+ const offsets = newlineOffsets(content);
13854
+ for (const pattern of patterns) {
13855
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
13856
+ for (const match of content.matchAll(re)) {
13857
+ if (refs.length >= limit) return refs;
13858
+ const specifier = match[1]?.trim();
13859
+ if (!specifier) continue;
13860
+ const module = specifier;
13861
+ const toName = pattern.name === "full" ? module : lastSegment(module);
13862
+ if (!toName) continue;
13863
+ const key = `${module}\0${toName}`;
13864
+ if (seen.has(key)) continue;
13865
+ seen.add(key);
13866
+ refs.push({
13867
+ fromId: 0,
13868
+ toName,
13869
+ callType: "import",
13870
+ line: lineAt(offsets, match.index ?? 0),
13871
+ lang: opts.lang,
13872
+ module
13873
+ });
13874
+ }
13875
+ }
13876
+ return refs;
12617
13877
  }
12618
-
12619
- // src/codebase-index/indexer.ts
12620
- init_languages2();
12621
13878
 
12622
13879
  // src/codebase-index/parser-dispatch.ts
12623
13880
  async function parseFileContent(file, content, lang) {
13881
+ const parsed = await dispatch(file, content, lang);
13882
+ return withRelations(parsed, content, lang);
13883
+ }
13884
+ async function dispatch(file, content, lang) {
12624
13885
  switch (lang) {
12625
13886
  case "ts":
12626
13887
  case "tsx":
@@ -12655,6 +13916,13 @@ async function parseFileContent(file, content, lang) {
12655
13916
  }
12656
13917
  }
12657
13918
  }
13919
+ function withRelations(parsed, content, lang) {
13920
+ let refs = parsed.refs ?? [];
13921
+ if (refs.length === 0 && hasImportPatterns(lang)) {
13922
+ refs = extractImports({ content, lang });
13923
+ }
13924
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
13925
+ }
12658
13926
 
12659
13927
  // src/codebase-index/indexer.ts
12660
13928
  var YIELD_EVERY_N = 50;
@@ -12691,7 +13959,7 @@ function normalizeComparablePath(value) {
12691
13959
  }
12692
13960
  function gitOutput(projectRoot, args) {
12693
13961
  return new Promise((resolve16, reject) => {
12694
- execFile2(
13962
+ execFile(
12695
13963
  "git",
12696
13964
  ["-C", projectRoot, ...args],
12697
13965
  {
@@ -12822,13 +14090,40 @@ function assignRefsToSymbols2(refs, symbols) {
12822
14090
  }
12823
14091
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12824
14092
  if (!owner2 || owner2.id <= 0) continue;
12825
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
14093
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
12826
14094
  if (seen.has(key)) continue;
12827
14095
  seen.add(key);
12828
14096
  assigned.push({ ...ref, fromId: owner2.id });
12829
14097
  }
12830
14098
  return assigned;
12831
14099
  }
14100
+ async function resolveProjectRelations(store, projectRoot, opts) {
14101
+ if (opts.signal?.aborted) return;
14102
+ try {
14103
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
14104
+ if (indexedFiles.length === 0) return;
14105
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
14106
+ if (opts.signal?.aborted) return;
14107
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
14108
+ const resolver = new ModuleResolver(
14109
+ structure,
14110
+ indexedFiles,
14111
+ store.getNamespaceDeclarations()
14112
+ );
14113
+ const pending2 = store.getUnresolvedImports(opts.onlyFiles);
14114
+ const resolutions = [];
14115
+ for (const entry of pending2) {
14116
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
14117
+ if (toFile && toFile !== entry.fromFile) {
14118
+ resolutions.push({ ...entry, toFile });
14119
+ }
14120
+ }
14121
+ if (opts.signal?.aborted) return;
14122
+ store.applyImportResolutions(resolutions);
14123
+ } catch (err) {
14124
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
14125
+ }
14126
+ }
12832
14127
  async function runIndexerWithStore(store, opts) {
12833
14128
  const { projectRoot, langs, ignore = [], signal } = opts;
12834
14129
  const relationGraphVersion = "2";
@@ -13073,6 +14368,14 @@ async function runIndexerWithStore(store, opts) {
13073
14368
  }
13074
14369
  }
13075
14370
  if (needsFullRefResolution) store.resolveRefs();
14371
+ await resolveProjectRelations(store, projectRoot, {
14372
+ // A watcher run re-resolves only what it touched; a full run (or a contract
14373
+ // bump) re-resolves everything, because a newly indexed file can be the
14374
+ // target of imports written long before it.
14375
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
14376
+ errors,
14377
+ signal
14378
+ });
13076
14379
  store.setMetadata("ref_resolution_version", refResolutionVersion);
13077
14380
  store.setMetadata("relation_graph_version", relationGraphVersion);
13078
14381
  if (!opts.files || filesIndexed >= 50) store.optimize();
@@ -13155,6 +14458,22 @@ function symbolGraphService(args) {
13155
14458
  indexStorePool.release(store);
13156
14459
  }
13157
14460
  }
14461
+ function incomingCallsService(args) {
14462
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
14463
+ try {
14464
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
14465
+ } finally {
14466
+ indexStorePool.release(store);
14467
+ }
14468
+ }
14469
+ function outgoingCallsService(args) {
14470
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
14471
+ try {
14472
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
14473
+ } finally {
14474
+ indexStorePool.release(store);
14475
+ }
14476
+ }
13158
14477
 
13159
14478
  // src/codebase-index/background-indexer.ts
13160
14479
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
@@ -13366,6 +14685,10 @@ async function callInline(op, args, opts) {
13366
14685
  return fileGraphService(args);
13367
14686
  case "symbolGraph":
13368
14687
  return symbolGraphService(args);
14688
+ case "incomingCalls":
14689
+ return incomingCallsService(args);
14690
+ case "outgoingCalls":
14691
+ return outgoingCallsService(args);
13369
14692
  default:
13370
14693
  throw new Error(`unknown index op: ${String(op)}`);
13371
14694
  }
@@ -13461,6 +14784,12 @@ async function codebaseIndexStats(args, opts = {}) {
13461
14784
  signal: opts.signal
13462
14785
  });
13463
14786
  }
14787
+ async function incomingCallsService2(args) {
14788
+ return callIndexOp("incomingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
14789
+ }
14790
+ async function outgoingCallsService2(args) {
14791
+ return callIndexOp("outgoingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
14792
+ }
13464
14793
 
13465
14794
  // src/codebase-index/codebase-index-tool.ts
13466
14795
  var codebaseIndexTool = {
@@ -13525,6 +14854,214 @@ var codebaseIndexTool = {
13525
14854
  }
13526
14855
  };
13527
14856
 
14857
+ // src/codebase-index/codebase-incoming-calls-tool.ts
14858
+ var codebaseIncomingCallsTool = {
14859
+ name: "codebase-incoming-calls",
14860
+ category: "Project",
14861
+ icon: "index",
14862
+ description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Always use this instead of grep when checking impact of a change.",
14863
+ usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- NEVER use grep or manual line reading to check where a function is called.\n- ALWAYS call codebase-incoming-calls({ symbol: "funcName" }) first.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
14864
+ permission: "auto",
14865
+ mutating: false,
14866
+ capabilities: ["fs.read"],
14867
+ timeoutMs: 35e3,
14868
+ inputSchema: {
14869
+ type: "object",
14870
+ properties: {
14871
+ symbol: {
14872
+ type: "string",
14873
+ description: "The function/method/type name to find callers for"
14874
+ },
14875
+ file: {
14876
+ type: "string",
14877
+ description: "Scope to a specific file when multiple symbols share the same name"
14878
+ },
14879
+ limit: {
14880
+ type: "integer",
14881
+ description: "Maximum call sites to return (default 50, max 200)",
14882
+ minimum: 1,
14883
+ maximum: 200
14884
+ }
14885
+ },
14886
+ required: ["symbol"]
14887
+ },
14888
+ async execute(input, ctx) {
14889
+ const state = getIndexState();
14890
+ if (state.indexing && !state.ready) {
14891
+ return {
14892
+ symbol: input.symbol,
14893
+ calls: [],
14894
+ total: 0,
14895
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
14896
+ };
14897
+ }
14898
+ if (state.lastError) {
14899
+ const circuit = state.circuit;
14900
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
14901
+ return {
14902
+ symbol: input.symbol,
14903
+ calls: [],
14904
+ total: 0,
14905
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
14906
+ };
14907
+ }
14908
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
14909
+ const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
14910
+ {
14911
+ projectRoot: ctx.projectRoot,
14912
+ indexDir: codebaseIndexDirOverride(ctx),
14913
+ symbol: input.symbol,
14914
+ file: input.file,
14915
+ limit
14916
+ }
14917
+ );
14918
+ if (!symbolFound) {
14919
+ let hasPersistedIndex = state.ready;
14920
+ if (!hasPersistedIndex) {
14921
+ try {
14922
+ const stats = await codebaseIndexStats({
14923
+ projectRoot: ctx.projectRoot,
14924
+ indexDir: codebaseIndexDirOverride(ctx)
14925
+ });
14926
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
14927
+ } catch {
14928
+ }
14929
+ }
14930
+ if (!hasPersistedIndex) {
14931
+ return {
14932
+ symbol: input.symbol,
14933
+ calls: [],
14934
+ total: 0,
14935
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
14936
+ };
14937
+ }
14938
+ return {
14939
+ symbol: input.symbol,
14940
+ calls: [],
14941
+ total: 0,
14942
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
14943
+ };
14944
+ }
14945
+ const notes = [];
14946
+ if (totalMatches > limit) {
14947
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
14948
+ }
14949
+ if (ambiguous) {
14950
+ notes.push(`Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`);
14951
+ }
14952
+ return {
14953
+ symbol: input.symbol,
14954
+ calls,
14955
+ total: calls.length,
14956
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
14957
+ };
14958
+ }
14959
+ };
14960
+
14961
+ // src/codebase-index/codebase-outgoing-calls-tool.ts
14962
+ var codebaseOutgoingCallsTool = {
14963
+ name: "codebase-outgoing-calls",
14964
+ category: "Project",
14965
+ icon: "index",
14966
+ description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
14967
+ usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
14968
+ permission: "auto",
14969
+ mutating: false,
14970
+ capabilities: ["fs.read"],
14971
+ timeoutMs: 35e3,
14972
+ inputSchema: {
14973
+ type: "object",
14974
+ properties: {
14975
+ symbol: {
14976
+ type: "string",
14977
+ description: "The function/method/type name to find callees for"
14978
+ },
14979
+ file: {
14980
+ type: "string",
14981
+ description: "Scope to a specific file when multiple symbols share the same name"
14982
+ },
14983
+ limit: {
14984
+ type: "integer",
14985
+ description: "Maximum call sites to return (default 50, max 200)",
14986
+ minimum: 1,
14987
+ maximum: 200
14988
+ }
14989
+ },
14990
+ required: ["symbol"]
14991
+ },
14992
+ async execute(input, ctx) {
14993
+ const state = getIndexState();
14994
+ if (state.indexing && !state.ready) {
14995
+ return {
14996
+ symbol: input.symbol,
14997
+ calls: [],
14998
+ total: 0,
14999
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
15000
+ };
15001
+ }
15002
+ if (state.lastError) {
15003
+ const circuit = state.circuit;
15004
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
15005
+ return {
15006
+ symbol: input.symbol,
15007
+ calls: [],
15008
+ total: 0,
15009
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
15010
+ };
15011
+ }
15012
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
15013
+ const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
15014
+ {
15015
+ projectRoot: ctx.projectRoot,
15016
+ indexDir: codebaseIndexDirOverride(ctx),
15017
+ symbol: input.symbol,
15018
+ file: input.file,
15019
+ limit
15020
+ }
15021
+ );
15022
+ if (!symbolFound) {
15023
+ let hasPersistedIndex = state.ready;
15024
+ if (!hasPersistedIndex) {
15025
+ try {
15026
+ const stats = await codebaseIndexStats({
15027
+ projectRoot: ctx.projectRoot,
15028
+ indexDir: codebaseIndexDirOverride(ctx)
15029
+ });
15030
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
15031
+ } catch {
15032
+ }
15033
+ }
15034
+ if (!hasPersistedIndex) {
15035
+ return {
15036
+ symbol: input.symbol,
15037
+ calls: [],
15038
+ total: 0,
15039
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
15040
+ };
15041
+ }
15042
+ return {
15043
+ symbol: input.symbol,
15044
+ calls: [],
15045
+ total: 0,
15046
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
15047
+ };
15048
+ }
15049
+ const notes = [];
15050
+ if (totalMatches > limit) {
15051
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
15052
+ }
15053
+ if (unresolvedCount > 0) {
15054
+ notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
15055
+ }
15056
+ return {
15057
+ symbol: input.symbol,
15058
+ calls,
15059
+ total: calls.length,
15060
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
15061
+ };
15062
+ }
15063
+ };
15064
+
13528
15065
  // src/codebase-index/codebase-search-tool.ts
13529
15066
  var codebaseSearchTool = {
13530
15067
  name: "codebase-search",
@@ -14245,17 +15782,17 @@ import {
14245
15782
  } from "@wrongstack/core/design";
14246
15783
  async function resolveReal(p) {
14247
15784
  const resolved = path25.resolve(p);
14248
- let probe2 = resolved;
15785
+ let probe = resolved;
14249
15786
  const missing = [];
14250
15787
  for (; ; ) {
14251
15788
  try {
14252
- return path25.resolve(await fs20.realpath(probe2), ...missing);
15789
+ return path25.resolve(await fs20.realpath(probe), ...missing);
14253
15790
  } catch (err) {
14254
15791
  if (err.code === "ENOENT") {
14255
- const parent = path25.dirname(probe2);
14256
- if (parent === probe2) return resolved;
14257
- missing.unshift(path25.basename(probe2));
14258
- probe2 = parent;
15792
+ const parent = path25.dirname(probe);
15793
+ if (parent === probe) return resolved;
15794
+ missing.unshift(path25.basename(probe));
15795
+ probe = parent;
14259
15796
  continue;
14260
15797
  }
14261
15798
  return resolved;
@@ -14530,7 +16067,7 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
14530
16067
 
14531
16068
  // src/diff.ts
14532
16069
  init_util();
14533
- import { spawn as spawn8 } from "node:child_process";
16070
+ import { spawn as spawn7 } from "node:child_process";
14534
16071
  import { statSync as statSync3 } from "node:fs";
14535
16072
  import * as fs21 from "node:fs/promises";
14536
16073
  import * as path26 from "node:path";
@@ -14633,7 +16170,7 @@ function runGit(args, cwd, signal) {
14633
16170
  return new Promise((resolve16) => {
14634
16171
  let stdout = "";
14635
16172
  let stderr = "";
14636
- const child = spawn8("git", args, {
16173
+ const child = spawn7("git", args, {
14637
16174
  cwd,
14638
16175
  signal,
14639
16176
  env: buildChildEnv3(),
@@ -14844,7 +16381,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
14844
16381
 
14845
16382
  // src/e2e.ts
14846
16383
  init_util();
14847
- import { open, readdir as readdir6 } from "node:fs/promises";
16384
+ import { open, readdir as readdir7 } from "node:fs/promises";
14848
16385
  import * as path27 from "node:path";
14849
16386
  async function readBoundedText(filePath, maxBytes) {
14850
16387
  let handle;
@@ -14962,7 +16499,7 @@ async function scanWorkspace(root, maxDepth, signal) {
14962
16499
  }
14963
16500
  let entries;
14964
16501
  try {
14965
- entries = await readdir6(current.directory, { withFileTypes: true });
16502
+ entries = await readdir7(current.directory, { withFileTypes: true });
14966
16503
  } catch {
14967
16504
  continue;
14968
16505
  }
@@ -15021,7 +16558,7 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
15021
16558
  while (true) {
15022
16559
  const names = /* @__PURE__ */ new Set();
15023
16560
  try {
15024
- for (const entry of await readdir6(directory)) names.add(entry);
16561
+ for (const entry of await readdir7(directory)) names.add(entry);
15025
16562
  } catch {
15026
16563
  }
15027
16564
  if (names.has("pnpm-lock.yaml")) return "pnpm";
@@ -15089,7 +16626,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
15089
16626
  if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
15090
16627
  let entries;
15091
16628
  try {
15092
- entries = await readdir6(directory, { withFileTypes: true });
16629
+ entries = await readdir7(directory, { withFileTypes: true });
15093
16630
  } catch {
15094
16631
  continue;
15095
16632
  }
@@ -15290,7 +16827,7 @@ function findLadderMatches(fileLf, oldLf) {
15290
16827
  const exact = [];
15291
16828
  let idx = fileLf.indexOf(oldLf);
15292
16829
  while (idx !== -1) {
15293
- exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
16830
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt2(fileLf, idx) });
15294
16831
  idx = fileLf.indexOf(oldLf, idx + 1);
15295
16832
  }
15296
16833
  if (exact.length > 0) return { tier: "exact", matches: exact };
@@ -15322,7 +16859,7 @@ function findLadderMatches(fileLf, oldLf) {
15322
16859
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
15323
16860
  return fuzzyScan(fileLines, needleLines, offsets);
15324
16861
  }
15325
- function lineAt(text, pos) {
16862
+ function lineAt2(text, pos) {
15326
16863
  if (pos < 512) {
15327
16864
  let line2 = 1;
15328
16865
  for (let i = 0; i < pos; i++) {
@@ -15784,7 +17321,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
15784
17321
  };
15785
17322
 
15786
17323
  // src/exec.ts
15787
- import { spawn as spawn9 } from "node:child_process";
17324
+ import { spawn as spawn8 } from "node:child_process";
15788
17325
  import {
15789
17326
  emitProcessCompleted as emitProcessCompleted3,
15790
17327
  emitProcessOutput as emitProcessOutput3,
@@ -17001,6 +18538,26 @@ var BLOCKED_ARG_PATTERNS = {
17001
18538
  pnpm: [],
17002
18539
  npx: []
17003
18540
  };
18541
+ var BLOCKED_OPTION_NAMES = {
18542
+ git: /* @__PURE__ */ new Set([
18543
+ "--exec",
18544
+ "--upload-pack",
18545
+ "--receive-pack",
18546
+ "--exec-path",
18547
+ "--git-dir",
18548
+ "--work-tree",
18549
+ "--namespace",
18550
+ "-c",
18551
+ "--config",
18552
+ "--config-env",
18553
+ "-C"
18554
+ ]),
18555
+ find: /* @__PURE__ */ new Set(["-exec", "-ok", "-execdir"])
18556
+ };
18557
+ function optionName(arg) {
18558
+ const eq = arg.indexOf("=");
18559
+ return eq > 0 ? arg.slice(0, eq) : arg;
18560
+ }
17004
18561
  var BLOCKED_SUBCOMMANDS = {
17005
18562
  docker: /* @__PURE__ */ new Set(["push"]),
17006
18563
  podman: /* @__PURE__ */ new Set(["push"]),
@@ -17038,6 +18595,15 @@ function validateArgs(cmd, args) {
17038
18595
  const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
17039
18596
  if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
17040
18597
  }
18598
+ const blockedOptions = BLOCKED_OPTION_NAMES[cmd];
18599
+ if (blockedOptions) {
18600
+ for (const arg of args) {
18601
+ if (arg === "--") break;
18602
+ if (blockedOptions.has(optionName(arg))) {
18603
+ return `Blocked option "${optionName(arg)}" for command "${cmd}"`;
18604
+ }
18605
+ }
18606
+ }
17041
18607
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
17042
18608
  if (!blocked) return null;
17043
18609
  for (const arg of args) {
@@ -17220,7 +18786,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
17220
18786
  };
17221
18787
  let child;
17222
18788
  try {
17223
- child = spawn9(spawnCmd, spawnArgs, {
18789
+ child = spawn8(spawnCmd, spawnArgs, {
17224
18790
  cwd,
17225
18791
  env: buildChildEnv2(sessionId),
17226
18792
  stdio: ["ignore", "pipe", "pipe"],
@@ -17854,7 +19420,7 @@ async function detectFixer(cwd) {
17854
19420
 
17855
19421
  // src/git.ts
17856
19422
  init_util();
17857
- import { spawn as spawn10 } from "node:child_process";
19423
+ import { spawn as spawn9 } from "node:child_process";
17858
19424
  import { statSync as statSync4 } from "node:fs";
17859
19425
  import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
17860
19426
  import { assessCommitSafety } from "@wrongstack/core/coordination";
@@ -18118,7 +19684,7 @@ function runGit2(args, cwd, signal) {
18118
19684
  return new Promise((resolve16) => {
18119
19685
  let stdout = "";
18120
19686
  let stderr = "";
18121
- const child = spawn10("git", args, {
19687
+ const child = spawn9("git", args, {
18122
19688
  cwd,
18123
19689
  signal,
18124
19690
  env: buildChildEnv4(),
@@ -18309,7 +19875,7 @@ var globTool = {
18309
19875
  };
18310
19876
 
18311
19877
  // src/grep.ts
18312
- import { spawn as spawn11 } from "node:child_process";
19878
+ import { spawn as spawn10 } from "node:child_process";
18313
19879
  import * as fs25 from "node:fs/promises";
18314
19880
  import * as path31 from "node:path";
18315
19881
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
@@ -18466,7 +20032,7 @@ var grepTool = {
18466
20032
  async function detectRg(signal) {
18467
20033
  return new Promise((resolve16) => {
18468
20034
  try {
18469
- const p = spawn11("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
20035
+ const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
18470
20036
  p.on("error", () => resolve16(false));
18471
20037
  p.on("close", (code) => resolve16(code === 0));
18472
20038
  } catch {
@@ -18500,7 +20066,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
18500
20066
  const FLUSH_AT = 16;
18501
20067
  const MAX_BUF_BYTES = 1e6;
18502
20068
  let bufOverflow = false;
18503
- const child = spawn11("rg", args, {
20069
+ const child = spawn10("rg", args, {
18504
20070
  signal,
18505
20071
  env: buildChildEnv5(),
18506
20072
  // rg diagnostics are not part of the tool result. Ignoring stderr avoids
@@ -18797,7 +20363,7 @@ async function runNative(input, base, mode, limit, signal) {
18797
20363
  init_spawn_stream();
18798
20364
  init_util();
18799
20365
  init_legacy_bridge();
18800
- import { join as join25 } from "node:path";
20366
+ import { join as join24 } from "node:path";
18801
20367
  import {
18802
20368
  detectEcosystem as detectPackageEcosystem,
18803
20369
  recordPackageAction
@@ -18981,17 +20547,17 @@ function resolveManifestPath(cwd, pkgManager) {
18981
20547
  case "pnpm":
18982
20548
  case "yarn":
18983
20549
  case "npm":
18984
- return join25(cwd, "package.json");
20550
+ return join24(cwd, "package.json");
18985
20551
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
18986
20552
  default:
18987
- return join25(cwd, "package.json");
20553
+ return join24(cwd, "package.json");
18988
20554
  }
18989
20555
  }
18990
20556
 
18991
20557
  // src/json.ts
18992
- init_util();
18993
20558
  import * as fs26 from "node:fs/promises";
18994
20559
  import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
20560
+ init_util();
18995
20561
  var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
18996
20562
  var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
18997
20563
  var JsonFileTooLargeError = class extends Error {
@@ -19430,8 +20996,12 @@ function validateJsonSchema(data, schema) {
19430
20996
  }
19431
20997
  }
19432
20998
  if (typeof value === "string" && s["pattern"]) {
19433
- const re = new RegExp(s["pattern"]);
19434
- if (!re.test(value)) errors.push(`${path38}: does not match pattern ${s["pattern"]}`);
20999
+ const compiled = compileUserRegex(s["pattern"], "");
21000
+ if (!compiled.ok) {
21001
+ errors.push(`${path38}: invalid schema pattern \u2014 ${compiled.reason}`);
21002
+ } else if (!compiled.regex.test(capSubject(value))) {
21003
+ errors.push(`${path38}: does not match pattern ${s["pattern"]}`);
21004
+ }
19435
21005
  }
19436
21006
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
19437
21007
  errors.push(`${path38}: string too short (min ${s["minLength"]})`);
@@ -21416,7 +22986,7 @@ async function detectLinter(cwd) {
21416
22986
  }
21417
22987
 
21418
22988
  // src/logs.ts
21419
- import { spawn as spawn12 } from "node:child_process";
22989
+ import { spawn as spawn11 } from "node:child_process";
21420
22990
  import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
21421
22991
  init_util();
21422
22992
  var logsTool = {
@@ -21523,7 +23093,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
21523
23093
  clearTimeout(timer);
21524
23094
  resolve16(result);
21525
23095
  };
21526
- const child = spawn12("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
23096
+ const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
21527
23097
  const timer = setTimeout(() => {
21528
23098
  child.kill("SIGTERM");
21529
23099
  finish(empty());
@@ -21632,7 +23202,7 @@ function parseLine(line) {
21632
23202
  // src/outdated.ts
21633
23203
  init_util();
21634
23204
  init_win32_resolve();
21635
- import { spawn as spawn13 } from "node:child_process";
23205
+ import { spawn as spawn12 } from "node:child_process";
21636
23206
  import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
21637
23207
  var outdatedTool = {
21638
23208
  name: "outdated",
@@ -21752,7 +23322,7 @@ function runOutdated(manager, args, cwd, signal) {
21752
23322
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
21753
23323
  const spawnCmd = shim?.command ?? resolved;
21754
23324
  const spawnArgs = shim?.args ?? args;
21755
- const child = spawn13(spawnCmd, spawnArgs, {
23325
+ const child = spawn12(spawnCmd, spawnArgs, {
21756
23326
  cwd,
21757
23327
  signal,
21758
23328
  env: buildChildEnv7(),
@@ -21818,7 +23388,7 @@ function parseOutdatedOutput(json2, exitCode) {
21818
23388
 
21819
23389
  // src/patch.ts
21820
23390
  init_util();
21821
- import { spawn as spawn14 } from "node:child_process";
23391
+ import { spawn as spawn13 } from "node:child_process";
21822
23392
  import * as fs27 from "node:fs/promises";
21823
23393
  import * as os9 from "node:os";
21824
23394
  import * as path32 from "node:path";
@@ -21960,7 +23530,7 @@ function runPatch(args, cwd, signal) {
21960
23530
  let stdout = "";
21961
23531
  let stderr = "";
21962
23532
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
21963
- const child = spawn14("patch", args, {
23533
+ const child = spawn13("patch", args, {
21964
23534
  cwd,
21965
23535
  signal,
21966
23536
  env,
@@ -22557,7 +24127,7 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
22557
24127
  }
22558
24128
 
22559
24129
  // src/replace.ts
22560
- import { spawn as spawn15 } from "node:child_process";
24130
+ import { spawn as spawn14 } from "node:child_process";
22561
24131
  import * as fs29 from "node:fs/promises";
22562
24132
  import * as path33 from "node:path";
22563
24133
  import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
@@ -22742,7 +24312,7 @@ async function globFiles(pattern, base, extraGlob) {
22742
24312
  function checkRg() {
22743
24313
  return new Promise((resolve16) => {
22744
24314
  try {
22745
- const p = spawn15("rg", ["--version"], {
24315
+ const p = spawn14("rg", ["--version"], {
22746
24316
  env: buildChildEnv9(),
22747
24317
  stdio: "ignore",
22748
24318
  windowsHide: true
@@ -22756,7 +24326,7 @@ function checkRg() {
22756
24326
  }
22757
24327
  function spawnRgFind(pattern, base) {
22758
24328
  const args = ["--files", "--glob", pattern, base];
22759
- const child = spawn15("rg", args, {
24329
+ const child = spawn14("rg", args, {
22760
24330
  signal: AbortSignal.timeout(3e4),
22761
24331
  env: buildChildEnv9(),
22762
24332
  stdio: ["ignore", "pipe", "pipe"],
@@ -24795,7 +26365,7 @@ var writeTool = {
24795
26365
  required: ["path", "content"]
24796
26366
  },
24797
26367
  async execute(input, ctx, opts) {
24798
- return writeFile7(input, ctx, opts?.signal);
26368
+ return writeFile6(input, ctx, opts?.signal);
24799
26369
  },
24800
26370
  async *executeStream(input, ctx, opts) {
24801
26371
  const prepared = await prepareWrite(input, ctx);
@@ -24808,7 +26378,7 @@ var writeTool = {
24808
26378
  yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
24809
26379
  }
24810
26380
  };
24811
- async function writeFile7(input, ctx, signal) {
26381
+ async function writeFile6(input, ctx, signal) {
24812
26382
  return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
24813
26383
  }
24814
26384
  async function prepareWrite(input, ctx) {
@@ -24896,6 +26466,8 @@ var TIER1_TOOLS = [
24896
26466
  editTool,
24897
26467
  codebaseStatsTool,
24898
26468
  codebaseSearchTool,
26469
+ codebaseIncomingCallsTool,
26470
+ codebaseOutgoingCallsTool,
24899
26471
  codebaseIndexTool,
24900
26472
  bashTool,
24901
26473
  grepTool,
@@ -24946,6 +26518,8 @@ var builtinTools = [
24946
26518
  editTool,
24947
26519
  codebaseStatsTool,
24948
26520
  codebaseSearchTool,
26521
+ codebaseIncomingCallsTool,
26522
+ codebaseOutgoingCallsTool,
24949
26523
  codebaseIndexTool,
24950
26524
  deadCodeScanTool,
24951
26525
  replaceTool,