@wrongstack/tools 0.299.0 → 0.300.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -724,7 +724,7 @@ var init_process_registry = __esm({
724
724
  const p = this.processes.get(pid);
725
725
  if (!p) return false;
726
726
  if (p.killed) return true;
727
- if (p.protected) return false;
727
+ if (p.protected && opts.includeProtected !== true) return false;
728
728
  if (opts.preserveBackground && p.background) return false;
729
729
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
730
730
  const isWin5 = os.platform() === "win32";
@@ -775,9 +775,13 @@ var init_process_registry = __esm({
775
775
  killAll(opts = {}) {
776
776
  const pids = Array.from(this.processes.keys());
777
777
  const killed = [];
778
+ const includeProtected = opts.includeProtected === true;
778
779
  for (const pid of pids) {
779
780
  const p = this.processes.get(pid);
780
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
781
+ if (!p) continue;
782
+ if (p.protected && !includeProtected) continue;
783
+ if (opts.preserveBackground && p.background) continue;
784
+ if (this.kill(pid, opts)) killed.push(pid);
781
785
  }
782
786
  return killed;
783
787
  }
@@ -1141,18 +1145,18 @@ async function resolveRealInsideRoot(absPath, ctx) {
1141
1145
  const realRoots = await Promise.all(
1142
1146
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1143
1147
  );
1144
- let probe2 = absPath;
1148
+ let probe = absPath;
1145
1149
  const pendingTail = [];
1146
1150
  for (; ; ) {
1147
1151
  let real;
1148
1152
  try {
1149
- real = await fsp2.realpath(probe2);
1153
+ real = await fsp2.realpath(probe);
1150
1154
  } catch (err) {
1151
1155
  if (err.code === "ENOENT") {
1152
- const parent = path3.dirname(probe2);
1153
- if (parent === probe2) return absPath;
1154
- pendingTail.unshift(path3.basename(probe2));
1155
- probe2 = parent;
1156
+ const parent = path3.dirname(probe);
1157
+ if (parent === probe) return absPath;
1158
+ pendingTail.unshift(path3.basename(probe));
1159
+ probe = parent;
1156
1160
  continue;
1157
1161
  }
1158
1162
  throw err;
@@ -4971,23 +4975,26 @@ var init_legacy_bridge = __esm({
4971
4975
  });
4972
4976
 
4973
4977
  // src/codebase-index/languages.ts
4974
- import * as path18 from "node:path";
4978
+ import * as path13 from "node:path";
4975
4979
  function detectLang(file) {
4976
- const base = path18.basename(file);
4980
+ const base = path13.basename(file);
4977
4981
  const lowerBase = base.toLowerCase();
4978
4982
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4979
4983
  return "ts";
4980
4984
  }
4981
4985
  const special = SPECIAL_FILENAMES[lowerBase];
4982
4986
  if (special) return special;
4983
- const ext = path18.extname(base).toLowerCase();
4987
+ const ext = path13.extname(base).toLowerCase();
4984
4988
  if (!ext) return null;
4985
4989
  return EXT_TO_LANG[ext] ?? null;
4986
4990
  }
4987
4991
  function isIndexablePath(file) {
4988
4992
  return detectLang(file) !== null;
4989
4993
  }
4990
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
4994
+ function languageFamily(lang) {
4995
+ return LANG_FAMILY[lang] ?? "other";
4996
+ }
4997
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
4991
4998
  var init_languages2 = __esm({
4992
4999
  "src/codebase-index/languages.ts"() {
4993
5000
  "use strict";
@@ -5078,6 +5085,52 @@ var init_languages2 = __esm({
5078
5085
  procfile: "other",
5079
5086
  justfile: "other"
5080
5087
  };
5088
+ LANG_FAMILY = {
5089
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
5090
+ // imports from — and is imported by — plain .ts files.
5091
+ ts: "js",
5092
+ tsx: "js",
5093
+ js: "js",
5094
+ jsx: "js",
5095
+ vue: "js",
5096
+ svelte: "js",
5097
+ go: "go",
5098
+ py: "py",
5099
+ rs: "rs",
5100
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
5101
+ java: "jvm",
5102
+ kotlin: "jvm",
5103
+ scala: "jvm",
5104
+ csharp: "dotnet",
5105
+ // A .h header is consumed by both C and C++ translation units.
5106
+ c: "c",
5107
+ cpp: "c",
5108
+ ruby: "ruby",
5109
+ php: "php",
5110
+ swift: "swift",
5111
+ dart: "dart",
5112
+ elixir: "elixir",
5113
+ haskell: "haskell",
5114
+ zig: "zig",
5115
+ lua: "lua",
5116
+ r: "r",
5117
+ shell: "shell",
5118
+ sql: "sql",
5119
+ json: "data",
5120
+ yaml: "data",
5121
+ toml: "data",
5122
+ html: "web",
5123
+ css: "web",
5124
+ proto: "proto",
5125
+ graphql: "graphql",
5126
+ md: "other",
5127
+ other: "other"
5128
+ };
5129
+ LANG_FAMILY_ENTRIES = Object.freeze(
5130
+ Object.entries(LANG_FAMILY).map(
5131
+ ([lang, family]) => Object.freeze([lang, family])
5132
+ )
5133
+ );
5081
5134
  }
5082
5135
  });
5083
5136
 
@@ -5242,7 +5295,7 @@ function getTypeName(name) {
5242
5295
  function deduplicateRefs(refs) {
5243
5296
  const seen = /* @__PURE__ */ new Set();
5244
5297
  return refs.filter((r) => {
5245
- const key = `${r.toName}:${r.callType}:${r.line}`;
5298
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
5246
5299
  if (seen.has(key)) return false;
5247
5300
  seen.add(key);
5248
5301
  return true;
@@ -5252,10 +5305,16 @@ function getImportSpecifierName(spec) {
5252
5305
  return spec.propertyName?.text ?? spec.name.text;
5253
5306
  }
5254
5307
  function emitImportSpecifierRefs(node, refs, lineNum) {
5308
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5255
5309
  const clause = node.importClause;
5256
- if (!clause) return;
5310
+ if (!clause) {
5311
+ if (module) {
5312
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5313
+ }
5314
+ return;
5315
+ }
5257
5316
  if (clause.name) {
5258
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5317
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5259
5318
  }
5260
5319
  const bindings2 = clause.namedBindings;
5261
5320
  if (!bindings2) return;
@@ -5265,26 +5324,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
5265
5324
  fromId: 0,
5266
5325
  toName: getImportSpecifierName(element),
5267
5326
  callType: "import",
5268
- line: lineNum
5327
+ line: lineNum,
5328
+ module
5269
5329
  });
5270
5330
  }
5271
5331
  } else if (ts.isNamespaceImport(bindings2)) {
5272
- refs.push({ fromId: 0, toName: bindings2.name.text, callType: "import", line: lineNum });
5332
+ refs.push({
5333
+ fromId: 0,
5334
+ toName: bindings2.name.text,
5335
+ callType: "import",
5336
+ line: lineNum,
5337
+ module
5338
+ });
5273
5339
  }
5274
5340
  }
5341
+ function moduleSpecifierOf(node) {
5342
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
5343
+ }
5275
5344
  function emitExportSpecifierRefs(node, refs, lineNum) {
5345
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5276
5346
  const clause = node.exportClause;
5277
5347
  if (clause && ts.isNamespaceExport(clause)) {
5278
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5348
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5279
5349
  return;
5280
5350
  }
5281
5351
  if (clause && ts.isNamedExports(clause)) {
5282
5352
  for (const element of clause.elements) {
5283
5353
  const originalName = element.propertyName?.text ?? element.name.text;
5284
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
5354
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
5285
5355
  }
5286
5356
  return;
5287
5357
  }
5358
+ if (module) {
5359
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5360
+ }
5288
5361
  }
5289
5362
  var ts, tsLoad, kindMapCache;
5290
5363
  var init_ts_parser = __esm({
@@ -5296,6 +5369,82 @@ var init_ts_parser = __esm({
5296
5369
  }
5297
5370
  });
5298
5371
 
5372
+ // src/codebase-index/parser-output.ts
5373
+ function coerceSymbols(value) {
5374
+ if (!Array.isArray(value)) return [];
5375
+ return value.flatMap((entry) => {
5376
+ const candidate = entry;
5377
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
5378
+ return [
5379
+ {
5380
+ name: candidate.name,
5381
+ kind: candidate.kind,
5382
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5383
+ col: typeof candidate.col === "number" ? candidate.col : 0,
5384
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
5385
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
5386
+ }
5387
+ ];
5388
+ });
5389
+ }
5390
+ function coerceRefs(value, lang) {
5391
+ if (!Array.isArray(value)) return [];
5392
+ return value.flatMap((entry) => {
5393
+ const candidate = entry;
5394
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
5395
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
5396
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
5397
+ return [
5398
+ {
5399
+ fromId: 0,
5400
+ toName: candidate.toName,
5401
+ callType: candidate.callType,
5402
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5403
+ lang,
5404
+ module
5405
+ }
5406
+ ];
5407
+ });
5408
+ }
5409
+ function parseParserOutput(stdout, lang) {
5410
+ const trimmed = stdout.trim();
5411
+ if (!trimmed) return { symbols: [], refs: [] };
5412
+ let parsed;
5413
+ try {
5414
+ parsed = JSON.parse(trimmed);
5415
+ } catch {
5416
+ return { symbols: [], refs: [] };
5417
+ }
5418
+ if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
5419
+ const record = parsed;
5420
+ return {
5421
+ symbols: coerceSymbols(record.symbols),
5422
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
5423
+ };
5424
+ }
5425
+ function dedupeRefs(refs) {
5426
+ const seen = /* @__PURE__ */ new Set();
5427
+ return refs.filter((ref) => {
5428
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
5429
+ if (seen.has(key)) return false;
5430
+ seen.add(key);
5431
+ return true;
5432
+ });
5433
+ }
5434
+ var CALL_TYPES;
5435
+ var init_parser_output = __esm({
5436
+ "src/codebase-index/parser-output.ts"() {
5437
+ "use strict";
5438
+ CALL_TYPES = /* @__PURE__ */ new Set([
5439
+ "call",
5440
+ "type_ref",
5441
+ "inherit",
5442
+ "implement",
5443
+ "import"
5444
+ ]);
5445
+ }
5446
+ });
5447
+
5299
5448
  // src/codebase-index/spawn-gate.ts
5300
5449
  function withSpawnGate(fn) {
5301
5450
  const run = chain.then(fn, fn);
@@ -5321,8 +5470,8 @@ __export(go_parser_exports, {
5321
5470
  });
5322
5471
  import { spawn as spawn5 } from "node:child_process";
5323
5472
  import * as os6 from "node:os";
5324
- import * as path19 from "node:path";
5325
- import * as fs14 from "node:fs/promises";
5473
+ import * as path20 from "node:path";
5474
+ import * as fs15 from "node:fs/promises";
5326
5475
  async function parseSymbols2(opts) {
5327
5476
  const { file, content, lang } = opts;
5328
5477
  try {
@@ -5330,7 +5479,8 @@ async function parseSymbols2(opts) {
5330
5479
  if (parsed.symbols.length > 0) {
5331
5480
  return parsed;
5332
5481
  }
5333
- return fallbackParse(file, content, lang);
5482
+ const fallback = fallbackParse(file, content, lang);
5483
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
5334
5484
  } catch {
5335
5485
  return fallbackParse(file, content, lang);
5336
5486
  }
@@ -5394,9 +5544,9 @@ async function syncGoParse(filePath, content, lang) {
5394
5544
  try {
5395
5545
  let scriptPath = _cachedGoScriptPath;
5396
5546
  if (!scriptPath) {
5397
- const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5398
- scriptPath = path19.join(tmpDir, "parse.go");
5399
- await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5547
+ const tmpDir = await fs15.mkdtemp(path20.join(os6.tmpdir(), "ws-go-parse-"));
5548
+ scriptPath = path20.join(tmpDir, "parse.go");
5549
+ await fs15.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5400
5550
  _cachedGoScriptPath = scriptPath;
5401
5551
  }
5402
5552
  const goBinary = resolveWin32Command("go");
@@ -5438,8 +5588,8 @@ async function syncGoParse(filePath, content, lang) {
5438
5588
  if (code !== 0 || !stdout.trim()) {
5439
5589
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5440
5590
  }
5441
- const raw = JSON.parse(stdout.trim());
5442
- const symbols = raw.map((s) => ({
5591
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
5592
+ const symbols = rawSymbols.map((s) => ({
5443
5593
  id: 0,
5444
5594
  lang,
5445
5595
  kind: s.kind,
@@ -5452,7 +5602,7 @@ async function syncGoParse(filePath, content, lang) {
5452
5602
  scope: s.scope ?? "",
5453
5603
  text: `${s.name} ${s.signature ?? ""}`.trim()
5454
5604
  }));
5455
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
5605
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
5456
5606
  } catch {
5457
5607
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5458
5608
  }
@@ -5462,6 +5612,7 @@ var init_go_parser = __esm({
5462
5612
  "src/codebase-index/go-parser.ts"() {
5463
5613
  "use strict";
5464
5614
  init_win32_resolve();
5615
+ init_parser_output();
5465
5616
  init_spawn_gate();
5466
5617
  init_languages2();
5467
5618
  GO_PARSE_SCRIPT = `
@@ -5475,6 +5626,7 @@ import (
5475
5626
  "go/token"
5476
5627
  "io"
5477
5628
  "os"
5629
+ "strconv"
5478
5630
  "strings"
5479
5631
  )
5480
5632
 
@@ -5487,16 +5639,34 @@ type Sym struct {
5487
5639
  Scope string \`json:"scope"\`
5488
5640
  }
5489
5641
 
5642
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
5643
+ // yields both. Module is the import path for CallType "import", else empty.
5644
+ type Ref struct {
5645
+ ToName string \`json:"toName"\`
5646
+ CallType string \`json:"callType"\`
5647
+ Line int \`json:"line"\`
5648
+ Module string \`json:"module"\`
5649
+ }
5650
+
5651
+ type Result struct {
5652
+ Symbols []Sym \`json:"symbols"\`
5653
+ Refs []Ref \`json:"refs"\`
5654
+ }
5655
+
5656
+ func emptyResult() string {
5657
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
5658
+ }
5659
+
5490
5660
  func main() {
5491
5661
  src, err := io.ReadAll(os.Stdin)
5492
5662
  if err != nil {
5493
- fmt.Print("[]")
5663
+ fmt.Print(emptyResult())
5494
5664
  return
5495
5665
  }
5496
5666
  fset := token.NewFileSet()
5497
5667
  node, err := parser.ParseFile(fset, "src.go", src, 0)
5498
5668
  if err != nil {
5499
- fmt.Print("[]")
5669
+ fmt.Print(emptyResult())
5500
5670
  return
5501
5671
  }
5502
5672
 
@@ -5560,9 +5730,43 @@ func main() {
5560
5730
  }
5561
5731
  }
5562
5732
 
5563
- data, err := json.Marshal(syms)
5733
+ refs := []Ref{}
5734
+ ast.Inspect(node, func(n ast.Node) bool {
5735
+ switch expr := n.(type) {
5736
+ case *ast.CallExpr:
5737
+ line := fset.Position(expr.Pos()).Line
5738
+ switch fun := expr.Fun.(type) {
5739
+ case *ast.Ident:
5740
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
5741
+ case *ast.SelectorExpr:
5742
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
5743
+ // declared symbol name, so it resolves the same way the TypeScript
5744
+ // and Python extractors' call refs do.
5745
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
5746
+ }
5747
+ case *ast.ImportSpec:
5748
+ if expr.Path != nil {
5749
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
5750
+ line := fset.Position(expr.Pos()).Line
5751
+ // A Go import names a package, not a symbol; the package's
5752
+ // last path segment is the name it is referenced by.
5753
+ name := importPath
5754
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
5755
+ name = importPath[idx+1:]
5756
+ }
5757
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
5758
+ }
5759
+ }
5760
+ }
5761
+ return true
5762
+ })
5763
+
5764
+ if syms == nil {
5765
+ syms = []Sym{}
5766
+ }
5767
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
5564
5768
  if err != nil {
5565
- fmt.Print("[]")
5769
+ fmt.Print(emptyResult())
5566
5770
  return
5567
5771
  }
5568
5772
  fmt.Print(string(data))
@@ -5914,9 +6118,13 @@ var init_generic_parser = __esm({
5914
6118
  ],
5915
6119
  elixir: [
5916
6120
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
5917
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
6121
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
6122
+ // against this symbol, and a `Foo`-only capture never matches it.
6123
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
5918
6124
  ],
5919
6125
  haskell: [
6126
+ // Target of `import Data.List`.
6127
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
5920
6128
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
5921
6129
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
5922
6130
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -6007,9 +6215,9 @@ __export(py_parser_exports, {
6007
6215
  parseSymbols: () => parseSymbols4
6008
6216
  });
6009
6217
  import { spawn as spawn6 } from "node:child_process";
6010
- import * as fs15 from "node:fs/promises";
6218
+ import * as fs16 from "node:fs/promises";
6011
6219
  import * as os7 from "node:os";
6012
- import * as path20 from "node:path";
6220
+ import * as path21 from "node:path";
6013
6221
  async function parseSymbols4(opts) {
6014
6222
  const { file, content, lang } = opts;
6015
6223
  try {
@@ -6087,10 +6295,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6087
6295
  async function syncPyParse(filePath, content, lang) {
6088
6296
  try {
6089
6297
  if (!_cachedScriptPath) {
6090
- const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6091
- await fs15.mkdir(tmpDir, { recursive: true });
6092
- _cachedScriptPath = path20.join(tmpDir, "parse.py");
6093
- await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6298
+ const tmpDir = path21.join(os7.tmpdir(), "ws-py-parse");
6299
+ await fs16.mkdir(tmpDir, { recursive: true });
6300
+ _cachedScriptPath = path21.join(tmpDir, "parse.py");
6301
+ await fs16.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6094
6302
  }
6095
6303
  cachedPyBinary ??= resolvePython();
6096
6304
  const pyBinary = await cachedPyBinary;
@@ -6104,7 +6312,7 @@ async function syncPyParse(filePath, content, lang) {
6104
6312
  if (code !== 0 || !stdout.trim()) {
6105
6313
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
6106
6314
  }
6107
- const raw = JSON.parse(stdout.trim());
6315
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
6108
6316
  const symbols = raw.map((s) => ({
6109
6317
  id: 0,
6110
6318
  lang,
@@ -6118,7 +6326,7 @@ async function syncPyParse(filePath, content, lang) {
6118
6326
  scope: s.scope ?? "",
6119
6327
  text: `${s.name} ${s.signature ?? ""}`.trim()
6120
6328
  }));
6121
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
6329
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
6122
6330
  } catch {
6123
6331
  return null;
6124
6332
  }
@@ -6129,6 +6337,7 @@ var init_py_parser = __esm({
6129
6337
  "use strict";
6130
6338
  init_win32_resolve();
6131
6339
  init_generic_parser();
6340
+ init_parser_output();
6132
6341
  init_spawn_gate();
6133
6342
  init_languages2();
6134
6343
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -6190,7 +6399,18 @@ class Sym:
6190
6399
  def is_private(name):
6191
6400
  return name.startswith("__") and not name.endswith("__")
6192
6401
 
6402
+ def leaf_name(node):
6403
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
6404
+ # TypeScript and Go extractors record call refs, so resolution behaves the
6405
+ # same across languages.
6406
+ if isinstance(node, ast.Attribute):
6407
+ return node.attr
6408
+ if isinstance(node, ast.Name):
6409
+ return node.id
6410
+ return get_name(node).split(".")[-1]
6411
+
6193
6412
  syms = []
6413
+ refs = []
6194
6414
  errors = []
6195
6415
 
6196
6416
  try:
@@ -6198,7 +6418,7 @@ try:
6198
6418
  tree = ast.parse(source, filename=sys.argv[1])
6199
6419
  except Exception as e:
6200
6420
  errors.append(str(e))
6201
- print("[]")
6421
+ print(json.dumps({"symbols": [], "refs": []}))
6202
6422
  sys.exit(0)
6203
6423
 
6204
6424
  # Module-level scope
@@ -6332,7 +6552,42 @@ class ModuleVisitor(ast.NodeVisitor):
6332
6552
  visitor = ModuleVisitor()
6333
6553
  visitor.visit(tree)
6334
6554
 
6335
- print(json.dumps([s.to_dict() for s in syms]))
6555
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
6556
+ # into function bodies (it would index locals as symbols), but that is exactly
6557
+ # where the calls are.
6558
+ for node in ast.walk(tree):
6559
+ if isinstance(node, ast.Call):
6560
+ name = leaf_name(node.func)
6561
+ if name:
6562
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
6563
+ elif isinstance(node, ast.Import):
6564
+ for alias in node.names:
6565
+ refs.append({
6566
+ "toName": alias.name.split(".")[-1],
6567
+ "callType": "import",
6568
+ "line": node.lineno,
6569
+ "module": alias.name,
6570
+ })
6571
+ elif isinstance(node, ast.ImportFrom):
6572
+ # PEP 328: node.level is the number of leading dots. Preserving them is
6573
+ # what lets the resolver walk up from the importing file's package \u2014
6574
+ # dropping them made \`from .foo import X\` indistinguishable from an
6575
+ # absolute \`foo\`.
6576
+ module = ("." * (node.level or 0)) + (node.module or "")
6577
+ for alias in node.names:
6578
+ refs.append({
6579
+ "toName": alias.name,
6580
+ "callType": "import",
6581
+ "line": node.lineno,
6582
+ "module": module,
6583
+ })
6584
+ elif isinstance(node, ast.ClassDef):
6585
+ for base in node.bases:
6586
+ name = leaf_name(base)
6587
+ if name:
6588
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
6589
+
6590
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
6336
6591
  `;
6337
6592
  _cachedScriptPath = null;
6338
6593
  }
@@ -6345,107 +6600,10 @@ __export(rs_parser_exports, {
6345
6600
  parseSymbols: () => parseSymbols5
6346
6601
  });
6347
6602
  import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6348
- import { execFile, spawn as spawn7 } from "node:child_process";
6349
- import * as fs16 from "node:fs/promises";
6350
- import * as path21 from "node:path";
6351
6603
  async function parseSymbols5(opts) {
6352
6604
  const { file, content, lang } = opts;
6353
- const nativeAvailable = await checkNativeParser();
6354
- if (nativeAvailable) {
6355
- const result = await withSpawnGate(() => tryNativeParse(file, content));
6356
- if (result) return result;
6357
- }
6358
6605
  return regexParse({ file, content, lang });
6359
6606
  }
6360
- function probe(command, args) {
6361
- return new Promise((resolve17, reject) => {
6362
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
6363
- if (error) reject(error);
6364
- else resolve17();
6365
- });
6366
- });
6367
- }
6368
- function checkNativeParser() {
6369
- nativeParserAvailability ??= (async () => {
6370
- try {
6371
- await probe("rustc", ["--version"]);
6372
- const toolsDir = path21.join(process.cwd(), "tools");
6373
- await probe(
6374
- "cargo",
6375
- [
6376
- "metadata",
6377
- "--no-deps",
6378
- "--format-version",
6379
- "1",
6380
- "--manifest-path",
6381
- path21.join(toolsDir, "Cargo.toml")
6382
- ]
6383
- );
6384
- return true;
6385
- } catch {
6386
- return false;
6387
- }
6388
- })();
6389
- return nativeParserAvailability;
6390
- }
6391
- async function tryNativeParse(file, content) {
6392
- try {
6393
- const toolsDir = path21.join(process.cwd(), "tools");
6394
- const crateDir = path21.join(toolsDir, "syn-parser");
6395
- const tmpFile = path21.join(crateDir, "src", "input.rs");
6396
- await fs16.writeFile(tmpFile, content, "utf8");
6397
- const cargoBinary = resolveWin32Command("cargo");
6398
- const result = await new Promise(
6399
- (resolve17, reject) => {
6400
- let settled = false;
6401
- const proc = spawn7(
6402
- cargoBinary,
6403
- ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6404
- {
6405
- cwd: process.cwd(),
6406
- stdio: ["pipe", "pipe", "pipe"],
6407
- windowsHide: true
6408
- }
6409
- );
6410
- proc.on("error", (err) => {
6411
- if (settled) return;
6412
- settled = true;
6413
- reject(err);
6414
- });
6415
- let stdout2 = "";
6416
- proc.stdout?.on("data", (chunk) => {
6417
- stdout2 += chunk.toString();
6418
- });
6419
- proc.stderr?.resume();
6420
- const timer = setTimeout(() => {
6421
- if (settled) return;
6422
- settled = true;
6423
- proc.kill("SIGKILL");
6424
- reject(new Error("timeout"));
6425
- }, 15e3);
6426
- timer.unref?.();
6427
- proc.on("close", (c) => {
6428
- if (settled) return;
6429
- settled = true;
6430
- clearTimeout(timer);
6431
- resolve17({ code: c, stdout: stdout2 });
6432
- });
6433
- }
6434
- );
6435
- const { code, stdout } = result;
6436
- if (code === 0 && stdout.trim()) {
6437
- const symbols = JSON.parse(stdout.trim());
6438
- return {
6439
- file,
6440
- lang: "rs",
6441
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
6442
- mtimeMs: Date.now()
6443
- };
6444
- }
6445
- } catch {
6446
- }
6447
- return null;
6448
- }
6449
6607
  function regexParse(opts) {
6450
6608
  const { file, content, lang } = opts;
6451
6609
  const symbols = [];
@@ -6501,12 +6659,10 @@ function regexParse(opts) {
6501
6659
  });
6502
6660
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
6503
6661
  }
6504
- var nativeParserAvailability, RS_PATTERNS;
6662
+ var RS_PATTERNS;
6505
6663
  var init_rs_parser = __esm({
6506
6664
  "src/codebase-index/rs-parser.ts"() {
6507
6665
  "use strict";
6508
- init_win32_resolve();
6509
- init_spawn_gate();
6510
6666
  init_languages2();
6511
6667
  RS_PATTERNS = [
6512
6668
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -10321,7 +10477,7 @@ async function shutdownBrowserTools() {
10321
10477
 
10322
10478
  // src/codebase-index/project-server-client.ts
10323
10479
  import { spawn as spawn4 } from "node:child_process";
10324
- import * as fs12 from "node:fs";
10480
+ import * as fs13 from "node:fs";
10325
10481
  import * as net3 from "node:net";
10326
10482
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10327
10483
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
@@ -10411,16 +10567,16 @@ function resetIndexCircuitBreaker() {
10411
10567
 
10412
10568
  // src/codebase-index/project-server-endpoint.ts
10413
10569
  import { createHash as createHash4 } from "node:crypto";
10414
- import * as fs11 from "node:fs";
10570
+ import * as fs12 from "node:fs";
10415
10571
  import * as os5 from "node:os";
10416
- import * as path16 from "node:path";
10572
+ import * as path17 from "node:path";
10417
10573
  import { fileURLToPath } from "node:url";
10418
10574
  import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
10419
10575
 
10420
10576
  // src/codebase-index/writer.ts
10421
10577
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
10422
- import * as fs10 from "node:fs";
10423
- import * as path15 from "node:path";
10578
+ import * as fs11 from "node:fs";
10579
+ import * as path16 from "node:path";
10424
10580
 
10425
10581
  // src/codebase-index/bm25.ts
10426
10582
  var K1 = 1.5;
@@ -10511,6 +10667,9 @@ var Bm25Index = class {
10511
10667
  }
10512
10668
  };
10513
10669
 
10670
+ // src/codebase-index/writer.ts
10671
+ init_languages2();
10672
+
10514
10673
  // src/codebase-index/lsp-kind.ts
10515
10674
  function lspKindToInternalKind(k) {
10516
10675
  switch (k) {
@@ -10545,7 +10704,7 @@ function lspKindToInternalKind(k) {
10545
10704
  }
10546
10705
 
10547
10706
  // src/codebase-index/schema.ts
10548
- var SCHEMA_VERSION = 3;
10707
+ var SCHEMA_VERSION = 4;
10549
10708
 
10550
10709
  // src/codebase-index/sqlite-runtime.ts
10551
10710
  import { createRequire } from "node:module";
@@ -10616,7 +10775,7 @@ function runSqliteWithRetry(fn) {
10616
10775
 
10617
10776
  // src/codebase-index/writer-admin.ts
10618
10777
  import * as fs9 from "node:fs";
10619
- import * as path13 from "node:path";
10778
+ import * as path14 from "node:path";
10620
10779
  var DB_FILE = "index.db";
10621
10780
  function getAllIndexableWithStatement(stmt) {
10622
10781
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10675,7 +10834,7 @@ function getAllFileMetasWithStatement(stmt) {
10675
10834
  }
10676
10835
  function getIndexDbSizeBytes(indexDir) {
10677
10836
  try {
10678
- return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10837
+ return fs9.statSync(path14.join(indexDir, DB_FILE)).size;
10679
10838
  } catch {
10680
10839
  return 0;
10681
10840
  }
@@ -10726,49 +10885,345 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
10726
10885
  }
10727
10886
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10728
10887
  if (refs.length === 0) return;
10729
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
10888
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
10730
10889
  for (let i = 0; i < refs.length; i += chunkSize) {
10731
10890
  const chunk = refs.slice(i, i + chunkSize);
10732
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
10891
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
10733
10892
  const insert = stmt(
10734
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
10893
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
10894
+ VALUES ${placeholders}`
10735
10895
  );
10736
10896
  const binds = [];
10737
10897
  for (const ref of chunk) {
10738
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
10898
+ binds.push(
10899
+ ref.fromId,
10900
+ ref.toName,
10901
+ ref.toId ?? null,
10902
+ ref.callType,
10903
+ ref.line,
10904
+ ref.lang ?? "",
10905
+ ref.module ?? null,
10906
+ ref.toFile ?? null
10907
+ );
10739
10908
  }
10740
10909
  insert.run(...binds);
10741
10910
  }
10742
10911
  }
10743
10912
 
10744
- // src/codebase-index/writer-graph-helpers.ts
10745
- import * as path14 from "node:path";
10746
- function derivePackage(filePath) {
10747
- const f = filePath.replace(/\\/g, "/");
10748
- const pkgsIdx = f.indexOf("/packages/");
10749
- if (pkgsIdx !== -1) {
10750
- const rest = f.slice(pkgsIdx + "/packages/".length);
10751
- const seg = rest.split("/")[0];
10752
- return seg ? `@wrongstack/${seg}` : void 0;
10753
- }
10754
- const appsIdx = f.indexOf("/apps/");
10913
+ // src/codebase-index/writer-graph-reader.ts
10914
+ init_languages2();
10915
+
10916
+ // src/codebase-index/module-roots.ts
10917
+ init_languages2();
10918
+ import * as fs10 from "node:fs/promises";
10919
+ import * as path15 from "node:path";
10920
+ function toPortablePath(file) {
10921
+ return file.replace(/\\/g, "/");
10922
+ }
10923
+ async function readTextIfPresent(file) {
10924
+ try {
10925
+ return await fs10.readFile(file, "utf8");
10926
+ } catch {
10927
+ return void 0;
10928
+ }
10929
+ }
10930
+ function parsePackageJsonName(source) {
10931
+ try {
10932
+ const parsed = JSON.parse(source);
10933
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
10934
+ } catch {
10935
+ return void 0;
10936
+ }
10937
+ }
10938
+ function parseGoModulePath(source) {
10939
+ for (const rawLine of source.split(/\r?\n/)) {
10940
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
10941
+ const match = /^module\s+(\S+)/.exec(line);
10942
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
10943
+ }
10944
+ return void 0;
10945
+ }
10946
+ function parseTomlTableName(source, tables) {
10947
+ let current = "";
10948
+ for (const rawLine of source.split(/\r?\n/)) {
10949
+ const line = rawLine.replace(/#.*$/, "").trim();
10950
+ if (line.startsWith("[[")) {
10951
+ current = "\0";
10952
+ continue;
10953
+ }
10954
+ const table = /^\[([^\]]+)\]$/.exec(line);
10955
+ if (table?.[1]) {
10956
+ current = table[1].trim();
10957
+ continue;
10958
+ }
10959
+ if (!tables.includes(current)) continue;
10960
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
10961
+ if (match?.[1]) return match[1];
10962
+ }
10963
+ return void 0;
10964
+ }
10965
+ function parsePomArtifactId(source) {
10966
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
10967
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
10968
+ }
10969
+ var LANGS_BY_KIND = {
10970
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
10971
+ cargo: ["rs"],
10972
+ go: ["go"],
10973
+ python: ["py"],
10974
+ maven: ["java", "kotlin", "scala"],
10975
+ gradle: ["java", "kotlin", "scala"],
10976
+ dotnet: ["csharp"]
10977
+ };
10978
+ function ancestorsOf(dir, stopAt) {
10979
+ const out = [];
10980
+ let current = dir;
10981
+ for (; ; ) {
10982
+ out.push(current);
10983
+ if (current === stopAt || current.length <= stopAt.length) break;
10984
+ const parent = path15.posix.dirname(current);
10985
+ if (parent === current) break;
10986
+ current = parent;
10987
+ }
10988
+ return out;
10989
+ }
10990
+ var MARKER_PROBES = [
10991
+ {
10992
+ kind: "npm",
10993
+ file: "package.json",
10994
+ build: (dir, source) => {
10995
+ const name = parsePackageJsonName(source) ?? path15.posix.basename(dir);
10996
+ return { name, importPath: name, sourceRoots: [dir] };
10997
+ }
10998
+ },
10999
+ {
11000
+ kind: "cargo",
11001
+ file: "Cargo.toml",
11002
+ build: (dir, source) => {
11003
+ const name = parseTomlTableName(source, ["package"]);
11004
+ if (!name) return void 0;
11005
+ return {
11006
+ name: `crate:${name}`,
11007
+ // Rust paths use underscores where crate names often use dashes.
11008
+ importPath: name.replace(/-/g, "_"),
11009
+ sourceRoots: [path15.posix.join(dir, "src")]
11010
+ };
11011
+ }
11012
+ },
11013
+ {
11014
+ kind: "go",
11015
+ file: "go.mod",
11016
+ build: (dir, source) => {
11017
+ const modulePath = parseGoModulePath(source);
11018
+ if (!modulePath) return void 0;
11019
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
11020
+ }
11021
+ },
11022
+ {
11023
+ kind: "python",
11024
+ file: "pyproject.toml",
11025
+ build: (dir, source) => {
11026
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path15.posix.basename(dir);
11027
+ return {
11028
+ name: `py:${name}`,
11029
+ importPath: void 0,
11030
+ // `src/` layout is the packaging-guide default; the root itself covers
11031
+ // the flat layout. Both are probed, missing ones simply never match.
11032
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
11033
+ };
11034
+ }
11035
+ },
11036
+ {
11037
+ kind: "python",
11038
+ file: "setup.py",
11039
+ build: (dir) => ({
11040
+ name: `py:${path15.posix.basename(dir)}`,
11041
+ importPath: void 0,
11042
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
11043
+ })
11044
+ },
11045
+ {
11046
+ kind: "maven",
11047
+ file: "pom.xml",
11048
+ build: (dir, source) => {
11049
+ const artifactId = parsePomArtifactId(source) ?? path15.posix.basename(dir);
11050
+ return {
11051
+ name: `mvn:${artifactId}`,
11052
+ importPath: void 0,
11053
+ sourceRoots: [
11054
+ path15.posix.join(dir, "src/main/java"),
11055
+ path15.posix.join(dir, "src/main/kotlin"),
11056
+ path15.posix.join(dir, "src/main/scala"),
11057
+ path15.posix.join(dir, "src/test/java")
11058
+ ]
11059
+ };
11060
+ }
11061
+ },
11062
+ {
11063
+ kind: "gradle",
11064
+ file: "build.gradle",
11065
+ build: (dir) => buildGradleRoot(dir)
11066
+ },
11067
+ {
11068
+ kind: "gradle",
11069
+ file: "build.gradle.kts",
11070
+ build: (dir) => buildGradleRoot(dir)
11071
+ }
11072
+ ];
11073
+ function buildGradleRoot(dir) {
11074
+ return {
11075
+ name: `gradle:${path15.posix.basename(dir)}`,
11076
+ importPath: void 0,
11077
+ sourceRoots: [
11078
+ path15.posix.join(dir, "src/main/java"),
11079
+ path15.posix.join(dir, "src/main/kotlin"),
11080
+ path15.posix.join(dir, "src/main/scala")
11081
+ ]
11082
+ };
11083
+ }
11084
+ async function probeDotnetRoot(dir) {
11085
+ let entries;
11086
+ try {
11087
+ entries = await fs10.readdir(dir);
11088
+ } catch {
11089
+ return void 0;
11090
+ }
11091
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
11092
+ if (!project) return void 0;
11093
+ const name = project.slice(0, -".csproj".length);
11094
+ return {
11095
+ dir,
11096
+ kind: "dotnet",
11097
+ name: `csproj:${name}`,
11098
+ importPath: void 0,
11099
+ sourceRoots: [dir]
11100
+ };
11101
+ }
11102
+ async function detectModuleRoots(projectRoot, files) {
11103
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
11104
+ const langsByDir = /* @__PURE__ */ new Map();
11105
+ for (const file of files) {
11106
+ const portable = toPortablePath(file);
11107
+ const lang = detectLang(portable);
11108
+ if (!lang) continue;
11109
+ const dir = path15.posix.dirname(portable);
11110
+ let langs = langsByDir.get(dir);
11111
+ if (!langs) {
11112
+ langs = /* @__PURE__ */ new Set();
11113
+ langsByDir.set(dir, langs);
11114
+ }
11115
+ langs.add(lang);
11116
+ }
11117
+ const candidates = /* @__PURE__ */ new Map();
11118
+ for (const [dir, langs] of langsByDir) {
11119
+ for (const ancestor of ancestorsOf(dir, root)) {
11120
+ let merged = candidates.get(ancestor);
11121
+ if (!merged) {
11122
+ merged = /* @__PURE__ */ new Set();
11123
+ candidates.set(ancestor, merged);
11124
+ }
11125
+ for (const lang of langs) merged.add(lang);
11126
+ }
11127
+ }
11128
+ const roots = [];
11129
+ await Promise.all(
11130
+ [...candidates].map(async ([dir, langs]) => {
11131
+ for (const probe of MARKER_PROBES) {
11132
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
11133
+ const source = await readTextIfPresent(path15.posix.join(dir, probe.file));
11134
+ if (source === void 0) continue;
11135
+ const built = probe.build(dir, source);
11136
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
11137
+ }
11138
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
11139
+ const dotnet = await probeDotnetRoot(dir);
11140
+ if (dotnet) roots.push(dotnet);
11141
+ }
11142
+ })
11143
+ );
11144
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
11145
+ return { projectRoot: root, roots };
11146
+ }
11147
+ function findOwningRoot(structure, file, kinds) {
11148
+ const portable = toPortablePath(file);
11149
+ for (const root of structure.roots) {
11150
+ if (kinds && !kinds.includes(root.kind)) continue;
11151
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
11152
+ }
11153
+ return void 0;
11154
+ }
11155
+ function derivePackageFromLayout(filePath) {
11156
+ const portable = toPortablePath(filePath);
11157
+ const packagesIdx = portable.indexOf("/packages/");
11158
+ if (packagesIdx !== -1) {
11159
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
11160
+ if (segment) return `@wrongstack/${segment}`;
11161
+ }
11162
+ const appsIdx = portable.indexOf("/apps/");
10755
11163
  if (appsIdx !== -1) {
10756
- const rest = f.slice(appsIdx + "/apps/".length);
10757
- const seg = rest.split("/")[0];
10758
- return seg ? `app:${seg}` : void 0;
11164
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
11165
+ if (segment) return `app:${segment}`;
10759
11166
  }
10760
11167
  return void 0;
10761
11168
  }
10762
- function packageFromImport(moduleName) {
10763
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
10764
- const parts = moduleName.split("/");
10765
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
11169
+ function pythonPackageLabel(structure, file, initDirs) {
11170
+ const portable = toPortablePath(file);
11171
+ const dir = path15.posix.dirname(portable);
11172
+ if (!initDirs.has(dir)) return void 0;
11173
+ const segments = [];
11174
+ let current = dir;
11175
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
11176
+ segments.unshift(path15.posix.basename(current));
11177
+ current = path15.posix.dirname(current);
11178
+ }
11179
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
10766
11180
  }
10767
- function buildPackageGraphNodes(fileCounts, files) {
11181
+ function assignPackageLabels(structure, files) {
11182
+ const initDirs = /* @__PURE__ */ new Set();
11183
+ for (const file of files) {
11184
+ const portable = toPortablePath(file);
11185
+ if (path15.posix.basename(portable) === "__init__.py") {
11186
+ initDirs.add(path15.posix.dirname(portable));
11187
+ }
11188
+ }
11189
+ const labels = /* @__PURE__ */ new Map();
11190
+ for (const file of files) {
11191
+ const portable = toPortablePath(file);
11192
+ const lang = detectLang(portable);
11193
+ if (lang === "go") {
11194
+ const owner3 = findOwningRoot(structure, portable, ["go"]);
11195
+ const dir = path15.posix.dirname(portable);
11196
+ if (owner3?.importPath) {
11197
+ const relative13 = path15.posix.relative(owner3.dir, dir);
11198
+ labels.set(file, relative13 ? `${owner3.importPath}/${relative13}` : owner3.importPath);
11199
+ } else {
11200
+ labels.set(file, `go:${path15.posix.relative(structure.projectRoot, dir) || "."}`);
11201
+ }
11202
+ continue;
11203
+ }
11204
+ if (lang === "py") {
11205
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
11206
+ if (dotted) {
11207
+ labels.set(file, dotted);
11208
+ continue;
11209
+ }
11210
+ }
11211
+ const owner2 = findOwningRoot(structure, portable);
11212
+ const label = owner2?.name ?? derivePackageFromLayout(portable) ?? "(root)";
11213
+ labels.set(file, label);
11214
+ }
11215
+ return labels;
11216
+ }
11217
+
11218
+ // src/codebase-index/writer-graph-helpers.ts
11219
+ function createPackageLabeller(stored) {
11220
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
11221
+ }
11222
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
10768
11223
  const pkgNodes = /* @__PURE__ */ new Map();
10769
11224
  const fileToPkg = /* @__PURE__ */ new Map();
10770
11225
  for (const { file, n } of fileCounts) {
10771
- const pkg = derivePackage(file) ?? "(root)";
11226
+ const pkg = packageOf(file);
10772
11227
  fileToPkg.set(file, pkg);
10773
11228
  const node = pkgNodes.get(pkg);
10774
11229
  if (node) {
@@ -10785,7 +11240,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10785
11240
  }
10786
11241
  }
10787
11242
  for (const { file } of files) {
10788
- const pkg = derivePackage(file) ?? "(root)";
11243
+ const pkg = packageOf(file);
10789
11244
  fileToPkg.set(file, pkg);
10790
11245
  const node = pkgNodes.get(pkg);
10791
11246
  if (node) {
@@ -10803,7 +11258,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10803
11258
  }
10804
11259
  return { pkgNodes, fileToPkg };
10805
11260
  }
10806
- function buildFileGraphNodeState(pkgSyms, localFiles) {
11261
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
10807
11262
  const fileNodes = /* @__PURE__ */ new Map();
10808
11263
  const symToFile = /* @__PURE__ */ new Map();
10809
11264
  const fileStats = /* @__PURE__ */ new Map();
@@ -10822,7 +11277,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10822
11277
  id: `file:${file}`,
10823
11278
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
10824
11279
  kind: "file",
10825
- package: derivePackage(file) ?? "(root)",
11280
+ package: packageOf(file),
10826
11281
  file,
10827
11282
  symbolCount: stats?.count ?? 0,
10828
11283
  lang: stats?.lang,
@@ -10834,7 +11289,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10834
11289
  }
10835
11290
  return { fileNodes, symToFile, fileStats, ensureFileNode };
10836
11291
  }
10837
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
11292
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
10838
11293
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
10839
11294
  const aExternal = a.file === fileFilter ? 0 : 1;
10840
11295
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -10846,7 +11301,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10846
11301
  symbolId: s.id,
10847
11302
  symbolKind: s.kind,
10848
11303
  file: s.file,
10849
- package: derivePackage(s.file) ?? "(root)",
11304
+ package: packageOf(s.file),
10850
11305
  lang: s.lang,
10851
11306
  line: s.line,
10852
11307
  signature: s.signature,
@@ -10854,29 +11309,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10854
11309
  external: s.file !== fileFilter
10855
11310
  }));
10856
11311
  }
10857
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10858
- if (!moduleName.startsWith(".")) return void 0;
10859
- const normalizedFrom = fromFile.replace(/\\/g, "/");
10860
- const absolute = path14.posix.normalize(
10861
- path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10862
- );
10863
- const extension = path14.posix.extname(absolute);
10864
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
10865
- const candidates = [
10866
- absolute,
10867
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10868
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10869
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10870
- ];
10871
- const indexedByPortablePath = new Map(
10872
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
10873
- );
10874
- for (const candidate of candidates) {
10875
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
10876
- if (indexed) return indexed;
10877
- }
10878
- return void 0;
10879
- }
10880
11312
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
10881
11313
  const key = `${source}\0${target}`;
10882
11314
  let edge = edgeMap.get(key);
@@ -10917,7 +11349,12 @@ function mapWriterRefRow(row) {
10917
11349
  toName: row.to_name,
10918
11350
  toId: row.to_id ?? void 0,
10919
11351
  callType: row.call_type,
10920
- line: row.line
11352
+ line: row.line,
11353
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
11354
+ // queries select; `undefined` keeps those rows valid Refs.
11355
+ lang: row.lang || void 0,
11356
+ module: row.module ?? void 0,
11357
+ toFile: row.to_file ?? void 0
10921
11358
  };
10922
11359
  }
10923
11360
 
@@ -11065,7 +11502,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
11065
11502
  function getPackageGraphWithStatement(stmt) {
11066
11503
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
11067
11504
  const files = stmt("SELECT DISTINCT file FROM files").all();
11068
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
11505
+ const packageOf = readPackageLabeller(stmt);
11506
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
11069
11507
  const refRows = stmt(
11070
11508
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
11071
11509
  FROM refs r
@@ -11076,32 +11514,42 @@ function getPackageGraphWithStatement(stmt) {
11076
11514
  ).all();
11077
11515
  const edgeMap = /* @__PURE__ */ new Map();
11078
11516
  for (const r of refRows) {
11079
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
11080
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
11517
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11518
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
11081
11519
  if (fromPkg === toPkg) continue;
11082
11520
  const n = Number(r.n) || 0;
11083
11521
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
11084
11522
  }
11085
11523
  const importRows = stmt(
11086
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
11524
+ `SELECT s.file AS from_file,
11525
+ COALESCE(r.to_file, st.file) AS to_file,
11526
+ COUNT(*) AS n
11087
11527
  FROM refs r
11088
11528
  JOIN symbols s ON s.id = r.from_id
11529
+ LEFT JOIN symbols st ON st.id = r.to_id
11089
11530
  WHERE r.call_type = 'import'
11090
- GROUP BY r.to_name, s.file`
11531
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
11532
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
11091
11533
  ).all();
11092
11534
  for (const r of importRows) {
11093
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
11094
- const toPkg = packageFromImport(r.to_name);
11095
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
11535
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11536
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
11537
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
11096
11538
  const n = Number(r.n) || 0;
11097
11539
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
11098
11540
  }
11099
11541
  const edges = materializeWeightedEdges(edgeMap, "pkg");
11100
11542
  return { nodes: [...pkgNodes.values()], edges };
11101
11543
  }
11544
+ function readPackageLabeller(stmt) {
11545
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
11546
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
11547
+ }
11102
11548
  function getFileGraphWithStatement(stmt, packageFilter) {
11103
11549
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
11104
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
11550
+ const packageOf = readPackageLabeller(stmt);
11551
+ const langOf = (file) => detectLang(file) ?? "other";
11552
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
11105
11553
  const localFiles = new Set(pkgFilePaths);
11106
11554
  if (localFiles.size === 0) return { nodes: [], edges: [] };
11107
11555
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -11110,9 +11558,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
11110
11558
  ).all(...pkgFilePaths);
11111
11559
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
11112
11560
  pkgSyms,
11113
- localFiles
11561
+ localFiles,
11562
+ packageOf
11114
11563
  );
11115
- const indexedFiles = new Set(allFiles.map((f) => f.file));
11116
11564
  const refRows = stmt(
11117
11565
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
11118
11566
  FROM refs r
@@ -11135,7 +11583,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
11135
11583
  for (const x of extras) {
11136
11584
  symToFile.set(x.id, x.file);
11137
11585
  if (!fileStats.has(x.file)) {
11138
- fileStats.set(x.file, { count: 0, lang: "ts" });
11586
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
11139
11587
  }
11140
11588
  }
11141
11589
  }
@@ -11152,17 +11600,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
11152
11600
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
11153
11601
  }
11154
11602
  const importRows = stmt(
11155
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
11603
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
11156
11604
  FROM refs r
11605
+ LEFT JOIN symbols st ON st.id = r.to_id
11157
11606
  WHERE r.call_type = 'import'
11607
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
11158
11608
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
11159
- GROUP BY r.from_id, r.to_name`
11609
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
11160
11610
  ).all(...pkgFilePaths);
11161
11611
  for (const r of importRows) {
11162
11612
  const fromFile = symToFile.get(r.from_id);
11163
11613
  if (!fromFile || !localFiles.has(fromFile)) continue;
11164
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
11614
+ const toFile = r.to_file;
11165
11615
  if (!toFile || fromFile === toFile) continue;
11616
+ if (!fileStats.has(toFile)) {
11617
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
11618
+ }
11166
11619
  ensureFileNode(fromFile);
11167
11620
  ensureFileNode(toFile);
11168
11621
  const n = Number(r.n) || 0;
@@ -11212,7 +11665,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
11212
11665
  ).all(...missingIds);
11213
11666
  for (const s of extras) symById.set(s.id, s);
11214
11667
  }
11215
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
11668
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
11216
11669
  return { nodes, edges };
11217
11670
  }
11218
11671
 
@@ -11234,7 +11687,7 @@ function assignRefsToSymbols(refs, symbols) {
11234
11687
  }
11235
11688
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
11236
11689
  if (!owner2 || owner2.id <= 0) continue;
11237
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
11690
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
11238
11691
  if (seen.has(key)) continue;
11239
11692
  seen.add(key);
11240
11693
  assigned.push({ ...ref, fromId: owner2.id });
@@ -11280,7 +11733,11 @@ var CORE_TABLES_SQL = `
11280
11733
  lang TEXT NOT NULL,
11281
11734
  mtime_ms INTEGER NOT NULL,
11282
11735
  symbol_count INTEGER NOT NULL DEFAULT 0,
11283
- last_indexed INTEGER NOT NULL
11736
+ last_indexed INTEGER NOT NULL,
11737
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
11738
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
11739
+ -- re-derived per query because the evidence lives on disk, not in the DB.
11740
+ package TEXT NOT NULL DEFAULT ''
11284
11741
  );
11285
11742
  CREATE TABLE IF NOT EXISTS symbols (
11286
11743
  id INTEGER PRIMARY KEY,
@@ -11297,6 +11754,9 @@ var CORE_TABLES_SQL = `
11297
11754
  file_fk TEXT NOT NULL
11298
11755
  );
11299
11756
  `;
11757
+ var FILE_INDEX_SQL = [
11758
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
11759
+ ];
11300
11760
  var SYMBOL_INDEX_SQL = [
11301
11761
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
11302
11762
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -11313,15 +11773,32 @@ var REFS_TABLE_SQL = `
11313
11773
  to_name TEXT NOT NULL,
11314
11774
  to_id INTEGER,
11315
11775
  call_type TEXT NOT NULL,
11316
- line INTEGER NOT NULL
11776
+ line INTEGER NOT NULL,
11777
+ lang TEXT NOT NULL DEFAULT '',
11778
+ module TEXT,
11779
+ to_file TEXT
11317
11780
  );
11318
11781
  `;
11319
11782
  var REFS_INDEX_SQL = [
11320
11783
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
11321
11784
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
11322
11785
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
11323
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
11786
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
11787
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
11788
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
11789
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
11790
+ // The post-index module resolution pass groups unresolved import refs by
11791
+ // (module, lang); graph readers then read to_file back.
11792
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
11793
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
11324
11794
  ];
11795
+ var LANG_FAMILY_TABLE_SQL = `
11796
+ CREATE TABLE IF NOT EXISTS lang_family (
11797
+ lang TEXT PRIMARY KEY,
11798
+ family TEXT NOT NULL
11799
+ );
11800
+ `;
11801
+ var LANG_FAMILY_WILDCARD = "*";
11325
11802
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
11326
11803
 
11327
11804
  // src/codebase-index/writer-search-helpers.ts
@@ -11533,15 +12010,69 @@ var IndexStore = class _IndexStore {
11533
12010
  }
11534
12011
  constructor(projectRoot, opts = {}) {
11535
12012
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
11536
- fs10.mkdirSync(this.indexDir, { recursive: true });
12013
+ fs11.mkdirSync(this.indexDir, { recursive: true });
11537
12014
  const Database = loadDatabaseSync();
11538
- this.db = new Database(path15.join(this.indexDir, DB_FILE2));
12015
+ this.db = new Database(path16.join(this.indexDir, DB_FILE2));
11539
12016
  applyIndexStorePragmas(this.db);
11540
12017
  this.initSchema();
11541
12018
  }
11542
12019
  runWithRetry(fn) {
11543
12020
  return runSqliteWithRetry(fn);
11544
12021
  }
12022
+ /**
12023
+ * Mirror the in-process language→family map into SQLite.
12024
+ *
12025
+ * Rewritten on every open rather than only on schema bumps: the mapping is
12026
+ * static lookup data, so a code-side change (a new language, a language
12027
+ * moving families) must take effect without forcing a full reindex.
12028
+ */
12029
+ seedLangFamilies() {
12030
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
12031
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
12032
+ insert.run("", LANG_FAMILY_WILDCARD);
12033
+ }
12034
+ /**
12035
+ * Add any column the current schema expects but the on-disk table lacks.
12036
+ *
12037
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
12038
+ * and the version check above only rebuilds on a version *mismatch*. That
12039
+ * leaves a real gap: several wstack processes share this database, and while
12040
+ * a version upgrade is rolling out one of them may still be running the
12041
+ * previous build. That older process sees the newer version number, drops the
12042
+ * tables, and recreates them from *its* DDL — without the newer columns —
12043
+ * while the metadata row still reads the new version. Every later query for
12044
+ * one of those columns then fails with `no such column`, and no amount of
12045
+ * reindexing fixes it, because the version numbers already agree.
12046
+ *
12047
+ * Repairing column-by-column makes the schema self-healing from any of those
12048
+ * states. Table and column names are compile-time literals from this module,
12049
+ * never user input.
12050
+ */
12051
+ repairMissingColumns() {
12052
+ const expected = [
12053
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
12054
+ {
12055
+ table: "refs",
12056
+ columns: [
12057
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
12058
+ ["module", "TEXT"],
12059
+ ["to_file", "TEXT"]
12060
+ ]
12061
+ }
12062
+ ];
12063
+ for (const { table, columns } of expected) {
12064
+ const present = new Set(
12065
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
12066
+ (row) => typeof row.name === "string" ? [row.name] : []
12067
+ )
12068
+ );
12069
+ if (present.size === 0) continue;
12070
+ for (const [name, type] of columns) {
12071
+ if (present.has(name)) continue;
12072
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
12073
+ }
12074
+ }
12075
+ }
11545
12076
  initSchema() {
11546
12077
  this.db.exec(METADATA_TABLE_SQL);
11547
12078
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -11564,9 +12095,13 @@ var IndexStore = class _IndexStore {
11564
12095
  );
11565
12096
  }
11566
12097
  this.db.exec(CORE_TABLES_SQL);
11567
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11568
12098
  this.db.exec(REFS_TABLE_SQL);
12099
+ this.repairMissingColumns();
12100
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
12101
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11569
12102
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
12103
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
12104
+ this.seedLangFamilies();
11570
12105
  try {
11571
12106
  this.db.exec(SYMBOLS_FTS_SQL);
11572
12107
  this.ftsAvailable = true;
@@ -11601,6 +12136,18 @@ var IndexStore = class _IndexStore {
11601
12136
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
11602
12137
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
11603
12138
  static MAX_SQL_VARS = 900;
12139
+ /**
12140
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
12141
+ * `sym` belong to the same language family — or the ref carries no language,
12142
+ * in which case the wildcard bind matches everything.
12143
+ *
12144
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
12145
+ */
12146
+ static FAMILY_MATCH_SQL = `(
12147
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
12148
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
12149
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
12150
+ )`;
11604
12151
  /**
11605
12152
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
11606
12153
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -11664,9 +12211,12 @@ var IndexStore = class _IndexStore {
11664
12211
  const placeholders = chunk.map(() => "?").join(",");
11665
12212
  const result = this.stmt(
11666
12213
  `UPDATE refs
11667
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
12214
+ SET to_id = (
12215
+ SELECT MIN(sym.id) FROM symbols sym
12216
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12217
+ )
11668
12218
  WHERE to_name IN (${placeholders})`
11669
- ).run(...chunk);
12219
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
11670
12220
  changes += result.changes ?? 0;
11671
12221
  }
11672
12222
  return changes;
@@ -11793,6 +12343,115 @@ var IndexStore = class _IndexStore {
11793
12343
  getAllFileMetas() {
11794
12344
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
11795
12345
  }
12346
+ // ─── Project structure & module resolution ──────────────────────────────────
12347
+ /** Store the Code Atlas grouping label for each indexed file. */
12348
+ setFilePackages(entries) {
12349
+ if (entries.size === 0) return;
12350
+ this.runWithRetry(() => {
12351
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
12352
+ for (const [file, label] of entries) update.run(label, file);
12353
+ });
12354
+ }
12355
+ /**
12356
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
12357
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
12358
+ * Ordered so the resolver's choice among duplicate declarations is stable.
12359
+ */
12360
+ getNamespaceDeclarations() {
12361
+ return this.stmt(
12362
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
12363
+ ).all();
12364
+ }
12365
+ /** `file → package` for every indexed file that has a label. */
12366
+ getFilePackages() {
12367
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
12368
+ return new Map(rows.map((row) => [row.file, row.package]));
12369
+ }
12370
+ /**
12371
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
12372
+ *
12373
+ * Distinct rather than per-ref because resolution depends only on these three
12374
+ * values: a file importing the same module twenty times resolves it once.
12375
+ */
12376
+ getUnresolvedImports(onlyFiles) {
12377
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
12378
+ FROM refs r
12379
+ JOIN symbols s ON s.id = r.from_id
12380
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
12381
+ if (!onlyFiles?.length) {
12382
+ return this.stmt(base).all();
12383
+ }
12384
+ const out = [];
12385
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
12386
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
12387
+ const placeholders = chunk.map(() => "?").join(",");
12388
+ out.push(
12389
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
12390
+ );
12391
+ }
12392
+ return out;
12393
+ }
12394
+ /**
12395
+ * Write resolved import targets back onto `refs.to_file`.
12396
+ *
12397
+ * Applied through a temp table and a single UPDATE: one statement per
12398
+ * resolution would mean thousands of round-trips on a first index.
12399
+ */
12400
+ applyImportResolutions(resolutions) {
12401
+ if (resolutions.length === 0) return 0;
12402
+ return this.runWithRetry(() => {
12403
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12404
+ this.db.exec(
12405
+ `CREATE TEMP TABLE import_resolution (
12406
+ from_file TEXT NOT NULL,
12407
+ lang TEXT NOT NULL,
12408
+ module TEXT NOT NULL,
12409
+ to_file TEXT NOT NULL
12410
+ )`
12411
+ );
12412
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
12413
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
12414
+ const chunk = resolutions.slice(i, i + chunkSize);
12415
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
12416
+ const binds = [];
12417
+ for (const entry of chunk) {
12418
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
12419
+ }
12420
+ this.stmt(
12421
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
12422
+ VALUES ${placeholders}`
12423
+ ).run(...binds);
12424
+ }
12425
+ this.db.exec(
12426
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
12427
+ ON import_resolution(module, lang, from_file)`
12428
+ );
12429
+ const result = this.stmt(
12430
+ `UPDATE refs
12431
+ SET to_file = (
12432
+ SELECT ir.to_file
12433
+ FROM temp.import_resolution ir
12434
+ JOIN symbols s ON s.id = refs.from_id
12435
+ WHERE ir.module = refs.module
12436
+ AND ir.lang = refs.lang
12437
+ AND ir.from_file = s.file
12438
+ LIMIT 1
12439
+ )
12440
+ WHERE refs.call_type = 'import'
12441
+ AND refs.module IS NOT NULL
12442
+ AND EXISTS (
12443
+ SELECT 1
12444
+ FROM temp.import_resolution ir
12445
+ JOIN symbols s ON s.id = refs.from_id
12446
+ WHERE ir.module = refs.module
12447
+ AND ir.lang = refs.lang
12448
+ AND ir.from_file = s.file
12449
+ )`
12450
+ ).run();
12451
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12452
+ return result.changes ?? 0;
12453
+ });
12454
+ }
11796
12455
  // ─── Search ──────────────────────────────────────────────────────────────────
11797
12456
  search(query, filter, opts) {
11798
12457
  const built = this.buildSearchWhere(query, filter);
@@ -12179,9 +12838,12 @@ var IndexStore = class _IndexStore {
12179
12838
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
12180
12839
  * Call this after all symbols have been inserted to fill in cross-references.
12181
12840
  *
12182
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
12183
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
12184
- * that found a targetmatching the previous per-row loop's return value.
12841
+ * A match additionally requires the referencing ref and the target symbol to
12842
+ * be in the same {@link LangFamily}. Without that guard a name match is a
12843
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
12844
+ * `Config` are declared in most languages at once, and each collision draws a
12845
+ * Code Atlas edge between files that never reference each other. Refs stored
12846
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
12185
12847
  */
12186
12848
  resolveRefs() {
12187
12849
  return this.runWithRetry(() => {
@@ -12190,20 +12852,35 @@ var IndexStore = class _IndexStore {
12190
12852
  `UPDATE refs
12191
12853
  SET to_id = s.id
12192
12854
  FROM (
12193
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
12194
- ) AS s
12855
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
12856
+ FROM symbols sym
12857
+ JOIN lang_family lf ON lf.lang = sym.lang
12858
+ GROUP BY sym.name, lf.family
12859
+ UNION ALL
12860
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
12861
+ FROM symbols sym
12862
+ GROUP BY sym.name
12863
+ ) AS s,
12864
+ lang_family AS rf
12195
12865
  WHERE refs.to_id IS NULL
12196
12866
  AND refs.to_name IS NOT NULL
12197
- AND refs.to_name = s.name`
12867
+ AND rf.lang = refs.lang
12868
+ AND s.name = refs.to_name
12869
+ AND s.family = rf.family`
12198
12870
  ).run();
12199
12871
  return result.changes ?? 0;
12200
12872
  } catch {
12201
12873
  const result = this.stmt(
12202
12874
  `UPDATE refs SET to_id = (
12203
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
12875
+ SELECT sym.id FROM symbols sym
12876
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12877
+ ORDER BY sym.id LIMIT 1
12204
12878
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
12205
- AND to_name IN (SELECT name FROM symbols)`
12206
- ).run();
12879
+ AND EXISTS (
12880
+ SELECT 1 FROM symbols sym
12881
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12882
+ )`
12883
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
12207
12884
  return result.changes ?? 0;
12208
12885
  }
12209
12886
  });
@@ -12392,21 +13069,21 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12392
13069
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
12393
13070
  var buildIdCache;
12394
13071
  function projectIndexServerBuildId(entrypoint) {
12395
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
13072
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path17.resolve(entrypoint);
12396
13073
  try {
12397
- const stat19 = fs11.statSync(file);
13074
+ const stat19 = fs12.statSync(file);
12398
13075
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
12399
13076
  return buildIdCache.buildId;
12400
13077
  }
12401
- const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
13078
+ const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
12402
13079
  buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
12403
13080
  return buildId;
12404
13081
  } catch {
12405
- return `unreadable:${path16.basename(file)}`;
13082
+ return `unreadable:${path17.basename(file)}`;
12406
13083
  }
12407
13084
  }
12408
13085
  function normalizeLocalPath(value) {
12409
- const resolved = path16.resolve(value);
13086
+ const resolved = path17.resolve(value);
12410
13087
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12411
13088
  }
12412
13089
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -12418,11 +13095,11 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
12418
13095
  if (process.platform === "win32") {
12419
13096
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12420
13097
  }
12421
- return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
13098
+ return path17.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
12422
13099
  }
12423
13100
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
12424
- return path16.join(
12425
- path16.resolve(resolveIndexDir(projectRoot, indexDir)),
13101
+ return path17.join(
13102
+ path17.resolve(resolveIndexDir(projectRoot, indexDir)),
12426
13103
  PROJECT_INDEX_SERVER_METADATA_FILE
12427
13104
  );
12428
13105
  }
@@ -12462,7 +13139,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
12462
13139
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12463
13140
  try {
12464
13141
  const url = new URL(rel, import.meta.url);
12465
- if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
13142
+ if (url.protocol === "file:" && fs13.existsSync(fileURLToPath2(url))) {
12466
13143
  builtUrl = url;
12467
13144
  break;
12468
13145
  }
@@ -12719,7 +13396,7 @@ var ProjectServerConnection = class {
12719
13396
  currentAuthToken() {
12720
13397
  if (this.authToken === void 0) {
12721
13398
  try {
12722
- const raw = fs12.readFileSync(
13399
+ const raw = fs13.readFileSync(
12723
13400
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
12724
13401
  "utf8"
12725
13402
  );
@@ -12984,7 +13661,7 @@ var ProjectServerConnection = class {
12984
13661
  if (!url) throw new Error("built codebase-index project server is unavailable");
12985
13662
  if (process.platform !== "win32") {
12986
13663
  try {
12987
- fs12.rmSync(this.endpoint, { force: true });
13664
+ fs13.rmSync(this.endpoint, { force: true });
12988
13665
  } catch {
12989
13666
  }
12990
13667
  }
@@ -13008,8 +13685,8 @@ var ProjectServerConnection = class {
13008
13685
  process.kill(pid);
13009
13686
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
13010
13687
  try {
13011
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
13012
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
13688
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
13689
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
13013
13690
  } catch {
13014
13691
  }
13015
13692
  return true;
@@ -13113,7 +13790,7 @@ import { Worker } from "node:worker_threads";
13113
13790
 
13114
13791
  // src/codebase-index/indexer.ts
13115
13792
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
13116
- import { execFile as execFile2 } from "node:child_process";
13793
+ import { execFile } from "node:child_process";
13117
13794
  import * as fs17 from "node:fs/promises";
13118
13795
  import { availableParallelism } from "node:os";
13119
13796
  import * as path23 from "node:path";
@@ -13124,8 +13801,8 @@ import {
13124
13801
  } from "@wrongstack/core/utils";
13125
13802
 
13126
13803
  // src/codebase-index/gitignore.ts
13127
- import * as fs13 from "node:fs/promises";
13128
- import * as path17 from "node:path";
13804
+ import * as fs14 from "node:fs/promises";
13805
+ import * as path18 from "node:path";
13129
13806
  import { compileGlob } from "@wrongstack/core/utils";
13130
13807
  function globBody(glob) {
13131
13808
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -13171,7 +13848,7 @@ function compileGitignore(lines) {
13171
13848
  async function loadGitignoreMatcher(projectRoot) {
13172
13849
  let lines = [];
13173
13850
  try {
13174
- const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
13851
+ const raw = await fs14.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
13175
13852
  lines = raw.split("\n");
13176
13853
  } catch {
13177
13854
  }
@@ -13181,8 +13858,434 @@ async function loadGitignoreMatcher(projectRoot) {
13181
13858
  // src/codebase-index/indexer.ts
13182
13859
  init_languages2();
13183
13860
 
13861
+ // src/codebase-index/module-resolver.ts
13862
+ init_languages2();
13863
+ import * as path19 from "node:path";
13864
+ var EXTENSIONS = {
13865
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
13866
+ py: [".py", ".pyi"],
13867
+ rs: [".rs"],
13868
+ jvm: [".java", ".kt", ".scala"],
13869
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
13870
+ ruby: [".rb"],
13871
+ go: [".go"]
13872
+ };
13873
+ var DIRECTORY_ENTRIES = {
13874
+ js: ["index"],
13875
+ py: ["__init__"],
13876
+ rs: ["mod"],
13877
+ ruby: ["index"]
13878
+ };
13879
+ function normalizeNamespace(value) {
13880
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
13881
+ }
13882
+ var ModuleResolver = class {
13883
+ structure;
13884
+ /** Lowercased portable path → the path as indexed (case is preserved). */
13885
+ byPath;
13886
+ /** Lowercased portable directory → files directly inside it, as indexed. */
13887
+ byDir;
13888
+ /** Normalized namespace → the file declaring it (first by path, stable). */
13889
+ byNamespace;
13890
+ constructor(structure, files, namespaces = []) {
13891
+ this.structure = structure;
13892
+ this.byPath = /* @__PURE__ */ new Map();
13893
+ this.byDir = /* @__PURE__ */ new Map();
13894
+ this.byNamespace = /* @__PURE__ */ new Map();
13895
+ const dirsByKey = /* @__PURE__ */ new Map();
13896
+ for (const file of files) {
13897
+ const portable = toPortablePath(file);
13898
+ const pathKey = portable.toLowerCase();
13899
+ const priorPath = this.byPath.get(pathKey);
13900
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
13901
+ else this.byPath.set(pathKey, file);
13902
+ const dir = path19.posix.dirname(portable);
13903
+ const dirKey = dir.toLowerCase();
13904
+ const knownDir = dirsByKey.get(dirKey);
13905
+ if (knownDir === void 0) {
13906
+ dirsByKey.set(dirKey, dir);
13907
+ this.byDir.set(dirKey, [file]);
13908
+ } else if (knownDir === dir) {
13909
+ this.byDir.get(dirKey)?.push(file);
13910
+ } else {
13911
+ dirsByKey.delete(dirKey);
13912
+ this.byDir.delete(dirKey);
13913
+ }
13914
+ }
13915
+ for (const { name, file } of namespaces) {
13916
+ const lang = detectLang(file);
13917
+ if (!lang) continue;
13918
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
13919
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
13920
+ this.byNamespace.set(key, file);
13921
+ }
13922
+ }
13923
+ }
13924
+ /**
13925
+ * Resolve `specifier` as written in `fromFile`.
13926
+ * Returns the indexed target path, or `undefined` when it is external or
13927
+ * cannot be located.
13928
+ */
13929
+ resolve(fromFile, lang, specifier) {
13930
+ const spec = specifier.trim().replace(/\\/g, "/");
13931
+ if (!spec) return void 0;
13932
+ const from = toPortablePath(fromFile);
13933
+ switch (languageFamily(lang)) {
13934
+ case "js":
13935
+ return this.resolveJs(from, spec);
13936
+ case "go":
13937
+ return this.resolveGo(spec);
13938
+ case "py":
13939
+ return this.resolvePython(from, spec);
13940
+ case "rs":
13941
+ return this.resolveRust(from, spec);
13942
+ case "jvm":
13943
+ return this.resolveJvm(spec);
13944
+ case "c":
13945
+ return this.resolveInclude(from, spec);
13946
+ case "ruby":
13947
+ return this.resolveRuby(from, spec);
13948
+ case "dotnet":
13949
+ case "php":
13950
+ case "elixir":
13951
+ case "haskell":
13952
+ return this.resolveNamespace(lang, spec);
13953
+ default:
13954
+ return void 0;
13955
+ }
13956
+ }
13957
+ /**
13958
+ * Resolve a namespace specifier to the file declaring it.
13959
+ *
13960
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
13961
+ * names a namespace outright, while PHP's `use App\Models\User` names a
13962
+ * *class* inside `App\Models`, so the prefix is what was declared.
13963
+ */
13964
+ resolveNamespace(lang, spec) {
13965
+ const family = languageFamily(lang);
13966
+ const normalized = normalizeNamespace(spec);
13967
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
13968
+ if (exact) return exact;
13969
+ const segments = normalized.split(".").filter(Boolean);
13970
+ if (segments.length < 2) return void 0;
13971
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
13972
+ }
13973
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
13974
+ lookup(candidate) {
13975
+ return this.byPath.get(path19.posix.normalize(candidate).toLowerCase());
13976
+ }
13977
+ /**
13978
+ * Try `base` verbatim, then `base` + each extension, then each directory
13979
+ * entry point inside `base`.
13980
+ */
13981
+ lookupWithExtensions(base, family) {
13982
+ const direct = this.lookup(base);
13983
+ if (direct) return direct;
13984
+ const extensions = EXTENSIONS[family] ?? [];
13985
+ const suffix = path19.posix.extname(base);
13986
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
13987
+ for (const ext of extensions) {
13988
+ const hit = this.lookup(`${stem}${ext}`);
13989
+ if (hit) return hit;
13990
+ }
13991
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
13992
+ for (const ext of extensions) {
13993
+ const hit = this.lookup(path19.posix.join(base, `${entry}${ext}`));
13994
+ if (hit) return hit;
13995
+ }
13996
+ }
13997
+ return void 0;
13998
+ }
13999
+ /**
14000
+ * A representative indexed file inside `dir`, for ecosystems whose import
14001
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
14002
+ *
14003
+ * The choice is deterministic — a file named after the directory, else the
14004
+ * first by name — so the same import always produces the same edge. Package
14005
+ * grouping is unaffected either way: every file in the directory carries the
14006
+ * same package label, so the package-level edge is exact regardless of which
14007
+ * member represents it.
14008
+ */
14009
+ representativeIn(dir, family) {
14010
+ const members = this.byDir.get(path19.posix.normalize(dir).toLowerCase());
14011
+ if (!members?.length) return void 0;
14012
+ const extensions = EXTENSIONS[family] ?? [];
14013
+ const eligible = members.filter((file) => extensions.includes(path19.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
14014
+ if (eligible.length === 0) return void 0;
14015
+ const base = path19.posix.basename(path19.posix.normalize(dir)).toLowerCase();
14016
+ const named = eligible.find(
14017
+ (file) => path19.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
14018
+ );
14019
+ return named ?? eligible[0];
14020
+ }
14021
+ // ─── Per-family resolution ──────────────────────────────────────────────────
14022
+ /** Relative specifiers, then workspace package names and their subpaths. */
14023
+ resolveJs(fromFile, spec) {
14024
+ if (spec.startsWith(".")) {
14025
+ const absolute = path19.posix.join(path19.posix.dirname(fromFile), spec);
14026
+ return this.lookupWithExtensions(absolute, "js");
14027
+ }
14028
+ const owner2 = this.structure.roots.find(
14029
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
14030
+ );
14031
+ if (!owner2?.importPath) return void 0;
14032
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
14033
+ if (!subpath) {
14034
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, "src/index"), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "index"), "js");
14035
+ }
14036
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, subpath), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "src", subpath), "js");
14037
+ }
14038
+ /** Go import paths are absolute module paths; a package is a directory. */
14039
+ resolveGo(spec) {
14040
+ const owner2 = this.structure.roots.find(
14041
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
14042
+ );
14043
+ if (!owner2?.importPath) return void 0;
14044
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
14045
+ return this.representativeIn(path19.posix.join(owner2.dir, subpath), "go");
14046
+ }
14047
+ /**
14048
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
14049
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
14050
+ */
14051
+ resolvePython(fromFile, spec) {
14052
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
14053
+ if (leadingDots > 0) {
14054
+ let base = path19.posix.dirname(fromFile);
14055
+ for (let i = 1; i < leadingDots; i++) base = path19.posix.dirname(base);
14056
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
14057
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest), "py");
14058
+ }
14059
+ const segments = spec.split(".").filter(Boolean);
14060
+ if (segments.length === 0) return void 0;
14061
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
14062
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
14063
+ const hit = this.lookupWithExtensions(path19.posix.join(base, ...segments), "py");
14064
+ if (hit) return hit;
14065
+ if (segments.length > 1) {
14066
+ const parent = this.lookupWithExtensions(
14067
+ path19.posix.join(base, ...segments.slice(0, -1)),
14068
+ "py"
14069
+ );
14070
+ if (parent) return parent;
14071
+ }
14072
+ }
14073
+ return void 0;
14074
+ }
14075
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
14076
+ resolveRust(fromFile, spec) {
14077
+ const segments = spec.split("::").filter(Boolean);
14078
+ if (segments.length === 0) return void 0;
14079
+ const head = segments[0];
14080
+ if (head === "self" || head === "super") {
14081
+ let base = path19.posix.dirname(fromFile);
14082
+ for (const segment of segments) {
14083
+ if (segment === "super") base = path19.posix.dirname(base);
14084
+ else if (segment !== "self") break;
14085
+ }
14086
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
14087
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest2), "rs");
14088
+ }
14089
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
14090
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
14091
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
14092
+ );
14093
+ if (!crate) {
14094
+ return this.lookupWithExtensions(
14095
+ path19.posix.join(path19.posix.dirname(fromFile), ...segments),
14096
+ "rs"
14097
+ );
14098
+ }
14099
+ const rest = segments.slice(1);
14100
+ for (const base of crate.sourceRoots) {
14101
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path19.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
14102
+ const exact = this.lookupWithExtensions(path19.posix.join(base, ...rest), "rs");
14103
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path19.posix.join(base, "lib"), "rs");
14104
+ if (hit) return hit;
14105
+ }
14106
+ return void 0;
14107
+ }
14108
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
14109
+ resolveJvm(spec) {
14110
+ const segments = spec.split(".").filter(Boolean);
14111
+ if (segments.length === 0) return void 0;
14112
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
14113
+ const wildcard = segments[segments.length - 1] === "*";
14114
+ const parts = wildcard ? segments.slice(0, -1) : segments;
14115
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
14116
+ const target = path19.posix.join(base, ...parts);
14117
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
14118
+ if (hit) return hit;
14119
+ }
14120
+ return void 0;
14121
+ }
14122
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
14123
+ resolveInclude(fromFile, spec) {
14124
+ const relative13 = this.lookupWithExtensions(
14125
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
14126
+ "c"
14127
+ );
14128
+ if (relative13) return relative13;
14129
+ for (const base of [
14130
+ path19.posix.join(this.structure.projectRoot, "include"),
14131
+ this.structure.projectRoot
14132
+ ]) {
14133
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "c");
14134
+ if (hit) return hit;
14135
+ }
14136
+ return void 0;
14137
+ }
14138
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
14139
+ resolveRuby(fromFile, spec) {
14140
+ const relative13 = this.lookupWithExtensions(
14141
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
14142
+ "ruby"
14143
+ );
14144
+ if (relative13) return relative13;
14145
+ for (const base of [
14146
+ path19.posix.join(this.structure.projectRoot, "lib"),
14147
+ this.structure.projectRoot
14148
+ ]) {
14149
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "ruby");
14150
+ if (hit) return hit;
14151
+ }
14152
+ return void 0;
14153
+ }
14154
+ };
14155
+
14156
+ // src/codebase-index/import-extractor.ts
14157
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
14158
+ var IMPORT_MAX_PER_FILE = 400;
14159
+ var DOTTED_IMPORT = [
14160
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
14161
+ ];
14162
+ var LANG_IMPORTS = {
14163
+ // Go and Python have real AST extractors; these patterns are the fallback for
14164
+ // machines with no Go toolchain or Python interpreter installed, where the
14165
+ // parser degrades to regex symbols and would otherwise contribute no edges.
14166
+ go: [
14167
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
14168
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
14169
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
14170
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
14171
+ ],
14172
+ py: [
14173
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
14174
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
14175
+ ],
14176
+ rs: [
14177
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
14178
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
14179
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
14180
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
14181
+ ],
14182
+ java: DOTTED_IMPORT,
14183
+ kotlin: DOTTED_IMPORT,
14184
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
14185
+ csharp: [
14186
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
14187
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
14188
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
14189
+ ],
14190
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
14191
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
14192
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
14193
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
14194
+ php: [
14195
+ // `use A\B\C` imports the class C, which is what the index has a symbol
14196
+ // for — the namespace symbol only covers the `A\B` prefix.
14197
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
14198
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
14199
+ ],
14200
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
14201
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
14202
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
14203
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
14204
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
14205
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
14206
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
14207
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
14208
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
14209
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
14210
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
14211
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
14212
+ html: [
14213
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
14214
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
14215
+ ],
14216
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
14217
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
14218
+ };
14219
+ function lastSegment(specifier) {
14220
+ const pathLike = /[/\\]|::/.test(specifier);
14221
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
14222
+ let last = segments[segments.length - 1] ?? specifier;
14223
+ if (last === "*" || last === "_") {
14224
+ last = segments[segments.length - 2] ?? specifier;
14225
+ }
14226
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
14227
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
14228
+ return dotted[dotted.length - 1] ?? last;
14229
+ }
14230
+ function newlineOffsets(content) {
14231
+ const offsets = [];
14232
+ for (let i = 0; i < content.length; i++) {
14233
+ if (content.charCodeAt(i) === 10) offsets.push(i);
14234
+ }
14235
+ return offsets;
14236
+ }
14237
+ function lineAt(offsets, index) {
14238
+ let low = 0;
14239
+ let high = offsets.length;
14240
+ while (low < high) {
14241
+ const mid = low + high >>> 1;
14242
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
14243
+ else high = mid;
14244
+ }
14245
+ return low + 1;
14246
+ }
14247
+ function hasImportPatterns(lang) {
14248
+ return LANG_IMPORTS[lang] !== void 0;
14249
+ }
14250
+ function extractImports(opts) {
14251
+ const patterns = LANG_IMPORTS[opts.lang];
14252
+ if (!patterns || !opts.content) return [];
14253
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
14254
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
14255
+ const refs = [];
14256
+ const seen = /* @__PURE__ */ new Set();
14257
+ const offsets = newlineOffsets(content);
14258
+ for (const pattern of patterns) {
14259
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
14260
+ for (const match of content.matchAll(re)) {
14261
+ if (refs.length >= limit) return refs;
14262
+ const specifier = match[1]?.trim();
14263
+ if (!specifier) continue;
14264
+ const module = specifier;
14265
+ const toName = pattern.name === "full" ? module : lastSegment(module);
14266
+ if (!toName) continue;
14267
+ const key = `${module}\0${toName}`;
14268
+ if (seen.has(key)) continue;
14269
+ seen.add(key);
14270
+ refs.push({
14271
+ fromId: 0,
14272
+ toName,
14273
+ callType: "import",
14274
+ line: lineAt(offsets, match.index ?? 0),
14275
+ lang: opts.lang,
14276
+ module
14277
+ });
14278
+ }
14279
+ }
14280
+ return refs;
14281
+ }
14282
+
13184
14283
  // src/codebase-index/parser-dispatch.ts
13185
14284
  async function parseFileContent(file, content, lang) {
14285
+ const parsed = await dispatch(file, content, lang);
14286
+ return withRelations(parsed, content, lang);
14287
+ }
14288
+ async function dispatch(file, content, lang) {
13186
14289
  switch (lang) {
13187
14290
  case "ts":
13188
14291
  case "tsx":
@@ -13217,6 +14320,13 @@ async function parseFileContent(file, content, lang) {
13217
14320
  }
13218
14321
  }
13219
14322
  }
14323
+ function withRelations(parsed, content, lang) {
14324
+ let refs = parsed.refs ?? [];
14325
+ if (refs.length === 0 && hasImportPatterns(lang)) {
14326
+ refs = extractImports({ content, lang });
14327
+ }
14328
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
14329
+ }
13220
14330
 
13221
14331
  // src/codebase-index/indexer.ts
13222
14332
  var YIELD_EVERY_N = 50;
@@ -13253,7 +14363,7 @@ function normalizeComparablePath(value) {
13253
14363
  }
13254
14364
  function gitOutput(projectRoot, args) {
13255
14365
  return new Promise((resolve17, reject) => {
13256
- execFile2(
14366
+ execFile(
13257
14367
  "git",
13258
14368
  ["-C", projectRoot, ...args],
13259
14369
  {
@@ -13384,13 +14494,40 @@ function assignRefsToSymbols2(refs, symbols) {
13384
14494
  }
13385
14495
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
13386
14496
  if (!owner2 || owner2.id <= 0) continue;
13387
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
14497
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
13388
14498
  if (seen.has(key)) continue;
13389
14499
  seen.add(key);
13390
14500
  assigned.push({ ...ref, fromId: owner2.id });
13391
14501
  }
13392
14502
  return assigned;
13393
14503
  }
14504
+ async function resolveProjectRelations(store, projectRoot, opts) {
14505
+ if (opts.signal?.aborted) return;
14506
+ try {
14507
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
14508
+ if (indexedFiles.length === 0) return;
14509
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
14510
+ if (opts.signal?.aborted) return;
14511
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
14512
+ const resolver = new ModuleResolver(
14513
+ structure,
14514
+ indexedFiles,
14515
+ store.getNamespaceDeclarations()
14516
+ );
14517
+ const pending2 = store.getUnresolvedImports(opts.onlyFiles);
14518
+ const resolutions = [];
14519
+ for (const entry of pending2) {
14520
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
14521
+ if (toFile && toFile !== entry.fromFile) {
14522
+ resolutions.push({ ...entry, toFile });
14523
+ }
14524
+ }
14525
+ if (opts.signal?.aborted) return;
14526
+ store.applyImportResolutions(resolutions);
14527
+ } catch (err) {
14528
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
14529
+ }
14530
+ }
13394
14531
  async function runIndexerWithStore(store, opts) {
13395
14532
  const { projectRoot, langs, ignore = [], signal } = opts;
13396
14533
  const relationGraphVersion = "2";
@@ -13635,6 +14772,14 @@ async function runIndexerWithStore(store, opts) {
13635
14772
  }
13636
14773
  }
13637
14774
  if (needsFullRefResolution) store.resolveRefs();
14775
+ await resolveProjectRelations(store, projectRoot, {
14776
+ // A watcher run re-resolves only what it touched; a full run (or a contract
14777
+ // bump) re-resolves everything, because a newly indexed file can be the
14778
+ // target of imports written long before it.
14779
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
14780
+ errors,
14781
+ signal
14782
+ });
13638
14783
  store.setMetadata("ref_resolution_version", refResolutionVersion);
13639
14784
  store.setMetadata("relation_graph_version", relationGraphVersion);
13640
14785
  if (!opts.files || filesIndexed >= 50) store.optimize();
@@ -15184,17 +16329,17 @@ import {
15184
16329
  } from "@wrongstack/core/design";
15185
16330
  async function resolveReal(p) {
15186
16331
  const resolved = path25.resolve(p);
15187
- let probe2 = resolved;
16332
+ let probe = resolved;
15188
16333
  const missing = [];
15189
16334
  for (; ; ) {
15190
16335
  try {
15191
- return path25.resolve(await fs20.realpath(probe2), ...missing);
16336
+ return path25.resolve(await fs20.realpath(probe), ...missing);
15192
16337
  } catch (err) {
15193
16338
  if (err.code === "ENOENT") {
15194
- const parent = path25.dirname(probe2);
15195
- if (parent === probe2) return resolved;
15196
- missing.unshift(path25.basename(probe2));
15197
- probe2 = parent;
16339
+ const parent = path25.dirname(probe);
16340
+ if (parent === probe) return resolved;
16341
+ missing.unshift(path25.basename(probe));
16342
+ probe = parent;
15198
16343
  continue;
15199
16344
  }
15200
16345
  return resolved;
@@ -15469,7 +16614,7 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
15469
16614
 
15470
16615
  // src/diff.ts
15471
16616
  init_util();
15472
- import { spawn as spawn8 } from "node:child_process";
16617
+ import { spawn as spawn7 } from "node:child_process";
15473
16618
  import { statSync as statSync3 } from "node:fs";
15474
16619
  import * as fs21 from "node:fs/promises";
15475
16620
  import * as path26 from "node:path";
@@ -15572,7 +16717,7 @@ function runGit(args, cwd, signal) {
15572
16717
  return new Promise((resolve17) => {
15573
16718
  let stdout = "";
15574
16719
  let stderr = "";
15575
- const child = spawn8("git", args, {
16720
+ const child = spawn7("git", args, {
15576
16721
  cwd,
15577
16722
  signal,
15578
16723
  env: buildChildEnv3(),
@@ -15783,7 +16928,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
15783
16928
 
15784
16929
  // src/e2e.ts
15785
16930
  init_util();
15786
- import { open, readdir as readdir6 } from "node:fs/promises";
16931
+ import { open, readdir as readdir7 } from "node:fs/promises";
15787
16932
  import * as path27 from "node:path";
15788
16933
  async function readBoundedText(filePath, maxBytes) {
15789
16934
  let handle;
@@ -15901,7 +17046,7 @@ async function scanWorkspace(root, maxDepth, signal) {
15901
17046
  }
15902
17047
  let entries;
15903
17048
  try {
15904
- entries = await readdir6(current.directory, { withFileTypes: true });
17049
+ entries = await readdir7(current.directory, { withFileTypes: true });
15905
17050
  } catch {
15906
17051
  continue;
15907
17052
  }
@@ -15960,7 +17105,7 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
15960
17105
  while (true) {
15961
17106
  const names = /* @__PURE__ */ new Set();
15962
17107
  try {
15963
- for (const entry of await readdir6(directory)) names.add(entry);
17108
+ for (const entry of await readdir7(directory)) names.add(entry);
15964
17109
  } catch {
15965
17110
  }
15966
17111
  if (names.has("pnpm-lock.yaml")) return "pnpm";
@@ -16028,7 +17173,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
16028
17173
  if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
16029
17174
  let entries;
16030
17175
  try {
16031
- entries = await readdir6(directory, { withFileTypes: true });
17176
+ entries = await readdir7(directory, { withFileTypes: true });
16032
17177
  } catch {
16033
17178
  continue;
16034
17179
  }
@@ -16229,7 +17374,7 @@ function findLadderMatches(fileLf, oldLf) {
16229
17374
  const exact = [];
16230
17375
  let idx = fileLf.indexOf(oldLf);
16231
17376
  while (idx !== -1) {
16232
- exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
17377
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt2(fileLf, idx) });
16233
17378
  idx = fileLf.indexOf(oldLf, idx + 1);
16234
17379
  }
16235
17380
  if (exact.length > 0) return { tier: "exact", matches: exact };
@@ -16261,7 +17406,7 @@ function findLadderMatches(fileLf, oldLf) {
16261
17406
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
16262
17407
  return fuzzyScan(fileLines, needleLines, offsets);
16263
17408
  }
16264
- function lineAt(text, pos) {
17409
+ function lineAt2(text, pos) {
16265
17410
  if (pos < 512) {
16266
17411
  let line2 = 1;
16267
17412
  for (let i = 0; i < pos; i++) {
@@ -16723,7 +17868,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
16723
17868
  };
16724
17869
 
16725
17870
  // src/exec.ts
16726
- import { spawn as spawn9 } from "node:child_process";
17871
+ import { spawn as spawn8 } from "node:child_process";
16727
17872
  import {
16728
17873
  emitProcessCompleted as emitProcessCompleted3,
16729
17874
  emitProcessOutput as emitProcessOutput3,
@@ -17644,6 +18789,26 @@ var BLOCKED_ARG_PATTERNS = {
17644
18789
  pnpm: [],
17645
18790
  npx: []
17646
18791
  };
18792
+ var BLOCKED_OPTION_NAMES = {
18793
+ git: /* @__PURE__ */ new Set([
18794
+ "--exec",
18795
+ "--upload-pack",
18796
+ "--receive-pack",
18797
+ "--exec-path",
18798
+ "--git-dir",
18799
+ "--work-tree",
18800
+ "--namespace",
18801
+ "-c",
18802
+ "--config",
18803
+ "--config-env",
18804
+ "-C"
18805
+ ]),
18806
+ find: /* @__PURE__ */ new Set(["-exec", "-ok", "-execdir"])
18807
+ };
18808
+ function optionName(arg) {
18809
+ const eq = arg.indexOf("=");
18810
+ return eq > 0 ? arg.slice(0, eq) : arg;
18811
+ }
17647
18812
  var BLOCKED_SUBCOMMANDS = {
17648
18813
  docker: /* @__PURE__ */ new Set(["push"]),
17649
18814
  podman: /* @__PURE__ */ new Set(["push"]),
@@ -17681,6 +18846,15 @@ function validateArgs(cmd, args) {
17681
18846
  const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
17682
18847
  if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
17683
18848
  }
18849
+ const blockedOptions = BLOCKED_OPTION_NAMES[cmd];
18850
+ if (blockedOptions) {
18851
+ for (const arg of args) {
18852
+ if (arg === "--") break;
18853
+ if (blockedOptions.has(optionName(arg))) {
18854
+ return `Blocked option "${optionName(arg)}" for command "${cmd}"`;
18855
+ }
18856
+ }
18857
+ }
17684
18858
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
17685
18859
  if (!blocked) return null;
17686
18860
  for (const arg of args) {
@@ -17863,7 +19037,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
17863
19037
  };
17864
19038
  let child;
17865
19039
  try {
17866
- child = spawn9(spawnCmd, spawnArgs, {
19040
+ child = spawn8(spawnCmd, spawnArgs, {
17867
19041
  cwd,
17868
19042
  env: buildChildEnv2(sessionId),
17869
19043
  stdio: ["ignore", "pipe", "pipe"],
@@ -18497,7 +19671,7 @@ async function detectFixer(cwd) {
18497
19671
 
18498
19672
  // src/git.ts
18499
19673
  init_util();
18500
- import { spawn as spawn10 } from "node:child_process";
19674
+ import { spawn as spawn9 } from "node:child_process";
18501
19675
  import { statSync as statSync4 } from "node:fs";
18502
19676
  import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
18503
19677
  import { assessCommitSafety } from "@wrongstack/core/coordination";
@@ -18761,7 +19935,7 @@ function runGit2(args, cwd, signal) {
18761
19935
  return new Promise((resolve17) => {
18762
19936
  let stdout = "";
18763
19937
  let stderr = "";
18764
- const child = spawn10("git", args, {
19938
+ const child = spawn9("git", args, {
18765
19939
  cwd,
18766
19940
  signal,
18767
19941
  env: buildChildEnv4(),
@@ -18952,7 +20126,7 @@ var globTool = {
18952
20126
  };
18953
20127
 
18954
20128
  // src/grep.ts
18955
- import { spawn as spawn11 } from "node:child_process";
20129
+ import { spawn as spawn10 } from "node:child_process";
18956
20130
  import * as fs25 from "node:fs/promises";
18957
20131
  import * as path31 from "node:path";
18958
20132
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
@@ -19109,7 +20283,7 @@ var grepTool = {
19109
20283
  async function detectRg(signal) {
19110
20284
  return new Promise((resolve17) => {
19111
20285
  try {
19112
- const p = spawn11("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
20286
+ const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
19113
20287
  p.on("error", () => resolve17(false));
19114
20288
  p.on("close", (code) => resolve17(code === 0));
19115
20289
  } catch {
@@ -19143,7 +20317,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
19143
20317
  const FLUSH_AT = 16;
19144
20318
  const MAX_BUF_BYTES = 1e6;
19145
20319
  let bufOverflow = false;
19146
- const child = spawn11("rg", args, {
20320
+ const child = spawn10("rg", args, {
19147
20321
  signal,
19148
20322
  env: buildChildEnv5(),
19149
20323
  // rg diagnostics are not part of the tool result. Ignoring stderr avoids
@@ -19440,7 +20614,7 @@ async function runNative(input, base, mode, limit, signal) {
19440
20614
  init_spawn_stream();
19441
20615
  init_util();
19442
20616
  init_legacy_bridge();
19443
- import { join as join25 } from "node:path";
20617
+ import { join as join24 } from "node:path";
19444
20618
  import {
19445
20619
  detectEcosystem as detectPackageEcosystem,
19446
20620
  recordPackageAction
@@ -19624,17 +20798,17 @@ function resolveManifestPath(cwd, pkgManager) {
19624
20798
  case "pnpm":
19625
20799
  case "yarn":
19626
20800
  case "npm":
19627
- return join25(cwd, "package.json");
20801
+ return join24(cwd, "package.json");
19628
20802
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
19629
20803
  default:
19630
- return join25(cwd, "package.json");
20804
+ return join24(cwd, "package.json");
19631
20805
  }
19632
20806
  }
19633
20807
 
19634
20808
  // src/json.ts
19635
- init_util();
19636
20809
  import * as fs26 from "node:fs/promises";
19637
20810
  import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
20811
+ init_util();
19638
20812
  var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
19639
20813
  var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
19640
20814
  var JsonFileTooLargeError = class extends Error {
@@ -20073,8 +21247,12 @@ function validateJsonSchema(data, schema) {
20073
21247
  }
20074
21248
  }
20075
21249
  if (typeof value === "string" && s["pattern"]) {
20076
- const re = new RegExp(s["pattern"]);
20077
- if (!re.test(value)) errors.push(`${path39}: does not match pattern ${s["pattern"]}`);
21250
+ const compiled = compileUserRegex(s["pattern"], "");
21251
+ if (!compiled.ok) {
21252
+ errors.push(`${path39}: invalid schema pattern \u2014 ${compiled.reason}`);
21253
+ } else if (!compiled.regex.test(capSubject(value))) {
21254
+ errors.push(`${path39}: does not match pattern ${s["pattern"]}`);
21255
+ }
20078
21256
  }
20079
21257
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
20080
21258
  errors.push(`${path39}: string too short (min ${s["minLength"]})`);
@@ -22473,7 +23651,7 @@ async function detectLinter(cwd) {
22473
23651
  }
22474
23652
 
22475
23653
  // src/logs.ts
22476
- import { spawn as spawn12 } from "node:child_process";
23654
+ import { spawn as spawn11 } from "node:child_process";
22477
23655
  import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
22478
23656
  init_util();
22479
23657
  var logsTool = {
@@ -22580,7 +23758,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
22580
23758
  clearTimeout(timer);
22581
23759
  resolve17(result);
22582
23760
  };
22583
- const child = spawn12("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
23761
+ const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
22584
23762
  const timer = setTimeout(() => {
22585
23763
  child.kill("SIGTERM");
22586
23764
  finish(empty());
@@ -22689,7 +23867,7 @@ function parseLine(line) {
22689
23867
  // src/outdated.ts
22690
23868
  init_util();
22691
23869
  init_win32_resolve();
22692
- import { spawn as spawn13 } from "node:child_process";
23870
+ import { spawn as spawn12 } from "node:child_process";
22693
23871
  import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
22694
23872
  var outdatedTool = {
22695
23873
  name: "outdated",
@@ -22809,7 +23987,7 @@ function runOutdated(manager, args, cwd, signal) {
22809
23987
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
22810
23988
  const spawnCmd = shim?.command ?? resolved;
22811
23989
  const spawnArgs = shim?.args ?? args;
22812
- const child = spawn13(spawnCmd, spawnArgs, {
23990
+ const child = spawn12(spawnCmd, spawnArgs, {
22813
23991
  cwd,
22814
23992
  signal,
22815
23993
  env: buildChildEnv7(),
@@ -22875,7 +24053,7 @@ function parseOutdatedOutput(json2, exitCode) {
22875
24053
 
22876
24054
  // src/patch.ts
22877
24055
  init_util();
22878
- import { spawn as spawn14 } from "node:child_process";
24056
+ import { spawn as spawn13 } from "node:child_process";
22879
24057
  import * as fs27 from "node:fs/promises";
22880
24058
  import * as os9 from "node:os";
22881
24059
  import * as path32 from "node:path";
@@ -23017,7 +24195,7 @@ function runPatch(args, cwd, signal) {
23017
24195
  let stdout = "";
23018
24196
  let stderr = "";
23019
24197
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
23020
- const child = spawn14("patch", args, {
24198
+ const child = spawn13("patch", args, {
23021
24199
  cwd,
23022
24200
  signal,
23023
24201
  env,
@@ -23614,7 +24792,7 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
23614
24792
  }
23615
24793
 
23616
24794
  // src/replace.ts
23617
- import { spawn as spawn15 } from "node:child_process";
24795
+ import { spawn as spawn14 } from "node:child_process";
23618
24796
  import * as fs29 from "node:fs/promises";
23619
24797
  import * as path33 from "node:path";
23620
24798
  import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
@@ -23799,7 +24977,7 @@ async function globFiles(pattern, base, extraGlob) {
23799
24977
  function checkRg() {
23800
24978
  return new Promise((resolve17) => {
23801
24979
  try {
23802
- const p = spawn15("rg", ["--version"], {
24980
+ const p = spawn14("rg", ["--version"], {
23803
24981
  env: buildChildEnv9(),
23804
24982
  stdio: "ignore",
23805
24983
  windowsHide: true
@@ -23813,7 +24991,7 @@ function checkRg() {
23813
24991
  }
23814
24992
  function spawnRgFind(pattern, base) {
23815
24993
  const args = ["--files", "--glob", pattern, base];
23816
- const child = spawn15("rg", args, {
24994
+ const child = spawn14("rg", args, {
23817
24995
  signal: AbortSignal.timeout(3e4),
23818
24996
  env: buildChildEnv9(),
23819
24997
  stdio: ["ignore", "pipe", "pipe"],
@@ -25852,7 +27030,7 @@ var writeTool = {
25852
27030
  required: ["path", "content"]
25853
27031
  },
25854
27032
  async execute(input, ctx, opts) {
25855
- return writeFile7(input, ctx, opts?.signal);
27033
+ return writeFile6(input, ctx, opts?.signal);
25856
27034
  },
25857
27035
  async *executeStream(input, ctx, opts) {
25858
27036
  const prepared = await prepareWrite(input, ctx);
@@ -25865,7 +27043,7 @@ var writeTool = {
25865
27043
  yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
25866
27044
  }
25867
27045
  };
25868
- async function writeFile7(input, ctx, signal) {
27046
+ async function writeFile6(input, ctx, signal) {
25869
27047
  return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
25870
27048
  }
25871
27049
  async function prepareWrite(input, ctx) {