@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/pack.js CHANGED
@@ -721,7 +721,7 @@ var init_process_registry = __esm({
721
721
  const p = this.processes.get(pid);
722
722
  if (!p) return false;
723
723
  if (p.killed) return true;
724
- if (p.protected) return false;
724
+ if (p.protected && opts.includeProtected !== true) return false;
725
725
  if (opts.preserveBackground && p.background) return false;
726
726
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
727
727
  const isWin5 = os.platform() === "win32";
@@ -772,9 +772,13 @@ var init_process_registry = __esm({
772
772
  killAll(opts = {}) {
773
773
  const pids = Array.from(this.processes.keys());
774
774
  const killed = [];
775
+ const includeProtected = opts.includeProtected === true;
775
776
  for (const pid of pids) {
776
777
  const p = this.processes.get(pid);
777
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
778
+ if (!p) continue;
779
+ if (p.protected && !includeProtected) continue;
780
+ if (opts.preserveBackground && p.background) continue;
781
+ if (this.kill(pid, opts)) killed.push(pid);
778
782
  }
779
783
  return killed;
780
784
  }
@@ -1138,18 +1142,18 @@ async function resolveRealInsideRoot(absPath, ctx) {
1138
1142
  const realRoots = await Promise.all(
1139
1143
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1140
1144
  );
1141
- let probe2 = absPath;
1145
+ let probe = absPath;
1142
1146
  const pendingTail = [];
1143
1147
  for (; ; ) {
1144
1148
  let real;
1145
1149
  try {
1146
- real = await fsp2.realpath(probe2);
1150
+ real = await fsp2.realpath(probe);
1147
1151
  } catch (err) {
1148
1152
  if (err.code === "ENOENT") {
1149
- const parent = path3.dirname(probe2);
1150
- if (parent === probe2) return absPath;
1151
- pendingTail.unshift(path3.basename(probe2));
1152
- probe2 = parent;
1153
+ const parent = path3.dirname(probe);
1154
+ if (parent === probe) return absPath;
1155
+ pendingTail.unshift(path3.basename(probe));
1156
+ probe = parent;
1153
1157
  continue;
1154
1158
  }
1155
1159
  throw err;
@@ -4968,20 +4972,23 @@ var init_legacy_bridge = __esm({
4968
4972
  });
4969
4973
 
4970
4974
  // src/codebase-index/languages.ts
4971
- import * as path18 from "node:path";
4975
+ import * as path13 from "node:path";
4972
4976
  function detectLang(file) {
4973
- const base = path18.basename(file);
4977
+ const base = path13.basename(file);
4974
4978
  const lowerBase = base.toLowerCase();
4975
4979
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4976
4980
  return "ts";
4977
4981
  }
4978
4982
  const special = SPECIAL_FILENAMES[lowerBase];
4979
4983
  if (special) return special;
4980
- const ext = path18.extname(base).toLowerCase();
4984
+ const ext = path13.extname(base).toLowerCase();
4981
4985
  if (!ext) return null;
4982
4986
  return EXT_TO_LANG[ext] ?? null;
4983
4987
  }
4984
- var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES;
4988
+ function languageFamily(lang) {
4989
+ return LANG_FAMILY[lang] ?? "other";
4990
+ }
4991
+ var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
4985
4992
  var init_languages2 = __esm({
4986
4993
  "src/codebase-index/languages.ts"() {
4987
4994
  "use strict";
@@ -5072,6 +5079,52 @@ var init_languages2 = __esm({
5072
5079
  procfile: "other",
5073
5080
  justfile: "other"
5074
5081
  };
5082
+ LANG_FAMILY = {
5083
+ // Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
5084
+ // imports from — and is imported by — plain .ts files.
5085
+ ts: "js",
5086
+ tsx: "js",
5087
+ js: "js",
5088
+ jsx: "js",
5089
+ vue: "js",
5090
+ svelte: "js",
5091
+ go: "go",
5092
+ py: "py",
5093
+ rs: "rs",
5094
+ // The JVM resolves across languages: Kotlin and Scala call Java directly.
5095
+ java: "jvm",
5096
+ kotlin: "jvm",
5097
+ scala: "jvm",
5098
+ csharp: "dotnet",
5099
+ // A .h header is consumed by both C and C++ translation units.
5100
+ c: "c",
5101
+ cpp: "c",
5102
+ ruby: "ruby",
5103
+ php: "php",
5104
+ swift: "swift",
5105
+ dart: "dart",
5106
+ elixir: "elixir",
5107
+ haskell: "haskell",
5108
+ zig: "zig",
5109
+ lua: "lua",
5110
+ r: "r",
5111
+ shell: "shell",
5112
+ sql: "sql",
5113
+ json: "data",
5114
+ yaml: "data",
5115
+ toml: "data",
5116
+ html: "web",
5117
+ css: "web",
5118
+ proto: "proto",
5119
+ graphql: "graphql",
5120
+ md: "other",
5121
+ other: "other"
5122
+ };
5123
+ LANG_FAMILY_ENTRIES = Object.freeze(
5124
+ Object.entries(LANG_FAMILY).map(
5125
+ ([lang, family]) => Object.freeze([lang, family])
5126
+ )
5127
+ );
5075
5128
  }
5076
5129
  });
5077
5130
 
@@ -5236,7 +5289,7 @@ function getTypeName(name) {
5236
5289
  function deduplicateRefs(refs) {
5237
5290
  const seen = /* @__PURE__ */ new Set();
5238
5291
  return refs.filter((r) => {
5239
- const key = `${r.toName}:${r.callType}:${r.line}`;
5292
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
5240
5293
  if (seen.has(key)) return false;
5241
5294
  seen.add(key);
5242
5295
  return true;
@@ -5246,10 +5299,16 @@ function getImportSpecifierName(spec) {
5246
5299
  return spec.propertyName?.text ?? spec.name.text;
5247
5300
  }
5248
5301
  function emitImportSpecifierRefs(node, refs, lineNum) {
5302
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5249
5303
  const clause = node.importClause;
5250
- if (!clause) return;
5304
+ if (!clause) {
5305
+ if (module) {
5306
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5307
+ }
5308
+ return;
5309
+ }
5251
5310
  if (clause.name) {
5252
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5311
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5253
5312
  }
5254
5313
  const bindings = clause.namedBindings;
5255
5314
  if (!bindings) return;
@@ -5259,26 +5318,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
5259
5318
  fromId: 0,
5260
5319
  toName: getImportSpecifierName(element),
5261
5320
  callType: "import",
5262
- line: lineNum
5321
+ line: lineNum,
5322
+ module
5263
5323
  });
5264
5324
  }
5265
5325
  } else if (ts.isNamespaceImport(bindings)) {
5266
- refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
5326
+ refs.push({
5327
+ fromId: 0,
5328
+ toName: bindings.name.text,
5329
+ callType: "import",
5330
+ line: lineNum,
5331
+ module
5332
+ });
5267
5333
  }
5268
5334
  }
5335
+ function moduleSpecifierOf(node) {
5336
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
5337
+ }
5269
5338
  function emitExportSpecifierRefs(node, refs, lineNum) {
5339
+ const module = moduleSpecifierOf(node.moduleSpecifier);
5270
5340
  const clause = node.exportClause;
5271
5341
  if (clause && ts.isNamespaceExport(clause)) {
5272
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5342
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
5273
5343
  return;
5274
5344
  }
5275
5345
  if (clause && ts.isNamedExports(clause)) {
5276
5346
  for (const element of clause.elements) {
5277
5347
  const originalName = element.propertyName?.text ?? element.name.text;
5278
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
5348
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
5279
5349
  }
5280
5350
  return;
5281
5351
  }
5352
+ if (module) {
5353
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
5354
+ }
5282
5355
  }
5283
5356
  var ts, tsLoad, kindMapCache;
5284
5357
  var init_ts_parser = __esm({
@@ -5290,6 +5363,82 @@ var init_ts_parser = __esm({
5290
5363
  }
5291
5364
  });
5292
5365
 
5366
+ // src/codebase-index/parser-output.ts
5367
+ function coerceSymbols(value) {
5368
+ if (!Array.isArray(value)) return [];
5369
+ return value.flatMap((entry) => {
5370
+ const candidate = entry;
5371
+ if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
5372
+ return [
5373
+ {
5374
+ name: candidate.name,
5375
+ kind: candidate.kind,
5376
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5377
+ col: typeof candidate.col === "number" ? candidate.col : 0,
5378
+ signature: typeof candidate.signature === "string" ? candidate.signature : "",
5379
+ scope: typeof candidate.scope === "string" ? candidate.scope : ""
5380
+ }
5381
+ ];
5382
+ });
5383
+ }
5384
+ function coerceRefs(value, lang) {
5385
+ if (!Array.isArray(value)) return [];
5386
+ return value.flatMap((entry) => {
5387
+ const candidate = entry;
5388
+ if (typeof candidate.toName !== "string" || !candidate.toName) return [];
5389
+ if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
5390
+ const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
5391
+ return [
5392
+ {
5393
+ fromId: 0,
5394
+ toName: candidate.toName,
5395
+ callType: candidate.callType,
5396
+ line: typeof candidate.line === "number" ? candidate.line : 1,
5397
+ lang,
5398
+ module
5399
+ }
5400
+ ];
5401
+ });
5402
+ }
5403
+ function parseParserOutput(stdout, lang) {
5404
+ const trimmed = stdout.trim();
5405
+ if (!trimmed) return { symbols: [], refs: [] };
5406
+ let parsed;
5407
+ try {
5408
+ parsed = JSON.parse(trimmed);
5409
+ } catch {
5410
+ return { symbols: [], refs: [] };
5411
+ }
5412
+ if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
5413
+ const record = parsed;
5414
+ return {
5415
+ symbols: coerceSymbols(record.symbols),
5416
+ refs: dedupeRefs(coerceRefs(record.refs, lang))
5417
+ };
5418
+ }
5419
+ function dedupeRefs(refs) {
5420
+ const seen = /* @__PURE__ */ new Set();
5421
+ return refs.filter((ref) => {
5422
+ const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
5423
+ if (seen.has(key)) return false;
5424
+ seen.add(key);
5425
+ return true;
5426
+ });
5427
+ }
5428
+ var CALL_TYPES;
5429
+ var init_parser_output = __esm({
5430
+ "src/codebase-index/parser-output.ts"() {
5431
+ "use strict";
5432
+ CALL_TYPES = /* @__PURE__ */ new Set([
5433
+ "call",
5434
+ "type_ref",
5435
+ "inherit",
5436
+ "implement",
5437
+ "import"
5438
+ ]);
5439
+ }
5440
+ });
5441
+
5293
5442
  // src/codebase-index/spawn-gate.ts
5294
5443
  function withSpawnGate(fn) {
5295
5444
  const run = chain.then(fn, fn);
@@ -5315,8 +5464,8 @@ __export(go_parser_exports, {
5315
5464
  });
5316
5465
  import { spawn as spawn5 } from "node:child_process";
5317
5466
  import * as os6 from "node:os";
5318
- import * as path19 from "node:path";
5319
- import * as fs14 from "node:fs/promises";
5467
+ import * as path20 from "node:path";
5468
+ import * as fs15 from "node:fs/promises";
5320
5469
  async function parseSymbols2(opts) {
5321
5470
  const { file, content, lang } = opts;
5322
5471
  try {
@@ -5324,7 +5473,8 @@ async function parseSymbols2(opts) {
5324
5473
  if (parsed.symbols.length > 0) {
5325
5474
  return parsed;
5326
5475
  }
5327
- return fallbackParse(file, content, lang);
5476
+ const fallback = fallbackParse(file, content, lang);
5477
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
5328
5478
  } catch {
5329
5479
  return fallbackParse(file, content, lang);
5330
5480
  }
@@ -5388,9 +5538,9 @@ async function syncGoParse(filePath, content, lang) {
5388
5538
  try {
5389
5539
  let scriptPath = _cachedGoScriptPath;
5390
5540
  if (!scriptPath) {
5391
- const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5392
- scriptPath = path19.join(tmpDir, "parse.go");
5393
- await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5541
+ const tmpDir = await fs15.mkdtemp(path20.join(os6.tmpdir(), "ws-go-parse-"));
5542
+ scriptPath = path20.join(tmpDir, "parse.go");
5543
+ await fs15.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5394
5544
  _cachedGoScriptPath = scriptPath;
5395
5545
  }
5396
5546
  const goBinary = resolveWin32Command("go");
@@ -5432,8 +5582,8 @@ async function syncGoParse(filePath, content, lang) {
5432
5582
  if (code !== 0 || !stdout.trim()) {
5433
5583
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5434
5584
  }
5435
- const raw = JSON.parse(stdout.trim());
5436
- const symbols = raw.map((s) => ({
5585
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
5586
+ const symbols = rawSymbols.map((s) => ({
5437
5587
  id: 0,
5438
5588
  lang,
5439
5589
  kind: s.kind,
@@ -5446,7 +5596,7 @@ async function syncGoParse(filePath, content, lang) {
5446
5596
  scope: s.scope ?? "",
5447
5597
  text: `${s.name} ${s.signature ?? ""}`.trim()
5448
5598
  }));
5449
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
5599
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
5450
5600
  } catch {
5451
5601
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
5452
5602
  }
@@ -5456,6 +5606,7 @@ var init_go_parser = __esm({
5456
5606
  "src/codebase-index/go-parser.ts"() {
5457
5607
  "use strict";
5458
5608
  init_win32_resolve();
5609
+ init_parser_output();
5459
5610
  init_spawn_gate();
5460
5611
  init_languages2();
5461
5612
  GO_PARSE_SCRIPT = `
@@ -5469,6 +5620,7 @@ import (
5469
5620
  "go/token"
5470
5621
  "io"
5471
5622
  "os"
5623
+ "strconv"
5472
5624
  "strings"
5473
5625
  )
5474
5626
 
@@ -5481,16 +5633,34 @@ type Sym struct {
5481
5633
  Scope string \`json:"scope"\`
5482
5634
  }
5483
5635
 
5636
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
5637
+ // yields both. Module is the import path for CallType "import", else empty.
5638
+ type Ref struct {
5639
+ ToName string \`json:"toName"\`
5640
+ CallType string \`json:"callType"\`
5641
+ Line int \`json:"line"\`
5642
+ Module string \`json:"module"\`
5643
+ }
5644
+
5645
+ type Result struct {
5646
+ Symbols []Sym \`json:"symbols"\`
5647
+ Refs []Ref \`json:"refs"\`
5648
+ }
5649
+
5650
+ func emptyResult() string {
5651
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
5652
+ }
5653
+
5484
5654
  func main() {
5485
5655
  src, err := io.ReadAll(os.Stdin)
5486
5656
  if err != nil {
5487
- fmt.Print("[]")
5657
+ fmt.Print(emptyResult())
5488
5658
  return
5489
5659
  }
5490
5660
  fset := token.NewFileSet()
5491
5661
  node, err := parser.ParseFile(fset, "src.go", src, 0)
5492
5662
  if err != nil {
5493
- fmt.Print("[]")
5663
+ fmt.Print(emptyResult())
5494
5664
  return
5495
5665
  }
5496
5666
 
@@ -5554,9 +5724,43 @@ func main() {
5554
5724
  }
5555
5725
  }
5556
5726
 
5557
- data, err := json.Marshal(syms)
5727
+ refs := []Ref{}
5728
+ ast.Inspect(node, func(n ast.Node) bool {
5729
+ switch expr := n.(type) {
5730
+ case *ast.CallExpr:
5731
+ line := fset.Position(expr.Pos()).Line
5732
+ switch fun := expr.Fun.(type) {
5733
+ case *ast.Ident:
5734
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
5735
+ case *ast.SelectorExpr:
5736
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
5737
+ // declared symbol name, so it resolves the same way the TypeScript
5738
+ // and Python extractors' call refs do.
5739
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
5740
+ }
5741
+ case *ast.ImportSpec:
5742
+ if expr.Path != nil {
5743
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
5744
+ line := fset.Position(expr.Pos()).Line
5745
+ // A Go import names a package, not a symbol; the package's
5746
+ // last path segment is the name it is referenced by.
5747
+ name := importPath
5748
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
5749
+ name = importPath[idx+1:]
5750
+ }
5751
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
5752
+ }
5753
+ }
5754
+ }
5755
+ return true
5756
+ })
5757
+
5758
+ if syms == nil {
5759
+ syms = []Sym{}
5760
+ }
5761
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
5558
5762
  if err != nil {
5559
- fmt.Print("[]")
5763
+ fmt.Print(emptyResult())
5560
5764
  return
5561
5765
  }
5562
5766
  fmt.Print(string(data))
@@ -5908,9 +6112,13 @@ var init_generic_parser = __esm({
5908
6112
  ],
5909
6113
  elixir: [
5910
6114
  { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
5911
- { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
6115
+ // Dotted module names must be captured whole: `alias Foo.Bar` resolves
6116
+ // against this symbol, and a `Foo`-only capture never matches it.
6117
+ { re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
5912
6118
  ],
5913
6119
  haskell: [
6120
+ // Target of `import Data.List`.
6121
+ { re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
5914
6122
  { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
5915
6123
  { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
5916
6124
  { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
@@ -6001,9 +6209,9 @@ __export(py_parser_exports, {
6001
6209
  parseSymbols: () => parseSymbols4
6002
6210
  });
6003
6211
  import { spawn as spawn6 } from "node:child_process";
6004
- import * as fs15 from "node:fs/promises";
6212
+ import * as fs16 from "node:fs/promises";
6005
6213
  import * as os7 from "node:os";
6006
- import * as path20 from "node:path";
6214
+ import * as path21 from "node:path";
6007
6215
  async function parseSymbols4(opts) {
6008
6216
  const { file, content, lang } = opts;
6009
6217
  try {
@@ -6081,10 +6289,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6081
6289
  async function syncPyParse(filePath, content, lang) {
6082
6290
  try {
6083
6291
  if (!_cachedScriptPath) {
6084
- const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6085
- await fs15.mkdir(tmpDir, { recursive: true });
6086
- _cachedScriptPath = path20.join(tmpDir, "parse.py");
6087
- await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6292
+ const tmpDir = path21.join(os7.tmpdir(), "ws-py-parse");
6293
+ await fs16.mkdir(tmpDir, { recursive: true });
6294
+ _cachedScriptPath = path21.join(tmpDir, "parse.py");
6295
+ await fs16.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6088
6296
  }
6089
6297
  cachedPyBinary ??= resolvePython();
6090
6298
  const pyBinary = await cachedPyBinary;
@@ -6098,7 +6306,7 @@ async function syncPyParse(filePath, content, lang) {
6098
6306
  if (code !== 0 || !stdout.trim()) {
6099
6307
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
6100
6308
  }
6101
- const raw = JSON.parse(stdout.trim());
6309
+ const { symbols: raw, refs } = parseParserOutput(stdout, lang);
6102
6310
  const symbols = raw.map((s) => ({
6103
6311
  id: 0,
6104
6312
  lang,
@@ -6112,7 +6320,7 @@ async function syncPyParse(filePath, content, lang) {
6112
6320
  scope: s.scope ?? "",
6113
6321
  text: `${s.name} ${s.signature ?? ""}`.trim()
6114
6322
  }));
6115
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
6323
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
6116
6324
  } catch {
6117
6325
  return null;
6118
6326
  }
@@ -6123,6 +6331,7 @@ var init_py_parser = __esm({
6123
6331
  "use strict";
6124
6332
  init_win32_resolve();
6125
6333
  init_generic_parser();
6334
+ init_parser_output();
6126
6335
  init_spawn_gate();
6127
6336
  init_languages2();
6128
6337
  PY_PARSE_SCRIPT = `import ast, json, sys, os
@@ -6184,7 +6393,18 @@ class Sym:
6184
6393
  def is_private(name):
6185
6394
  return name.startswith("__") and not name.endswith("__")
6186
6395
 
6396
+ def leaf_name(node):
6397
+ # Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
6398
+ # TypeScript and Go extractors record call refs, so resolution behaves the
6399
+ # same across languages.
6400
+ if isinstance(node, ast.Attribute):
6401
+ return node.attr
6402
+ if isinstance(node, ast.Name):
6403
+ return node.id
6404
+ return get_name(node).split(".")[-1]
6405
+
6187
6406
  syms = []
6407
+ refs = []
6188
6408
  errors = []
6189
6409
 
6190
6410
  try:
@@ -6192,7 +6412,7 @@ try:
6192
6412
  tree = ast.parse(source, filename=sys.argv[1])
6193
6413
  except Exception as e:
6194
6414
  errors.append(str(e))
6195
- print("[]")
6415
+ print(json.dumps({"symbols": [], "refs": []}))
6196
6416
  sys.exit(0)
6197
6417
 
6198
6418
  # Module-level scope
@@ -6326,7 +6546,42 @@ class ModuleVisitor(ast.NodeVisitor):
6326
6546
  visitor = ModuleVisitor()
6327
6547
  visitor.visit(tree)
6328
6548
 
6329
- print(json.dumps([s.to_dict() for s in syms]))
6549
+ # Refs need a separate full walk: ModuleVisitor deliberately does not descend
6550
+ # into function bodies (it would index locals as symbols), but that is exactly
6551
+ # where the calls are.
6552
+ for node in ast.walk(tree):
6553
+ if isinstance(node, ast.Call):
6554
+ name = leaf_name(node.func)
6555
+ if name:
6556
+ refs.append({"toName": name, "callType": "call", "line": node.lineno})
6557
+ elif isinstance(node, ast.Import):
6558
+ for alias in node.names:
6559
+ refs.append({
6560
+ "toName": alias.name.split(".")[-1],
6561
+ "callType": "import",
6562
+ "line": node.lineno,
6563
+ "module": alias.name,
6564
+ })
6565
+ elif isinstance(node, ast.ImportFrom):
6566
+ # PEP 328: node.level is the number of leading dots. Preserving them is
6567
+ # what lets the resolver walk up from the importing file's package \u2014
6568
+ # dropping them made \`from .foo import X\` indistinguishable from an
6569
+ # absolute \`foo\`.
6570
+ module = ("." * (node.level or 0)) + (node.module or "")
6571
+ for alias in node.names:
6572
+ refs.append({
6573
+ "toName": alias.name,
6574
+ "callType": "import",
6575
+ "line": node.lineno,
6576
+ "module": module,
6577
+ })
6578
+ elif isinstance(node, ast.ClassDef):
6579
+ for base in node.bases:
6580
+ name = leaf_name(base)
6581
+ if name:
6582
+ refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
6583
+
6584
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
6330
6585
  `;
6331
6586
  _cachedScriptPath = null;
6332
6587
  }
@@ -6339,107 +6594,10 @@ __export(rs_parser_exports, {
6339
6594
  parseSymbols: () => parseSymbols5
6340
6595
  });
6341
6596
  import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6342
- import { execFile, spawn as spawn7 } from "node:child_process";
6343
- import * as fs16 from "node:fs/promises";
6344
- import * as path21 from "node:path";
6345
6597
  async function parseSymbols5(opts) {
6346
6598
  const { file, content, lang } = opts;
6347
- const nativeAvailable = await checkNativeParser();
6348
- if (nativeAvailable) {
6349
- const result = await withSpawnGate(() => tryNativeParse(file, content));
6350
- if (result) return result;
6351
- }
6352
6599
  return regexParse({ file, content, lang });
6353
6600
  }
6354
- function probe(command, args) {
6355
- return new Promise((resolve16, reject) => {
6356
- execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
6357
- if (error) reject(error);
6358
- else resolve16();
6359
- });
6360
- });
6361
- }
6362
- function checkNativeParser() {
6363
- nativeParserAvailability ??= (async () => {
6364
- try {
6365
- await probe("rustc", ["--version"]);
6366
- const toolsDir = path21.join(process.cwd(), "tools");
6367
- await probe(
6368
- "cargo",
6369
- [
6370
- "metadata",
6371
- "--no-deps",
6372
- "--format-version",
6373
- "1",
6374
- "--manifest-path",
6375
- path21.join(toolsDir, "Cargo.toml")
6376
- ]
6377
- );
6378
- return true;
6379
- } catch {
6380
- return false;
6381
- }
6382
- })();
6383
- return nativeParserAvailability;
6384
- }
6385
- async function tryNativeParse(file, content) {
6386
- try {
6387
- const toolsDir = path21.join(process.cwd(), "tools");
6388
- const crateDir = path21.join(toolsDir, "syn-parser");
6389
- const tmpFile = path21.join(crateDir, "src", "input.rs");
6390
- await fs16.writeFile(tmpFile, content, "utf8");
6391
- const cargoBinary = resolveWin32Command("cargo");
6392
- const result = await new Promise(
6393
- (resolve16, reject) => {
6394
- let settled = false;
6395
- const proc = spawn7(
6396
- cargoBinary,
6397
- ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6398
- {
6399
- cwd: process.cwd(),
6400
- stdio: ["pipe", "pipe", "pipe"],
6401
- windowsHide: true
6402
- }
6403
- );
6404
- proc.on("error", (err) => {
6405
- if (settled) return;
6406
- settled = true;
6407
- reject(err);
6408
- });
6409
- let stdout2 = "";
6410
- proc.stdout?.on("data", (chunk) => {
6411
- stdout2 += chunk.toString();
6412
- });
6413
- proc.stderr?.resume();
6414
- const timer = setTimeout(() => {
6415
- if (settled) return;
6416
- settled = true;
6417
- proc.kill("SIGKILL");
6418
- reject(new Error("timeout"));
6419
- }, 15e3);
6420
- timer.unref?.();
6421
- proc.on("close", (c) => {
6422
- if (settled) return;
6423
- settled = true;
6424
- clearTimeout(timer);
6425
- resolve16({ code: c, stdout: stdout2 });
6426
- });
6427
- }
6428
- );
6429
- const { code, stdout } = result;
6430
- if (code === 0 && stdout.trim()) {
6431
- const symbols = JSON.parse(stdout.trim());
6432
- return {
6433
- file,
6434
- lang: "rs",
6435
- symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
6436
- mtimeMs: Date.now()
6437
- };
6438
- }
6439
- } catch {
6440
- }
6441
- return null;
6442
- }
6443
6601
  function regexParse(opts) {
6444
6602
  const { file, content, lang } = opts;
6445
6603
  const symbols = [];
@@ -6495,12 +6653,10 @@ function regexParse(opts) {
6495
6653
  });
6496
6654
  return { file, lang, symbols: deduped, mtimeMs: Date.now() };
6497
6655
  }
6498
- var nativeParserAvailability, RS_PATTERNS;
6656
+ var RS_PATTERNS;
6499
6657
  var init_rs_parser = __esm({
6500
6658
  "src/codebase-index/rs-parser.ts"() {
6501
6659
  "use strict";
6502
- init_win32_resolve();
6503
- init_spawn_gate();
6504
6660
  init_languages2();
6505
6661
  RS_PATTERNS = [
6506
6662
  { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
@@ -9954,7 +10110,7 @@ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
9954
10110
 
9955
10111
  // src/codebase-index/project-server-client.ts
9956
10112
  import { spawn as spawn4 } from "node:child_process";
9957
- import * as fs12 from "node:fs";
10113
+ import * as fs13 from "node:fs";
9958
10114
  import * as net3 from "node:net";
9959
10115
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9960
10116
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
@@ -10041,16 +10197,16 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
10041
10197
 
10042
10198
  // src/codebase-index/project-server-endpoint.ts
10043
10199
  import { createHash as createHash4 } from "node:crypto";
10044
- import * as fs11 from "node:fs";
10200
+ import * as fs12 from "node:fs";
10045
10201
  import * as os5 from "node:os";
10046
- import * as path16 from "node:path";
10202
+ import * as path17 from "node:path";
10047
10203
  import { fileURLToPath } from "node:url";
10048
10204
  import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
10049
10205
 
10050
10206
  // src/codebase-index/writer.ts
10051
10207
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
10052
- import * as fs10 from "node:fs";
10053
- import * as path15 from "node:path";
10208
+ import * as fs11 from "node:fs";
10209
+ import * as path16 from "node:path";
10054
10210
 
10055
10211
  // src/codebase-index/bm25.ts
10056
10212
  var K1 = 1.5;
@@ -10141,6 +10297,9 @@ var Bm25Index = class {
10141
10297
  }
10142
10298
  };
10143
10299
 
10300
+ // src/codebase-index/writer.ts
10301
+ init_languages2();
10302
+
10144
10303
  // src/codebase-index/lsp-kind.ts
10145
10304
  function lspKindToInternalKind(k) {
10146
10305
  switch (k) {
@@ -10175,7 +10334,7 @@ function lspKindToInternalKind(k) {
10175
10334
  }
10176
10335
 
10177
10336
  // src/codebase-index/schema.ts
10178
- var SCHEMA_VERSION = 3;
10337
+ var SCHEMA_VERSION = 4;
10179
10338
 
10180
10339
  // src/codebase-index/sqlite-runtime.ts
10181
10340
  import { createRequire } from "node:module";
@@ -10246,7 +10405,7 @@ function runSqliteWithRetry(fn) {
10246
10405
 
10247
10406
  // src/codebase-index/writer-admin.ts
10248
10407
  import * as fs9 from "node:fs";
10249
- import * as path13 from "node:path";
10408
+ import * as path14 from "node:path";
10250
10409
  var DB_FILE = "index.db";
10251
10410
  function getAllIndexableWithStatement(stmt) {
10252
10411
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10305,7 +10464,7 @@ function getAllFileMetasWithStatement(stmt) {
10305
10464
  }
10306
10465
  function getIndexDbSizeBytes(indexDir) {
10307
10466
  try {
10308
- return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10467
+ return fs9.statSync(path14.join(indexDir, DB_FILE)).size;
10309
10468
  } catch {
10310
10469
  return 0;
10311
10470
  }
@@ -10356,49 +10515,345 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
10356
10515
  }
10357
10516
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10358
10517
  if (refs.length === 0) return;
10359
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 5));
10518
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
10360
10519
  for (let i = 0; i < refs.length; i += chunkSize) {
10361
10520
  const chunk = refs.slice(i, i + chunkSize);
10362
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
10521
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
10363
10522
  const insert = stmt(
10364
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
10523
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
10524
+ VALUES ${placeholders}`
10365
10525
  );
10366
10526
  const binds = [];
10367
10527
  for (const ref of chunk) {
10368
- binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
10528
+ binds.push(
10529
+ ref.fromId,
10530
+ ref.toName,
10531
+ ref.toId ?? null,
10532
+ ref.callType,
10533
+ ref.line,
10534
+ ref.lang ?? "",
10535
+ ref.module ?? null,
10536
+ ref.toFile ?? null
10537
+ );
10369
10538
  }
10370
10539
  insert.run(...binds);
10371
10540
  }
10372
10541
  }
10373
10542
 
10374
- // src/codebase-index/writer-graph-helpers.ts
10375
- import * as path14 from "node:path";
10376
- function derivePackage(filePath) {
10377
- const f = filePath.replace(/\\/g, "/");
10378
- const pkgsIdx = f.indexOf("/packages/");
10379
- if (pkgsIdx !== -1) {
10380
- const rest = f.slice(pkgsIdx + "/packages/".length);
10381
- const seg = rest.split("/")[0];
10382
- return seg ? `@wrongstack/${seg}` : void 0;
10383
- }
10384
- const appsIdx = f.indexOf("/apps/");
10543
+ // src/codebase-index/writer-graph-reader.ts
10544
+ init_languages2();
10545
+
10546
+ // src/codebase-index/module-roots.ts
10547
+ init_languages2();
10548
+ import * as fs10 from "node:fs/promises";
10549
+ import * as path15 from "node:path";
10550
+ function toPortablePath(file) {
10551
+ return file.replace(/\\/g, "/");
10552
+ }
10553
+ async function readTextIfPresent(file) {
10554
+ try {
10555
+ return await fs10.readFile(file, "utf8");
10556
+ } catch {
10557
+ return void 0;
10558
+ }
10559
+ }
10560
+ function parsePackageJsonName(source) {
10561
+ try {
10562
+ const parsed = JSON.parse(source);
10563
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
10564
+ } catch {
10565
+ return void 0;
10566
+ }
10567
+ }
10568
+ function parseGoModulePath(source) {
10569
+ for (const rawLine of source.split(/\r?\n/)) {
10570
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
10571
+ const match = /^module\s+(\S+)/.exec(line);
10572
+ if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
10573
+ }
10574
+ return void 0;
10575
+ }
10576
+ function parseTomlTableName(source, tables) {
10577
+ let current = "";
10578
+ for (const rawLine of source.split(/\r?\n/)) {
10579
+ const line = rawLine.replace(/#.*$/, "").trim();
10580
+ if (line.startsWith("[[")) {
10581
+ current = "\0";
10582
+ continue;
10583
+ }
10584
+ const table = /^\[([^\]]+)\]$/.exec(line);
10585
+ if (table?.[1]) {
10586
+ current = table[1].trim();
10587
+ continue;
10588
+ }
10589
+ if (!tables.includes(current)) continue;
10590
+ const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
10591
+ if (match?.[1]) return match[1];
10592
+ }
10593
+ return void 0;
10594
+ }
10595
+ function parsePomArtifactId(source) {
10596
+ const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
10597
+ return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
10598
+ }
10599
+ var LANGS_BY_KIND = {
10600
+ npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
10601
+ cargo: ["rs"],
10602
+ go: ["go"],
10603
+ python: ["py"],
10604
+ maven: ["java", "kotlin", "scala"],
10605
+ gradle: ["java", "kotlin", "scala"],
10606
+ dotnet: ["csharp"]
10607
+ };
10608
+ function ancestorsOf(dir, stopAt) {
10609
+ const out = [];
10610
+ let current = dir;
10611
+ for (; ; ) {
10612
+ out.push(current);
10613
+ if (current === stopAt || current.length <= stopAt.length) break;
10614
+ const parent = path15.posix.dirname(current);
10615
+ if (parent === current) break;
10616
+ current = parent;
10617
+ }
10618
+ return out;
10619
+ }
10620
+ var MARKER_PROBES = [
10621
+ {
10622
+ kind: "npm",
10623
+ file: "package.json",
10624
+ build: (dir, source) => {
10625
+ const name = parsePackageJsonName(source) ?? path15.posix.basename(dir);
10626
+ return { name, importPath: name, sourceRoots: [dir] };
10627
+ }
10628
+ },
10629
+ {
10630
+ kind: "cargo",
10631
+ file: "Cargo.toml",
10632
+ build: (dir, source) => {
10633
+ const name = parseTomlTableName(source, ["package"]);
10634
+ if (!name) return void 0;
10635
+ return {
10636
+ name: `crate:${name}`,
10637
+ // Rust paths use underscores where crate names often use dashes.
10638
+ importPath: name.replace(/-/g, "_"),
10639
+ sourceRoots: [path15.posix.join(dir, "src")]
10640
+ };
10641
+ }
10642
+ },
10643
+ {
10644
+ kind: "go",
10645
+ file: "go.mod",
10646
+ build: (dir, source) => {
10647
+ const modulePath = parseGoModulePath(source);
10648
+ if (!modulePath) return void 0;
10649
+ return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
10650
+ }
10651
+ },
10652
+ {
10653
+ kind: "python",
10654
+ file: "pyproject.toml",
10655
+ build: (dir, source) => {
10656
+ const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path15.posix.basename(dir);
10657
+ return {
10658
+ name: `py:${name}`,
10659
+ importPath: void 0,
10660
+ // `src/` layout is the packaging-guide default; the root itself covers
10661
+ // the flat layout. Both are probed, missing ones simply never match.
10662
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
10663
+ };
10664
+ }
10665
+ },
10666
+ {
10667
+ kind: "python",
10668
+ file: "setup.py",
10669
+ build: (dir) => ({
10670
+ name: `py:${path15.posix.basename(dir)}`,
10671
+ importPath: void 0,
10672
+ sourceRoots: [path15.posix.join(dir, "src"), dir]
10673
+ })
10674
+ },
10675
+ {
10676
+ kind: "maven",
10677
+ file: "pom.xml",
10678
+ build: (dir, source) => {
10679
+ const artifactId = parsePomArtifactId(source) ?? path15.posix.basename(dir);
10680
+ return {
10681
+ name: `mvn:${artifactId}`,
10682
+ importPath: void 0,
10683
+ sourceRoots: [
10684
+ path15.posix.join(dir, "src/main/java"),
10685
+ path15.posix.join(dir, "src/main/kotlin"),
10686
+ path15.posix.join(dir, "src/main/scala"),
10687
+ path15.posix.join(dir, "src/test/java")
10688
+ ]
10689
+ };
10690
+ }
10691
+ },
10692
+ {
10693
+ kind: "gradle",
10694
+ file: "build.gradle",
10695
+ build: (dir) => buildGradleRoot(dir)
10696
+ },
10697
+ {
10698
+ kind: "gradle",
10699
+ file: "build.gradle.kts",
10700
+ build: (dir) => buildGradleRoot(dir)
10701
+ }
10702
+ ];
10703
+ function buildGradleRoot(dir) {
10704
+ return {
10705
+ name: `gradle:${path15.posix.basename(dir)}`,
10706
+ importPath: void 0,
10707
+ sourceRoots: [
10708
+ path15.posix.join(dir, "src/main/java"),
10709
+ path15.posix.join(dir, "src/main/kotlin"),
10710
+ path15.posix.join(dir, "src/main/scala")
10711
+ ]
10712
+ };
10713
+ }
10714
+ async function probeDotnetRoot(dir) {
10715
+ let entries;
10716
+ try {
10717
+ entries = await fs10.readdir(dir);
10718
+ } catch {
10719
+ return void 0;
10720
+ }
10721
+ const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
10722
+ if (!project) return void 0;
10723
+ const name = project.slice(0, -".csproj".length);
10724
+ return {
10725
+ dir,
10726
+ kind: "dotnet",
10727
+ name: `csproj:${name}`,
10728
+ importPath: void 0,
10729
+ sourceRoots: [dir]
10730
+ };
10731
+ }
10732
+ async function detectModuleRoots(projectRoot, files) {
10733
+ const root = toPortablePath(projectRoot).replace(/\/+$/, "");
10734
+ const langsByDir = /* @__PURE__ */ new Map();
10735
+ for (const file of files) {
10736
+ const portable = toPortablePath(file);
10737
+ const lang = detectLang(portable);
10738
+ if (!lang) continue;
10739
+ const dir = path15.posix.dirname(portable);
10740
+ let langs = langsByDir.get(dir);
10741
+ if (!langs) {
10742
+ langs = /* @__PURE__ */ new Set();
10743
+ langsByDir.set(dir, langs);
10744
+ }
10745
+ langs.add(lang);
10746
+ }
10747
+ const candidates = /* @__PURE__ */ new Map();
10748
+ for (const [dir, langs] of langsByDir) {
10749
+ for (const ancestor of ancestorsOf(dir, root)) {
10750
+ let merged = candidates.get(ancestor);
10751
+ if (!merged) {
10752
+ merged = /* @__PURE__ */ new Set();
10753
+ candidates.set(ancestor, merged);
10754
+ }
10755
+ for (const lang of langs) merged.add(lang);
10756
+ }
10757
+ }
10758
+ const roots = [];
10759
+ await Promise.all(
10760
+ [...candidates].map(async ([dir, langs]) => {
10761
+ for (const probe of MARKER_PROBES) {
10762
+ if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
10763
+ const source = await readTextIfPresent(path15.posix.join(dir, probe.file));
10764
+ if (source === void 0) continue;
10765
+ const built = probe.build(dir, source);
10766
+ if (built) roots.push({ dir, kind: probe.kind, ...built });
10767
+ }
10768
+ if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
10769
+ const dotnet = await probeDotnetRoot(dir);
10770
+ if (dotnet) roots.push(dotnet);
10771
+ }
10772
+ })
10773
+ );
10774
+ roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
10775
+ return { projectRoot: root, roots };
10776
+ }
10777
+ function findOwningRoot(structure, file, kinds) {
10778
+ const portable = toPortablePath(file);
10779
+ for (const root of structure.roots) {
10780
+ if (kinds && !kinds.includes(root.kind)) continue;
10781
+ if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
10782
+ }
10783
+ return void 0;
10784
+ }
10785
+ function derivePackageFromLayout(filePath) {
10786
+ const portable = toPortablePath(filePath);
10787
+ const packagesIdx = portable.indexOf("/packages/");
10788
+ if (packagesIdx !== -1) {
10789
+ const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
10790
+ if (segment) return `@wrongstack/${segment}`;
10791
+ }
10792
+ const appsIdx = portable.indexOf("/apps/");
10385
10793
  if (appsIdx !== -1) {
10386
- const rest = f.slice(appsIdx + "/apps/".length);
10387
- const seg = rest.split("/")[0];
10388
- return seg ? `app:${seg}` : void 0;
10794
+ const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
10795
+ if (segment) return `app:${segment}`;
10389
10796
  }
10390
10797
  return void 0;
10391
10798
  }
10392
- function packageFromImport(moduleName) {
10393
- if (!moduleName.startsWith("@wrongstack/")) return void 0;
10394
- const parts = moduleName.split("/");
10395
- return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
10799
+ function pythonPackageLabel(structure, file, initDirs) {
10800
+ const portable = toPortablePath(file);
10801
+ const dir = path15.posix.dirname(portable);
10802
+ if (!initDirs.has(dir)) return void 0;
10803
+ const segments = [];
10804
+ let current = dir;
10805
+ while (initDirs.has(current) && current.length > structure.projectRoot.length) {
10806
+ segments.unshift(path15.posix.basename(current));
10807
+ current = path15.posix.dirname(current);
10808
+ }
10809
+ return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
10396
10810
  }
10397
- function buildPackageGraphNodes(fileCounts, files) {
10811
+ function assignPackageLabels(structure, files) {
10812
+ const initDirs = /* @__PURE__ */ new Set();
10813
+ for (const file of files) {
10814
+ const portable = toPortablePath(file);
10815
+ if (path15.posix.basename(portable) === "__init__.py") {
10816
+ initDirs.add(path15.posix.dirname(portable));
10817
+ }
10818
+ }
10819
+ const labels = /* @__PURE__ */ new Map();
10820
+ for (const file of files) {
10821
+ const portable = toPortablePath(file);
10822
+ const lang = detectLang(portable);
10823
+ if (lang === "go") {
10824
+ const owner3 = findOwningRoot(structure, portable, ["go"]);
10825
+ const dir = path15.posix.dirname(portable);
10826
+ if (owner3?.importPath) {
10827
+ const relative12 = path15.posix.relative(owner3.dir, dir);
10828
+ labels.set(file, relative12 ? `${owner3.importPath}/${relative12}` : owner3.importPath);
10829
+ } else {
10830
+ labels.set(file, `go:${path15.posix.relative(structure.projectRoot, dir) || "."}`);
10831
+ }
10832
+ continue;
10833
+ }
10834
+ if (lang === "py") {
10835
+ const dotted = pythonPackageLabel(structure, portable, initDirs);
10836
+ if (dotted) {
10837
+ labels.set(file, dotted);
10838
+ continue;
10839
+ }
10840
+ }
10841
+ const owner2 = findOwningRoot(structure, portable);
10842
+ const label = owner2?.name ?? derivePackageFromLayout(portable) ?? "(root)";
10843
+ labels.set(file, label);
10844
+ }
10845
+ return labels;
10846
+ }
10847
+
10848
+ // src/codebase-index/writer-graph-helpers.ts
10849
+ function createPackageLabeller(stored) {
10850
+ return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
10851
+ }
10852
+ function buildPackageGraphNodes(fileCounts, files, packageOf) {
10398
10853
  const pkgNodes = /* @__PURE__ */ new Map();
10399
10854
  const fileToPkg = /* @__PURE__ */ new Map();
10400
10855
  for (const { file, n } of fileCounts) {
10401
- const pkg = derivePackage(file) ?? "(root)";
10856
+ const pkg = packageOf(file);
10402
10857
  fileToPkg.set(file, pkg);
10403
10858
  const node = pkgNodes.get(pkg);
10404
10859
  if (node) {
@@ -10415,7 +10870,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10415
10870
  }
10416
10871
  }
10417
10872
  for (const { file } of files) {
10418
- const pkg = derivePackage(file) ?? "(root)";
10873
+ const pkg = packageOf(file);
10419
10874
  fileToPkg.set(file, pkg);
10420
10875
  const node = pkgNodes.get(pkg);
10421
10876
  if (node) {
@@ -10433,7 +10888,7 @@ function buildPackageGraphNodes(fileCounts, files) {
10433
10888
  }
10434
10889
  return { pkgNodes, fileToPkg };
10435
10890
  }
10436
- function buildFileGraphNodeState(pkgSyms, localFiles) {
10891
+ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
10437
10892
  const fileNodes = /* @__PURE__ */ new Map();
10438
10893
  const symToFile = /* @__PURE__ */ new Map();
10439
10894
  const fileStats = /* @__PURE__ */ new Map();
@@ -10452,7 +10907,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10452
10907
  id: `file:${file}`,
10453
10908
  label: file.replace(/\\/g, "/").split("/").pop() ?? file,
10454
10909
  kind: "file",
10455
- package: derivePackage(file) ?? "(root)",
10910
+ package: packageOf(file),
10456
10911
  file,
10457
10912
  symbolCount: stats?.count ?? 0,
10458
10913
  lang: stats?.lang,
@@ -10464,7 +10919,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
10464
10919
  }
10465
10920
  return { fileNodes, symToFile, fileStats, ensureFileNode };
10466
10921
  }
10467
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10922
+ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
10468
10923
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
10469
10924
  const aExternal = a.file === fileFilter ? 0 : 1;
10470
10925
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -10476,7 +10931,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10476
10931
  symbolId: s.id,
10477
10932
  symbolKind: s.kind,
10478
10933
  file: s.file,
10479
- package: derivePackage(s.file) ?? "(root)",
10934
+ package: packageOf(s.file),
10480
10935
  lang: s.lang,
10481
10936
  line: s.line,
10482
10937
  signature: s.signature,
@@ -10484,29 +10939,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10484
10939
  external: s.file !== fileFilter
10485
10940
  }));
10486
10941
  }
10487
- function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10488
- if (!moduleName.startsWith(".")) return void 0;
10489
- const normalizedFrom = fromFile.replace(/\\/g, "/");
10490
- const absolute = path14.posix.normalize(
10491
- path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10492
- );
10493
- const extension = path14.posix.extname(absolute);
10494
- const base = extension ? absolute.slice(0, -extension.length) : absolute;
10495
- const candidates = [
10496
- absolute,
10497
- ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10498
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10499
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10500
- ];
10501
- const indexedByPortablePath = new Map(
10502
- [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
10503
- );
10504
- for (const candidate of candidates) {
10505
- const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
10506
- if (indexed) return indexed;
10507
- }
10508
- return void 0;
10509
- }
10510
10942
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
10511
10943
  const key = `${source}\0${target}`;
10512
10944
  let edge = edgeMap.get(key);
@@ -10547,7 +10979,12 @@ function mapWriterRefRow(row) {
10547
10979
  toName: row.to_name,
10548
10980
  toId: row.to_id ?? void 0,
10549
10981
  callType: row.call_type,
10550
- line: row.line
10982
+ line: row.line,
10983
+ // `lang`/`module`/`to_file` are absent from the narrower column lists some
10984
+ // queries select; `undefined` keeps those rows valid Refs.
10985
+ lang: row.lang || void 0,
10986
+ module: row.module ?? void 0,
10987
+ toFile: row.to_file ?? void 0
10551
10988
  };
10552
10989
  }
10553
10990
 
@@ -10695,7 +11132,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
10695
11132
  function getPackageGraphWithStatement(stmt) {
10696
11133
  const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
10697
11134
  const files = stmt("SELECT DISTINCT file FROM files").all();
10698
- const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files);
11135
+ const packageOf = readPackageLabeller(stmt);
11136
+ const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
10699
11137
  const refRows = stmt(
10700
11138
  `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
10701
11139
  FROM refs r
@@ -10706,32 +11144,42 @@ function getPackageGraphWithStatement(stmt) {
10706
11144
  ).all();
10707
11145
  const edgeMap = /* @__PURE__ */ new Map();
10708
11146
  for (const r of refRows) {
10709
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
10710
- const toPkg = fileToPkg.get(r.to_file) ?? derivePackage(r.to_file) ?? "(root)";
11147
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11148
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
10711
11149
  if (fromPkg === toPkg) continue;
10712
11150
  const n = Number(r.n) || 0;
10713
11151
  addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
10714
11152
  }
10715
11153
  const importRows = stmt(
10716
- `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
11154
+ `SELECT s.file AS from_file,
11155
+ COALESCE(r.to_file, st.file) AS to_file,
11156
+ COUNT(*) AS n
10717
11157
  FROM refs r
10718
11158
  JOIN symbols s ON s.id = r.from_id
11159
+ LEFT JOIN symbols st ON st.id = r.to_id
10719
11160
  WHERE r.call_type = 'import'
10720
- GROUP BY r.to_name, s.file`
11161
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
11162
+ GROUP BY s.file, COALESCE(r.to_file, st.file)`
10721
11163
  ).all();
10722
11164
  for (const r of importRows) {
10723
- const fromPkg = fileToPkg.get(r.from_file) ?? derivePackage(r.from_file) ?? "(root)";
10724
- const toPkg = packageFromImport(r.to_name);
10725
- if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
11165
+ const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
11166
+ const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
11167
+ if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
10726
11168
  const n = Number(r.n) || 0;
10727
11169
  addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
10728
11170
  }
10729
11171
  const edges = materializeWeightedEdges(edgeMap, "pkg");
10730
11172
  return { nodes: [...pkgNodes.values()], edges };
10731
11173
  }
11174
+ function readPackageLabeller(stmt) {
11175
+ const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
11176
+ return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
11177
+ }
10732
11178
  function getFileGraphWithStatement(stmt, packageFilter) {
10733
11179
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
10734
- const pkgFilePaths = allFiles.filter((f) => (derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
11180
+ const packageOf = readPackageLabeller(stmt);
11181
+ const langOf = (file) => detectLang(file) ?? "other";
11182
+ const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
10735
11183
  const localFiles = new Set(pkgFilePaths);
10736
11184
  if (localFiles.size === 0) return { nodes: [], edges: [] };
10737
11185
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -10740,9 +11188,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10740
11188
  ).all(...pkgFilePaths);
10741
11189
  const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
10742
11190
  pkgSyms,
10743
- localFiles
11191
+ localFiles,
11192
+ packageOf
10744
11193
  );
10745
- const indexedFiles = new Set(allFiles.map((f) => f.file));
10746
11194
  const refRows = stmt(
10747
11195
  `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
10748
11196
  FROM refs r
@@ -10765,7 +11213,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10765
11213
  for (const x of extras) {
10766
11214
  symToFile.set(x.id, x.file);
10767
11215
  if (!fileStats.has(x.file)) {
10768
- fileStats.set(x.file, { count: 0, lang: "ts" });
11216
+ fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
10769
11217
  }
10770
11218
  }
10771
11219
  }
@@ -10782,17 +11230,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
10782
11230
  addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
10783
11231
  }
10784
11232
  const importRows = stmt(
10785
- `SELECT r.from_id, r.to_name, COUNT(*) AS n
11233
+ `SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
10786
11234
  FROM refs r
11235
+ LEFT JOIN symbols st ON st.id = r.to_id
10787
11236
  WHERE r.call_type = 'import'
11237
+ AND COALESCE(r.to_file, st.file) IS NOT NULL
10788
11238
  AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
10789
- GROUP BY r.from_id, r.to_name`
11239
+ GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
10790
11240
  ).all(...pkgFilePaths);
10791
11241
  for (const r of importRows) {
10792
11242
  const fromFile = symToFile.get(r.from_id);
10793
11243
  if (!fromFile || !localFiles.has(fromFile)) continue;
10794
- const toFile = resolveRelativeImport(fromFile, r.to_name, indexedFiles);
11244
+ const toFile = r.to_file;
10795
11245
  if (!toFile || fromFile === toFile) continue;
11246
+ if (!fileStats.has(toFile)) {
11247
+ fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
11248
+ }
10796
11249
  ensureFileNode(fromFile);
10797
11250
  ensureFileNode(toFile);
10798
11251
  const n = Number(r.n) || 0;
@@ -10842,7 +11295,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
10842
11295
  ).all(...missingIds);
10843
11296
  for (const s of extras) symById.set(s.id, s);
10844
11297
  }
10845
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
11298
+ const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
10846
11299
  return { nodes, edges };
10847
11300
  }
10848
11301
 
@@ -10864,7 +11317,7 @@ function assignRefsToSymbols(refs, symbols) {
10864
11317
  }
10865
11318
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10866
11319
  if (!owner2 || owner2.id <= 0) continue;
10867
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
11320
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
10868
11321
  if (seen.has(key)) continue;
10869
11322
  seen.add(key);
10870
11323
  assigned.push({ ...ref, fromId: owner2.id });
@@ -10910,7 +11363,11 @@ var CORE_TABLES_SQL = `
10910
11363
  lang TEXT NOT NULL,
10911
11364
  mtime_ms INTEGER NOT NULL,
10912
11365
  symbol_count INTEGER NOT NULL DEFAULT 0,
10913
- last_indexed INTEGER NOT NULL
11366
+ last_indexed INTEGER NOT NULL,
11367
+ -- Code Atlas grouping label, computed at index time from the ecosystem's
11368
+ -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
11369
+ -- re-derived per query because the evidence lives on disk, not in the DB.
11370
+ package TEXT NOT NULL DEFAULT ''
10914
11371
  );
10915
11372
  CREATE TABLE IF NOT EXISTS symbols (
10916
11373
  id INTEGER PRIMARY KEY,
@@ -10927,6 +11384,9 @@ var CORE_TABLES_SQL = `
10927
11384
  file_fk TEXT NOT NULL
10928
11385
  );
10929
11386
  `;
11387
+ var FILE_INDEX_SQL = [
11388
+ "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
11389
+ ];
10930
11390
  var SYMBOL_INDEX_SQL = [
10931
11391
  "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10932
11392
  "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
@@ -10943,15 +11403,32 @@ var REFS_TABLE_SQL = `
10943
11403
  to_name TEXT NOT NULL,
10944
11404
  to_id INTEGER,
10945
11405
  call_type TEXT NOT NULL,
10946
- line INTEGER NOT NULL
11406
+ line INTEGER NOT NULL,
11407
+ lang TEXT NOT NULL DEFAULT '',
11408
+ module TEXT,
11409
+ to_file TEXT
10947
11410
  );
10948
11411
  `;
10949
11412
  var REFS_INDEX_SQL = [
10950
11413
  "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10951
11414
  "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10952
11415
  "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10953
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
11416
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
11417
+ // Name resolution matches (to_name, lang) pairs; the composite keeps the
11418
+ // language-scoped UPDATE from degrading into a scan of every same-named row.
11419
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
11420
+ // The post-index module resolution pass groups unresolved import refs by
11421
+ // (module, lang); graph readers then read to_file back.
11422
+ "CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
11423
+ "CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
10954
11424
  ];
11425
+ var LANG_FAMILY_TABLE_SQL = `
11426
+ CREATE TABLE IF NOT EXISTS lang_family (
11427
+ lang TEXT PRIMARY KEY,
11428
+ family TEXT NOT NULL
11429
+ );
11430
+ `;
11431
+ var LANG_FAMILY_WILDCARD = "*";
10955
11432
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10956
11433
 
10957
11434
  // src/codebase-index/writer-search-helpers.ts
@@ -11163,15 +11640,69 @@ var IndexStore = class _IndexStore {
11163
11640
  }
11164
11641
  constructor(projectRoot, opts = {}) {
11165
11642
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
11166
- fs10.mkdirSync(this.indexDir, { recursive: true });
11643
+ fs11.mkdirSync(this.indexDir, { recursive: true });
11167
11644
  const Database = loadDatabaseSync();
11168
- this.db = new Database(path15.join(this.indexDir, DB_FILE2));
11645
+ this.db = new Database(path16.join(this.indexDir, DB_FILE2));
11169
11646
  applyIndexStorePragmas(this.db);
11170
11647
  this.initSchema();
11171
11648
  }
11172
11649
  runWithRetry(fn) {
11173
11650
  return runSqliteWithRetry(fn);
11174
11651
  }
11652
+ /**
11653
+ * Mirror the in-process language→family map into SQLite.
11654
+ *
11655
+ * Rewritten on every open rather than only on schema bumps: the mapping is
11656
+ * static lookup data, so a code-side change (a new language, a language
11657
+ * moving families) must take effect without forcing a full reindex.
11658
+ */
11659
+ seedLangFamilies() {
11660
+ const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
11661
+ for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
11662
+ insert.run("", LANG_FAMILY_WILDCARD);
11663
+ }
11664
+ /**
11665
+ * Add any column the current schema expects but the on-disk table lacks.
11666
+ *
11667
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
11668
+ * and the version check above only rebuilds on a version *mismatch*. That
11669
+ * leaves a real gap: several wstack processes share this database, and while
11670
+ * a version upgrade is rolling out one of them may still be running the
11671
+ * previous build. That older process sees the newer version number, drops the
11672
+ * tables, and recreates them from *its* DDL — without the newer columns —
11673
+ * while the metadata row still reads the new version. Every later query for
11674
+ * one of those columns then fails with `no such column`, and no amount of
11675
+ * reindexing fixes it, because the version numbers already agree.
11676
+ *
11677
+ * Repairing column-by-column makes the schema self-healing from any of those
11678
+ * states. Table and column names are compile-time literals from this module,
11679
+ * never user input.
11680
+ */
11681
+ repairMissingColumns() {
11682
+ const expected = [
11683
+ { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
11684
+ {
11685
+ table: "refs",
11686
+ columns: [
11687
+ ["lang", "TEXT NOT NULL DEFAULT ''"],
11688
+ ["module", "TEXT"],
11689
+ ["to_file", "TEXT"]
11690
+ ]
11691
+ }
11692
+ ];
11693
+ for (const { table, columns } of expected) {
11694
+ const present = new Set(
11695
+ this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
11696
+ (row) => typeof row.name === "string" ? [row.name] : []
11697
+ )
11698
+ );
11699
+ if (present.size === 0) continue;
11700
+ for (const [name, type] of columns) {
11701
+ if (present.has(name)) continue;
11702
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
11703
+ }
11704
+ }
11705
+ }
11175
11706
  initSchema() {
11176
11707
  this.db.exec(METADATA_TABLE_SQL);
11177
11708
  const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
@@ -11194,9 +11725,13 @@ var IndexStore = class _IndexStore {
11194
11725
  );
11195
11726
  }
11196
11727
  this.db.exec(CORE_TABLES_SQL);
11197
- for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11198
11728
  this.db.exec(REFS_TABLE_SQL);
11729
+ this.repairMissingColumns();
11730
+ for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
11731
+ for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
11199
11732
  for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
11733
+ this.db.exec(LANG_FAMILY_TABLE_SQL);
11734
+ this.seedLangFamilies();
11200
11735
  try {
11201
11736
  this.db.exec(SYMBOLS_FTS_SQL);
11202
11737
  this.ftsAvailable = true;
@@ -11231,6 +11766,18 @@ var IndexStore = class _IndexStore {
11231
11766
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
11232
11767
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
11233
11768
  static MAX_SQL_VARS = 900;
11769
+ /**
11770
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
11771
+ * `sym` belong to the same language family — or the ref carries no language,
11772
+ * in which case the wildcard bind matches everything.
11773
+ *
11774
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
11775
+ */
11776
+ static FAMILY_MATCH_SQL = `(
11777
+ (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
11778
+ OR (SELECT family FROM lang_family WHERE lang = sym.lang)
11779
+ = (SELECT family FROM lang_family WHERE lang = refs.lang)
11780
+ )`;
11234
11781
  /**
11235
11782
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
11236
11783
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -11294,9 +11841,12 @@ var IndexStore = class _IndexStore {
11294
11841
  const placeholders = chunk.map(() => "?").join(",");
11295
11842
  const result = this.stmt(
11296
11843
  `UPDATE refs
11297
- SET to_id = (SELECT MIN(id) FROM symbols WHERE name = refs.to_name)
11844
+ SET to_id = (
11845
+ SELECT MIN(sym.id) FROM symbols sym
11846
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
11847
+ )
11298
11848
  WHERE to_name IN (${placeholders})`
11299
- ).run(...chunk);
11849
+ ).run(LANG_FAMILY_WILDCARD, ...chunk);
11300
11850
  changes += result.changes ?? 0;
11301
11851
  }
11302
11852
  return changes;
@@ -11423,6 +11973,115 @@ var IndexStore = class _IndexStore {
11423
11973
  getAllFileMetas() {
11424
11974
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
11425
11975
  }
11976
+ // ─── Project structure & module resolution ──────────────────────────────────
11977
+ /** Store the Code Atlas grouping label for each indexed file. */
11978
+ setFilePackages(entries) {
11979
+ if (entries.size === 0) return;
11980
+ this.runWithRetry(() => {
11981
+ const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
11982
+ for (const [file, label] of entries) update.run(label, file);
11983
+ });
11984
+ }
11985
+ /**
11986
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
11987
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
11988
+ * Ordered so the resolver's choice among duplicate declarations is stable.
11989
+ */
11990
+ getNamespaceDeclarations() {
11991
+ return this.stmt(
11992
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
11993
+ ).all();
11994
+ }
11995
+ /** `file → package` for every indexed file that has a label. */
11996
+ getFilePackages() {
11997
+ const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
11998
+ return new Map(rows.map((row) => [row.file, row.package]));
11999
+ }
12000
+ /**
12001
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
12002
+ *
12003
+ * Distinct rather than per-ref because resolution depends only on these three
12004
+ * values: a file importing the same module twenty times resolves it once.
12005
+ */
12006
+ getUnresolvedImports(onlyFiles) {
12007
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
12008
+ FROM refs r
12009
+ JOIN symbols s ON s.id = r.from_id
12010
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
12011
+ if (!onlyFiles?.length) {
12012
+ return this.stmt(base).all();
12013
+ }
12014
+ const out = [];
12015
+ for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
12016
+ const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
12017
+ const placeholders = chunk.map(() => "?").join(",");
12018
+ out.push(
12019
+ ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
12020
+ );
12021
+ }
12022
+ return out;
12023
+ }
12024
+ /**
12025
+ * Write resolved import targets back onto `refs.to_file`.
12026
+ *
12027
+ * Applied through a temp table and a single UPDATE: one statement per
12028
+ * resolution would mean thousands of round-trips on a first index.
12029
+ */
12030
+ applyImportResolutions(resolutions) {
12031
+ if (resolutions.length === 0) return 0;
12032
+ return this.runWithRetry(() => {
12033
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12034
+ this.db.exec(
12035
+ `CREATE TEMP TABLE import_resolution (
12036
+ from_file TEXT NOT NULL,
12037
+ lang TEXT NOT NULL,
12038
+ module TEXT NOT NULL,
12039
+ to_file TEXT NOT NULL
12040
+ )`
12041
+ );
12042
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
12043
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
12044
+ const chunk = resolutions.slice(i, i + chunkSize);
12045
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
12046
+ const binds = [];
12047
+ for (const entry of chunk) {
12048
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
12049
+ }
12050
+ this.stmt(
12051
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
12052
+ VALUES ${placeholders}`
12053
+ ).run(...binds);
12054
+ }
12055
+ this.db.exec(
12056
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
12057
+ ON import_resolution(module, lang, from_file)`
12058
+ );
12059
+ const result = this.stmt(
12060
+ `UPDATE refs
12061
+ SET to_file = (
12062
+ SELECT ir.to_file
12063
+ FROM temp.import_resolution ir
12064
+ JOIN symbols s ON s.id = refs.from_id
12065
+ WHERE ir.module = refs.module
12066
+ AND ir.lang = refs.lang
12067
+ AND ir.from_file = s.file
12068
+ LIMIT 1
12069
+ )
12070
+ WHERE refs.call_type = 'import'
12071
+ AND refs.module IS NOT NULL
12072
+ AND EXISTS (
12073
+ SELECT 1
12074
+ FROM temp.import_resolution ir
12075
+ JOIN symbols s ON s.id = refs.from_id
12076
+ WHERE ir.module = refs.module
12077
+ AND ir.lang = refs.lang
12078
+ AND ir.from_file = s.file
12079
+ )`
12080
+ ).run();
12081
+ this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
12082
+ return result.changes ?? 0;
12083
+ });
12084
+ }
11426
12085
  // ─── Search ──────────────────────────────────────────────────────────────────
11427
12086
  search(query, filter, opts) {
11428
12087
  const built = this.buildSearchWhere(query, filter);
@@ -11809,9 +12468,12 @@ var IndexStore = class _IndexStore {
11809
12468
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
11810
12469
  * Call this after all symbols have been inserted to fill in cross-references.
11811
12470
  *
11812
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
11813
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
11814
- * that found a targetmatching the previous per-row loop's return value.
12471
+ * A match additionally requires the referencing ref and the target symbol to
12472
+ * be in the same {@link LangFamily}. Without that guard a name match is a
12473
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
12474
+ * `Config` are declared in most languages at once, and each collision draws a
12475
+ * Code Atlas edge between files that never reference each other. Refs stored
12476
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
11815
12477
  */
11816
12478
  resolveRefs() {
11817
12479
  return this.runWithRetry(() => {
@@ -11820,20 +12482,35 @@ var IndexStore = class _IndexStore {
11820
12482
  `UPDATE refs
11821
12483
  SET to_id = s.id
11822
12484
  FROM (
11823
- SELECT name, MIN(id) AS id FROM symbols GROUP BY name
11824
- ) AS s
12485
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
12486
+ FROM symbols sym
12487
+ JOIN lang_family lf ON lf.lang = sym.lang
12488
+ GROUP BY sym.name, lf.family
12489
+ UNION ALL
12490
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
12491
+ FROM symbols sym
12492
+ GROUP BY sym.name
12493
+ ) AS s,
12494
+ lang_family AS rf
11825
12495
  WHERE refs.to_id IS NULL
11826
12496
  AND refs.to_name IS NOT NULL
11827
- AND refs.to_name = s.name`
12497
+ AND rf.lang = refs.lang
12498
+ AND s.name = refs.to_name
12499
+ AND s.family = rf.family`
11828
12500
  ).run();
11829
12501
  return result.changes ?? 0;
11830
12502
  } catch {
11831
12503
  const result = this.stmt(
11832
12504
  `UPDATE refs SET to_id = (
11833
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
12505
+ SELECT sym.id FROM symbols sym
12506
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12507
+ ORDER BY sym.id LIMIT 1
11834
12508
  ) WHERE to_id IS NULL AND to_name IS NOT NULL
11835
- AND to_name IN (SELECT name FROM symbols)`
11836
- ).run();
12509
+ AND EXISTS (
12510
+ SELECT 1 FROM symbols sym
12511
+ WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
12512
+ )`
12513
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
11837
12514
  return result.changes ?? 0;
11838
12515
  }
11839
12516
  });
@@ -12022,21 +12699,21 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12022
12699
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
12023
12700
  var buildIdCache;
12024
12701
  function projectIndexServerBuildId(entrypoint) {
12025
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
12702
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path17.resolve(entrypoint);
12026
12703
  try {
12027
- const stat18 = fs11.statSync(file);
12704
+ const stat18 = fs12.statSync(file);
12028
12705
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
12029
12706
  return buildIdCache.buildId;
12030
12707
  }
12031
- const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
12708
+ const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
12032
12709
  buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
12033
12710
  return buildId;
12034
12711
  } catch {
12035
- return `unreadable:${path16.basename(file)}`;
12712
+ return `unreadable:${path17.basename(file)}`;
12036
12713
  }
12037
12714
  }
12038
12715
  function normalizeLocalPath(value) {
12039
- const resolved = path16.resolve(value);
12716
+ const resolved = path17.resolve(value);
12040
12717
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12041
12718
  }
12042
12719
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -12048,11 +12725,11 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
12048
12725
  if (process.platform === "win32") {
12049
12726
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12050
12727
  }
12051
- return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
12728
+ return path17.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
12052
12729
  }
12053
12730
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
12054
- return path16.join(
12055
- path16.resolve(resolveIndexDir(projectRoot, indexDir)),
12731
+ return path17.join(
12732
+ path17.resolve(resolveIndexDir(projectRoot, indexDir)),
12056
12733
  PROJECT_INDEX_SERVER_METADATA_FILE
12057
12734
  );
12058
12735
  }
@@ -12092,7 +12769,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
12092
12769
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12093
12770
  try {
12094
12771
  const url = new URL(rel, import.meta.url);
12095
- if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
12772
+ if (url.protocol === "file:" && fs13.existsSync(fileURLToPath2(url))) {
12096
12773
  builtUrl = url;
12097
12774
  break;
12098
12775
  }
@@ -12349,7 +13026,7 @@ var ProjectServerConnection = class {
12349
13026
  currentAuthToken() {
12350
13027
  if (this.authToken === void 0) {
12351
13028
  try {
12352
- const raw = fs12.readFileSync(
13029
+ const raw = fs13.readFileSync(
12353
13030
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
12354
13031
  "utf8"
12355
13032
  );
@@ -12614,7 +13291,7 @@ var ProjectServerConnection = class {
12614
13291
  if (!url) throw new Error("built codebase-index project server is unavailable");
12615
13292
  if (process.platform !== "win32") {
12616
13293
  try {
12617
- fs12.rmSync(this.endpoint, { force: true });
13294
+ fs13.rmSync(this.endpoint, { force: true });
12618
13295
  } catch {
12619
13296
  }
12620
13297
  }
@@ -12638,8 +13315,8 @@ var ProjectServerConnection = class {
12638
13315
  process.kill(pid);
12639
13316
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12640
13317
  try {
12641
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
12642
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
13318
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
13319
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
12643
13320
  } catch {
12644
13321
  }
12645
13322
  return true;
@@ -12709,7 +13386,7 @@ import { Worker } from "node:worker_threads";
12709
13386
 
12710
13387
  // src/codebase-index/indexer.ts
12711
13388
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
12712
- import { execFile as execFile2 } from "node:child_process";
13389
+ import { execFile } from "node:child_process";
12713
13390
  import * as fs17 from "node:fs/promises";
12714
13391
  import { availableParallelism } from "node:os";
12715
13392
  import * as path23 from "node:path";
@@ -12720,8 +13397,8 @@ import {
12720
13397
  } from "@wrongstack/core/utils";
12721
13398
 
12722
13399
  // src/codebase-index/gitignore.ts
12723
- import * as fs13 from "node:fs/promises";
12724
- import * as path17 from "node:path";
13400
+ import * as fs14 from "node:fs/promises";
13401
+ import * as path18 from "node:path";
12725
13402
  import { compileGlob } from "@wrongstack/core/utils";
12726
13403
  function globBody(glob) {
12727
13404
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -12767,7 +13444,7 @@ function compileGitignore(lines) {
12767
13444
  async function loadGitignoreMatcher(projectRoot) {
12768
13445
  let lines = [];
12769
13446
  try {
12770
- const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
13447
+ const raw = await fs14.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
12771
13448
  lines = raw.split("\n");
12772
13449
  } catch {
12773
13450
  }
@@ -12777,8 +13454,434 @@ async function loadGitignoreMatcher(projectRoot) {
12777
13454
  // src/codebase-index/indexer.ts
12778
13455
  init_languages2();
12779
13456
 
13457
+ // src/codebase-index/module-resolver.ts
13458
+ init_languages2();
13459
+ import * as path19 from "node:path";
13460
+ var EXTENSIONS = {
13461
+ js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
13462
+ py: [".py", ".pyi"],
13463
+ rs: [".rs"],
13464
+ jvm: [".java", ".kt", ".scala"],
13465
+ c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
13466
+ ruby: [".rb"],
13467
+ go: [".go"]
13468
+ };
13469
+ var DIRECTORY_ENTRIES = {
13470
+ js: ["index"],
13471
+ py: ["__init__"],
13472
+ rs: ["mod"],
13473
+ ruby: ["index"]
13474
+ };
13475
+ function normalizeNamespace(value) {
13476
+ return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
13477
+ }
13478
+ var ModuleResolver = class {
13479
+ structure;
13480
+ /** Lowercased portable path → the path as indexed (case is preserved). */
13481
+ byPath;
13482
+ /** Lowercased portable directory → files directly inside it, as indexed. */
13483
+ byDir;
13484
+ /** Normalized namespace → the file declaring it (first by path, stable). */
13485
+ byNamespace;
13486
+ constructor(structure, files, namespaces = []) {
13487
+ this.structure = structure;
13488
+ this.byPath = /* @__PURE__ */ new Map();
13489
+ this.byDir = /* @__PURE__ */ new Map();
13490
+ this.byNamespace = /* @__PURE__ */ new Map();
13491
+ const dirsByKey = /* @__PURE__ */ new Map();
13492
+ for (const file of files) {
13493
+ const portable = toPortablePath(file);
13494
+ const pathKey = portable.toLowerCase();
13495
+ const priorPath = this.byPath.get(pathKey);
13496
+ if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
13497
+ else this.byPath.set(pathKey, file);
13498
+ const dir = path19.posix.dirname(portable);
13499
+ const dirKey = dir.toLowerCase();
13500
+ const knownDir = dirsByKey.get(dirKey);
13501
+ if (knownDir === void 0) {
13502
+ dirsByKey.set(dirKey, dir);
13503
+ this.byDir.set(dirKey, [file]);
13504
+ } else if (knownDir === dir) {
13505
+ this.byDir.get(dirKey)?.push(file);
13506
+ } else {
13507
+ dirsByKey.delete(dirKey);
13508
+ this.byDir.delete(dirKey);
13509
+ }
13510
+ }
13511
+ for (const { name, file } of namespaces) {
13512
+ const lang = detectLang(file);
13513
+ if (!lang) continue;
13514
+ const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
13515
+ if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
13516
+ this.byNamespace.set(key, file);
13517
+ }
13518
+ }
13519
+ }
13520
+ /**
13521
+ * Resolve `specifier` as written in `fromFile`.
13522
+ * Returns the indexed target path, or `undefined` when it is external or
13523
+ * cannot be located.
13524
+ */
13525
+ resolve(fromFile, lang, specifier) {
13526
+ const spec = specifier.trim().replace(/\\/g, "/");
13527
+ if (!spec) return void 0;
13528
+ const from = toPortablePath(fromFile);
13529
+ switch (languageFamily(lang)) {
13530
+ case "js":
13531
+ return this.resolveJs(from, spec);
13532
+ case "go":
13533
+ return this.resolveGo(spec);
13534
+ case "py":
13535
+ return this.resolvePython(from, spec);
13536
+ case "rs":
13537
+ return this.resolveRust(from, spec);
13538
+ case "jvm":
13539
+ return this.resolveJvm(spec);
13540
+ case "c":
13541
+ return this.resolveInclude(from, spec);
13542
+ case "ruby":
13543
+ return this.resolveRuby(from, spec);
13544
+ case "dotnet":
13545
+ case "php":
13546
+ case "elixir":
13547
+ case "haskell":
13548
+ return this.resolveNamespace(lang, spec);
13549
+ default:
13550
+ return void 0;
13551
+ }
13552
+ }
13553
+ /**
13554
+ * Resolve a namespace specifier to the file declaring it.
13555
+ *
13556
+ * Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
13557
+ * names a namespace outright, while PHP's `use App\Models\User` names a
13558
+ * *class* inside `App\Models`, so the prefix is what was declared.
13559
+ */
13560
+ resolveNamespace(lang, spec) {
13561
+ const family = languageFamily(lang);
13562
+ const normalized = normalizeNamespace(spec);
13563
+ const exact = this.byNamespace.get(`${family}:${normalized}`);
13564
+ if (exact) return exact;
13565
+ const segments = normalized.split(".").filter(Boolean);
13566
+ if (segments.length < 2) return void 0;
13567
+ return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
13568
+ }
13569
+ // ─── Lookup primitives ──────────────────────────────────────────────────────
13570
+ lookup(candidate) {
13571
+ return this.byPath.get(path19.posix.normalize(candidate).toLowerCase());
13572
+ }
13573
+ /**
13574
+ * Try `base` verbatim, then `base` + each extension, then each directory
13575
+ * entry point inside `base`.
13576
+ */
13577
+ lookupWithExtensions(base, family) {
13578
+ const direct = this.lookup(base);
13579
+ if (direct) return direct;
13580
+ const extensions = EXTENSIONS[family] ?? [];
13581
+ const suffix = path19.posix.extname(base);
13582
+ const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
13583
+ for (const ext of extensions) {
13584
+ const hit = this.lookup(`${stem}${ext}`);
13585
+ if (hit) return hit;
13586
+ }
13587
+ for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
13588
+ for (const ext of extensions) {
13589
+ const hit = this.lookup(path19.posix.join(base, `${entry}${ext}`));
13590
+ if (hit) return hit;
13591
+ }
13592
+ }
13593
+ return void 0;
13594
+ }
13595
+ /**
13596
+ * A representative indexed file inside `dir`, for ecosystems whose import
13597
+ * unit is a directory rather than a file (Go packages, JVM wildcard imports).
13598
+ *
13599
+ * The choice is deterministic — a file named after the directory, else the
13600
+ * first by name — so the same import always produces the same edge. Package
13601
+ * grouping is unaffected either way: every file in the directory carries the
13602
+ * same package label, so the package-level edge is exact regardless of which
13603
+ * member represents it.
13604
+ */
13605
+ representativeIn(dir, family) {
13606
+ const members = this.byDir.get(path19.posix.normalize(dir).toLowerCase());
13607
+ if (!members?.length) return void 0;
13608
+ const extensions = EXTENSIONS[family] ?? [];
13609
+ const eligible = members.filter((file) => extensions.includes(path19.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
13610
+ if (eligible.length === 0) return void 0;
13611
+ const base = path19.posix.basename(path19.posix.normalize(dir)).toLowerCase();
13612
+ const named = eligible.find(
13613
+ (file) => path19.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
13614
+ );
13615
+ return named ?? eligible[0];
13616
+ }
13617
+ // ─── Per-family resolution ──────────────────────────────────────────────────
13618
+ /** Relative specifiers, then workspace package names and their subpaths. */
13619
+ resolveJs(fromFile, spec) {
13620
+ if (spec.startsWith(".")) {
13621
+ const absolute = path19.posix.join(path19.posix.dirname(fromFile), spec);
13622
+ return this.lookupWithExtensions(absolute, "js");
13623
+ }
13624
+ const owner2 = this.structure.roots.find(
13625
+ (root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
13626
+ );
13627
+ if (!owner2?.importPath) return void 0;
13628
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
13629
+ if (!subpath) {
13630
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, "src/index"), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "index"), "js");
13631
+ }
13632
+ return this.lookupWithExtensions(path19.posix.join(owner2.dir, subpath), "js") ?? this.lookupWithExtensions(path19.posix.join(owner2.dir, "src", subpath), "js");
13633
+ }
13634
+ /** Go import paths are absolute module paths; a package is a directory. */
13635
+ resolveGo(spec) {
13636
+ const owner2 = this.structure.roots.find(
13637
+ (root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
13638
+ );
13639
+ if (!owner2?.importPath) return void 0;
13640
+ const subpath = spec.slice(owner2.importPath.length).replace(/^\//, "");
13641
+ return this.representativeIn(path19.posix.join(owner2.dir, subpath), "go");
13642
+ }
13643
+ /**
13644
+ * `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
13645
+ * leading dots the extractor preserves (`.sibling`, `..parent.mod`).
13646
+ */
13647
+ resolvePython(fromFile, spec) {
13648
+ const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
13649
+ if (leadingDots > 0) {
13650
+ let base = path19.posix.dirname(fromFile);
13651
+ for (let i = 1; i < leadingDots; i++) base = path19.posix.dirname(base);
13652
+ const rest = spec.slice(leadingDots).split(".").filter(Boolean);
13653
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest), "py");
13654
+ }
13655
+ const segments = spec.split(".").filter(Boolean);
13656
+ if (segments.length === 0) return void 0;
13657
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
13658
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
13659
+ const hit = this.lookupWithExtensions(path19.posix.join(base, ...segments), "py");
13660
+ if (hit) return hit;
13661
+ if (segments.length > 1) {
13662
+ const parent = this.lookupWithExtensions(
13663
+ path19.posix.join(base, ...segments.slice(0, -1)),
13664
+ "py"
13665
+ );
13666
+ if (parent) return parent;
13667
+ }
13668
+ }
13669
+ return void 0;
13670
+ }
13671
+ /** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
13672
+ resolveRust(fromFile, spec) {
13673
+ const segments = spec.split("::").filter(Boolean);
13674
+ if (segments.length === 0) return void 0;
13675
+ const head = segments[0];
13676
+ if (head === "self" || head === "super") {
13677
+ let base = path19.posix.dirname(fromFile);
13678
+ for (const segment of segments) {
13679
+ if (segment === "super") base = path19.posix.dirname(base);
13680
+ else if (segment !== "self") break;
13681
+ }
13682
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
13683
+ return this.lookupWithExtensions(path19.posix.join(base, ...rest2), "rs");
13684
+ }
13685
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
13686
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
13687
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
13688
+ );
13689
+ if (!crate) {
13690
+ return this.lookupWithExtensions(
13691
+ path19.posix.join(path19.posix.dirname(fromFile), ...segments),
13692
+ "rs"
13693
+ );
13694
+ }
13695
+ const rest = segments.slice(1);
13696
+ for (const base of crate.sourceRoots) {
13697
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path19.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
13698
+ const exact = this.lookupWithExtensions(path19.posix.join(base, ...rest), "rs");
13699
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path19.posix.join(base, "lib"), "rs");
13700
+ if (hit) return hit;
13701
+ }
13702
+ return void 0;
13703
+ }
13704
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
13705
+ resolveJvm(spec) {
13706
+ const segments = spec.split(".").filter(Boolean);
13707
+ if (segments.length === 0) return void 0;
13708
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
13709
+ const wildcard = segments[segments.length - 1] === "*";
13710
+ const parts = wildcard ? segments.slice(0, -1) : segments;
13711
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
13712
+ const target = path19.posix.join(base, ...parts);
13713
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
13714
+ if (hit) return hit;
13715
+ }
13716
+ return void 0;
13717
+ }
13718
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
13719
+ resolveInclude(fromFile, spec) {
13720
+ const relative12 = this.lookupWithExtensions(
13721
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
13722
+ "c"
13723
+ );
13724
+ if (relative12) return relative12;
13725
+ for (const base of [
13726
+ path19.posix.join(this.structure.projectRoot, "include"),
13727
+ this.structure.projectRoot
13728
+ ]) {
13729
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "c");
13730
+ if (hit) return hit;
13731
+ }
13732
+ return void 0;
13733
+ }
13734
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
13735
+ resolveRuby(fromFile, spec) {
13736
+ const relative12 = this.lookupWithExtensions(
13737
+ path19.posix.join(path19.posix.dirname(fromFile), spec),
13738
+ "ruby"
13739
+ );
13740
+ if (relative12) return relative12;
13741
+ for (const base of [
13742
+ path19.posix.join(this.structure.projectRoot, "lib"),
13743
+ this.structure.projectRoot
13744
+ ]) {
13745
+ const hit = this.lookupWithExtensions(path19.posix.join(base, spec), "ruby");
13746
+ if (hit) return hit;
13747
+ }
13748
+ return void 0;
13749
+ }
13750
+ };
13751
+
13752
+ // src/codebase-index/import-extractor.ts
13753
+ var IMPORT_MAX_FILE_CHARS = 512 * 1024;
13754
+ var IMPORT_MAX_PER_FILE = 400;
13755
+ var DOTTED_IMPORT = [
13756
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
13757
+ ];
13758
+ var LANG_IMPORTS = {
13759
+ // Go and Python have real AST extractors; these patterns are the fallback for
13760
+ // machines with no Go toolchain or Python interpreter installed, where the
13761
+ // parser degrades to regex symbols and would otherwise contribute no edges.
13762
+ go: [
13763
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
13764
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
13765
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
13766
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
13767
+ ],
13768
+ py: [
13769
+ { re: /^[ \t]*import\s+([\w.]+)/gm },
13770
+ { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
13771
+ ],
13772
+ rs: [
13773
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
13774
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
13775
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
13776
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
13777
+ ],
13778
+ java: DOTTED_IMPORT,
13779
+ kotlin: DOTTED_IMPORT,
13780
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
13781
+ csharp: [
13782
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
13783
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
13784
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
13785
+ ],
13786
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
13787
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
13788
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
13789
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
13790
+ php: [
13791
+ // `use A\B\C` imports the class C, which is what the index has a symbol
13792
+ // for — the namespace symbol only covers the `A\B` prefix.
13793
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
13794
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
13795
+ ],
13796
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
13797
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
13798
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
13799
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
13800
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
13801
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
13802
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
13803
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
13804
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
13805
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
13806
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
13807
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
13808
+ html: [
13809
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
13810
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
13811
+ ],
13812
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
13813
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
13814
+ };
13815
+ function lastSegment(specifier) {
13816
+ const pathLike = /[/\\]|::/.test(specifier);
13817
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
13818
+ let last = segments[segments.length - 1] ?? specifier;
13819
+ if (last === "*" || last === "_") {
13820
+ last = segments[segments.length - 2] ?? specifier;
13821
+ }
13822
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
13823
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
13824
+ return dotted[dotted.length - 1] ?? last;
13825
+ }
13826
+ function newlineOffsets(content) {
13827
+ const offsets = [];
13828
+ for (let i = 0; i < content.length; i++) {
13829
+ if (content.charCodeAt(i) === 10) offsets.push(i);
13830
+ }
13831
+ return offsets;
13832
+ }
13833
+ function lineAt(offsets, index) {
13834
+ let low = 0;
13835
+ let high = offsets.length;
13836
+ while (low < high) {
13837
+ const mid = low + high >>> 1;
13838
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
13839
+ else high = mid;
13840
+ }
13841
+ return low + 1;
13842
+ }
13843
+ function hasImportPatterns(lang) {
13844
+ return LANG_IMPORTS[lang] !== void 0;
13845
+ }
13846
+ function extractImports(opts) {
13847
+ const patterns = LANG_IMPORTS[opts.lang];
13848
+ if (!patterns || !opts.content) return [];
13849
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
13850
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
13851
+ const refs = [];
13852
+ const seen = /* @__PURE__ */ new Set();
13853
+ const offsets = newlineOffsets(content);
13854
+ for (const pattern of patterns) {
13855
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
13856
+ for (const match of content.matchAll(re)) {
13857
+ if (refs.length >= limit) return refs;
13858
+ const specifier = match[1]?.trim();
13859
+ if (!specifier) continue;
13860
+ const module = specifier;
13861
+ const toName = pattern.name === "full" ? module : lastSegment(module);
13862
+ if (!toName) continue;
13863
+ const key = `${module}\0${toName}`;
13864
+ if (seen.has(key)) continue;
13865
+ seen.add(key);
13866
+ refs.push({
13867
+ fromId: 0,
13868
+ toName,
13869
+ callType: "import",
13870
+ line: lineAt(offsets, match.index ?? 0),
13871
+ lang: opts.lang,
13872
+ module
13873
+ });
13874
+ }
13875
+ }
13876
+ return refs;
13877
+ }
13878
+
12780
13879
  // src/codebase-index/parser-dispatch.ts
12781
13880
  async function parseFileContent(file, content, lang) {
13881
+ const parsed = await dispatch(file, content, lang);
13882
+ return withRelations(parsed, content, lang);
13883
+ }
13884
+ async function dispatch(file, content, lang) {
12782
13885
  switch (lang) {
12783
13886
  case "ts":
12784
13887
  case "tsx":
@@ -12813,6 +13916,13 @@ async function parseFileContent(file, content, lang) {
12813
13916
  }
12814
13917
  }
12815
13918
  }
13919
+ function withRelations(parsed, content, lang) {
13920
+ let refs = parsed.refs ?? [];
13921
+ if (refs.length === 0 && hasImportPatterns(lang)) {
13922
+ refs = extractImports({ content, lang });
13923
+ }
13924
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
13925
+ }
12816
13926
 
12817
13927
  // src/codebase-index/indexer.ts
12818
13928
  var YIELD_EVERY_N = 50;
@@ -12849,7 +13959,7 @@ function normalizeComparablePath(value) {
12849
13959
  }
12850
13960
  function gitOutput(projectRoot, args) {
12851
13961
  return new Promise((resolve16, reject) => {
12852
- execFile2(
13962
+ execFile(
12853
13963
  "git",
12854
13964
  ["-C", projectRoot, ...args],
12855
13965
  {
@@ -12980,13 +14090,40 @@ function assignRefsToSymbols2(refs, symbols) {
12980
14090
  }
12981
14091
  if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12982
14092
  if (!owner2 || owner2.id <= 0) continue;
12983
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
14093
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
12984
14094
  if (seen.has(key)) continue;
12985
14095
  seen.add(key);
12986
14096
  assigned.push({ ...ref, fromId: owner2.id });
12987
14097
  }
12988
14098
  return assigned;
12989
14099
  }
14100
+ async function resolveProjectRelations(store, projectRoot, opts) {
14101
+ if (opts.signal?.aborted) return;
14102
+ try {
14103
+ const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
14104
+ if (indexedFiles.length === 0) return;
14105
+ const structure = await detectModuleRoots(projectRoot, indexedFiles);
14106
+ if (opts.signal?.aborted) return;
14107
+ store.setFilePackages(assignPackageLabels(structure, indexedFiles));
14108
+ const resolver = new ModuleResolver(
14109
+ structure,
14110
+ indexedFiles,
14111
+ store.getNamespaceDeclarations()
14112
+ );
14113
+ const pending2 = store.getUnresolvedImports(opts.onlyFiles);
14114
+ const resolutions = [];
14115
+ for (const entry of pending2) {
14116
+ const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
14117
+ if (toFile && toFile !== entry.fromFile) {
14118
+ resolutions.push({ ...entry, toFile });
14119
+ }
14120
+ }
14121
+ if (opts.signal?.aborted) return;
14122
+ store.applyImportResolutions(resolutions);
14123
+ } catch (err) {
14124
+ opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
14125
+ }
14126
+ }
12990
14127
  async function runIndexerWithStore(store, opts) {
12991
14128
  const { projectRoot, langs, ignore = [], signal } = opts;
12992
14129
  const relationGraphVersion = "2";
@@ -13231,6 +14368,14 @@ async function runIndexerWithStore(store, opts) {
13231
14368
  }
13232
14369
  }
13233
14370
  if (needsFullRefResolution) store.resolveRefs();
14371
+ await resolveProjectRelations(store, projectRoot, {
14372
+ // A watcher run re-resolves only what it touched; a full run (or a contract
14373
+ // bump) re-resolves everything, because a newly indexed file can be the
14374
+ // target of imports written long before it.
14375
+ onlyFiles: needsFullRefResolution ? void 0 : opts.files,
14376
+ errors,
14377
+ signal
14378
+ });
13234
14379
  store.setMetadata("ref_resolution_version", refResolutionVersion);
13235
14380
  store.setMetadata("relation_graph_version", relationGraphVersion);
13236
14381
  if (!opts.files || filesIndexed >= 50) store.optimize();
@@ -14637,17 +15782,17 @@ import {
14637
15782
  } from "@wrongstack/core/design";
14638
15783
  async function resolveReal(p) {
14639
15784
  const resolved = path25.resolve(p);
14640
- let probe2 = resolved;
15785
+ let probe = resolved;
14641
15786
  const missing = [];
14642
15787
  for (; ; ) {
14643
15788
  try {
14644
- return path25.resolve(await fs20.realpath(probe2), ...missing);
15789
+ return path25.resolve(await fs20.realpath(probe), ...missing);
14645
15790
  } catch (err) {
14646
15791
  if (err.code === "ENOENT") {
14647
- const parent = path25.dirname(probe2);
14648
- if (parent === probe2) return resolved;
14649
- missing.unshift(path25.basename(probe2));
14650
- probe2 = parent;
15792
+ const parent = path25.dirname(probe);
15793
+ if (parent === probe) return resolved;
15794
+ missing.unshift(path25.basename(probe));
15795
+ probe = parent;
14651
15796
  continue;
14652
15797
  }
14653
15798
  return resolved;
@@ -14922,7 +16067,7 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
14922
16067
 
14923
16068
  // src/diff.ts
14924
16069
  init_util();
14925
- import { spawn as spawn8 } from "node:child_process";
16070
+ import { spawn as spawn7 } from "node:child_process";
14926
16071
  import { statSync as statSync3 } from "node:fs";
14927
16072
  import * as fs21 from "node:fs/promises";
14928
16073
  import * as path26 from "node:path";
@@ -15025,7 +16170,7 @@ function runGit(args, cwd, signal) {
15025
16170
  return new Promise((resolve16) => {
15026
16171
  let stdout = "";
15027
16172
  let stderr = "";
15028
- const child = spawn8("git", args, {
16173
+ const child = spawn7("git", args, {
15029
16174
  cwd,
15030
16175
  signal,
15031
16176
  env: buildChildEnv3(),
@@ -15236,7 +16381,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
15236
16381
 
15237
16382
  // src/e2e.ts
15238
16383
  init_util();
15239
- import { open, readdir as readdir6 } from "node:fs/promises";
16384
+ import { open, readdir as readdir7 } from "node:fs/promises";
15240
16385
  import * as path27 from "node:path";
15241
16386
  async function readBoundedText(filePath, maxBytes) {
15242
16387
  let handle;
@@ -15354,7 +16499,7 @@ async function scanWorkspace(root, maxDepth, signal) {
15354
16499
  }
15355
16500
  let entries;
15356
16501
  try {
15357
- entries = await readdir6(current.directory, { withFileTypes: true });
16502
+ entries = await readdir7(current.directory, { withFileTypes: true });
15358
16503
  } catch {
15359
16504
  continue;
15360
16505
  }
@@ -15413,7 +16558,7 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
15413
16558
  while (true) {
15414
16559
  const names = /* @__PURE__ */ new Set();
15415
16560
  try {
15416
- for (const entry of await readdir6(directory)) names.add(entry);
16561
+ for (const entry of await readdir7(directory)) names.add(entry);
15417
16562
  } catch {
15418
16563
  }
15419
16564
  if (names.has("pnpm-lock.yaml")) return "pnpm";
@@ -15481,7 +16626,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
15481
16626
  if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
15482
16627
  let entries;
15483
16628
  try {
15484
- entries = await readdir6(directory, { withFileTypes: true });
16629
+ entries = await readdir7(directory, { withFileTypes: true });
15485
16630
  } catch {
15486
16631
  continue;
15487
16632
  }
@@ -15682,7 +16827,7 @@ function findLadderMatches(fileLf, oldLf) {
15682
16827
  const exact = [];
15683
16828
  let idx = fileLf.indexOf(oldLf);
15684
16829
  while (idx !== -1) {
15685
- exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
16830
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt2(fileLf, idx) });
15686
16831
  idx = fileLf.indexOf(oldLf, idx + 1);
15687
16832
  }
15688
16833
  if (exact.length > 0) return { tier: "exact", matches: exact };
@@ -15714,7 +16859,7 @@ function findLadderMatches(fileLf, oldLf) {
15714
16859
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
15715
16860
  return fuzzyScan(fileLines, needleLines, offsets);
15716
16861
  }
15717
- function lineAt(text, pos) {
16862
+ function lineAt2(text, pos) {
15718
16863
  if (pos < 512) {
15719
16864
  let line2 = 1;
15720
16865
  for (let i = 0; i < pos; i++) {
@@ -16176,7 +17321,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
16176
17321
  };
16177
17322
 
16178
17323
  // src/exec.ts
16179
- import { spawn as spawn9 } from "node:child_process";
17324
+ import { spawn as spawn8 } from "node:child_process";
16180
17325
  import {
16181
17326
  emitProcessCompleted as emitProcessCompleted3,
16182
17327
  emitProcessOutput as emitProcessOutput3,
@@ -17393,6 +18538,26 @@ var BLOCKED_ARG_PATTERNS = {
17393
18538
  pnpm: [],
17394
18539
  npx: []
17395
18540
  };
18541
+ var BLOCKED_OPTION_NAMES = {
18542
+ git: /* @__PURE__ */ new Set([
18543
+ "--exec",
18544
+ "--upload-pack",
18545
+ "--receive-pack",
18546
+ "--exec-path",
18547
+ "--git-dir",
18548
+ "--work-tree",
18549
+ "--namespace",
18550
+ "-c",
18551
+ "--config",
18552
+ "--config-env",
18553
+ "-C"
18554
+ ]),
18555
+ find: /* @__PURE__ */ new Set(["-exec", "-ok", "-execdir"])
18556
+ };
18557
+ function optionName(arg) {
18558
+ const eq = arg.indexOf("=");
18559
+ return eq > 0 ? arg.slice(0, eq) : arg;
18560
+ }
17396
18561
  var BLOCKED_SUBCOMMANDS = {
17397
18562
  docker: /* @__PURE__ */ new Set(["push"]),
17398
18563
  podman: /* @__PURE__ */ new Set(["push"]),
@@ -17430,6 +18595,15 @@ function validateArgs(cmd, args) {
17430
18595
  const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
17431
18596
  if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
17432
18597
  }
18598
+ const blockedOptions = BLOCKED_OPTION_NAMES[cmd];
18599
+ if (blockedOptions) {
18600
+ for (const arg of args) {
18601
+ if (arg === "--") break;
18602
+ if (blockedOptions.has(optionName(arg))) {
18603
+ return `Blocked option "${optionName(arg)}" for command "${cmd}"`;
18604
+ }
18605
+ }
18606
+ }
17433
18607
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
17434
18608
  if (!blocked) return null;
17435
18609
  for (const arg of args) {
@@ -17612,7 +18786,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
17612
18786
  };
17613
18787
  let child;
17614
18788
  try {
17615
- child = spawn9(spawnCmd, spawnArgs, {
18789
+ child = spawn8(spawnCmd, spawnArgs, {
17616
18790
  cwd,
17617
18791
  env: buildChildEnv2(sessionId),
17618
18792
  stdio: ["ignore", "pipe", "pipe"],
@@ -18246,7 +19420,7 @@ async function detectFixer(cwd) {
18246
19420
 
18247
19421
  // src/git.ts
18248
19422
  init_util();
18249
- import { spawn as spawn10 } from "node:child_process";
19423
+ import { spawn as spawn9 } from "node:child_process";
18250
19424
  import { statSync as statSync4 } from "node:fs";
18251
19425
  import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
18252
19426
  import { assessCommitSafety } from "@wrongstack/core/coordination";
@@ -18510,7 +19684,7 @@ function runGit2(args, cwd, signal) {
18510
19684
  return new Promise((resolve16) => {
18511
19685
  let stdout = "";
18512
19686
  let stderr = "";
18513
- const child = spawn10("git", args, {
19687
+ const child = spawn9("git", args, {
18514
19688
  cwd,
18515
19689
  signal,
18516
19690
  env: buildChildEnv4(),
@@ -18701,7 +19875,7 @@ var globTool = {
18701
19875
  };
18702
19876
 
18703
19877
  // src/grep.ts
18704
- import { spawn as spawn11 } from "node:child_process";
19878
+ import { spawn as spawn10 } from "node:child_process";
18705
19879
  import * as fs25 from "node:fs/promises";
18706
19880
  import * as path31 from "node:path";
18707
19881
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
@@ -18858,7 +20032,7 @@ var grepTool = {
18858
20032
  async function detectRg(signal) {
18859
20033
  return new Promise((resolve16) => {
18860
20034
  try {
18861
- const p = spawn11("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
20035
+ const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
18862
20036
  p.on("error", () => resolve16(false));
18863
20037
  p.on("close", (code) => resolve16(code === 0));
18864
20038
  } catch {
@@ -18892,7 +20066,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
18892
20066
  const FLUSH_AT = 16;
18893
20067
  const MAX_BUF_BYTES = 1e6;
18894
20068
  let bufOverflow = false;
18895
- const child = spawn11("rg", args, {
20069
+ const child = spawn10("rg", args, {
18896
20070
  signal,
18897
20071
  env: buildChildEnv5(),
18898
20072
  // rg diagnostics are not part of the tool result. Ignoring stderr avoids
@@ -19189,7 +20363,7 @@ async function runNative(input, base, mode, limit, signal) {
19189
20363
  init_spawn_stream();
19190
20364
  init_util();
19191
20365
  init_legacy_bridge();
19192
- import { join as join25 } from "node:path";
20366
+ import { join as join24 } from "node:path";
19193
20367
  import {
19194
20368
  detectEcosystem as detectPackageEcosystem,
19195
20369
  recordPackageAction
@@ -19373,17 +20547,17 @@ function resolveManifestPath(cwd, pkgManager) {
19373
20547
  case "pnpm":
19374
20548
  case "yarn":
19375
20549
  case "npm":
19376
- return join25(cwd, "package.json");
20550
+ return join24(cwd, "package.json");
19377
20551
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
19378
20552
  default:
19379
- return join25(cwd, "package.json");
20553
+ return join24(cwd, "package.json");
19380
20554
  }
19381
20555
  }
19382
20556
 
19383
20557
  // src/json.ts
19384
- init_util();
19385
20558
  import * as fs26 from "node:fs/promises";
19386
20559
  import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
20560
+ init_util();
19387
20561
  var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
19388
20562
  var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
19389
20563
  var JsonFileTooLargeError = class extends Error {
@@ -19822,8 +20996,12 @@ function validateJsonSchema(data, schema) {
19822
20996
  }
19823
20997
  }
19824
20998
  if (typeof value === "string" && s["pattern"]) {
19825
- const re = new RegExp(s["pattern"]);
19826
- if (!re.test(value)) errors.push(`${path38}: does not match pattern ${s["pattern"]}`);
20999
+ const compiled = compileUserRegex(s["pattern"], "");
21000
+ if (!compiled.ok) {
21001
+ errors.push(`${path38}: invalid schema pattern \u2014 ${compiled.reason}`);
21002
+ } else if (!compiled.regex.test(capSubject(value))) {
21003
+ errors.push(`${path38}: does not match pattern ${s["pattern"]}`);
21004
+ }
19827
21005
  }
19828
21006
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
19829
21007
  errors.push(`${path38}: string too short (min ${s["minLength"]})`);
@@ -21808,7 +22986,7 @@ async function detectLinter(cwd) {
21808
22986
  }
21809
22987
 
21810
22988
  // src/logs.ts
21811
- import { spawn as spawn12 } from "node:child_process";
22989
+ import { spawn as spawn11 } from "node:child_process";
21812
22990
  import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
21813
22991
  init_util();
21814
22992
  var logsTool = {
@@ -21915,7 +23093,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
21915
23093
  clearTimeout(timer);
21916
23094
  resolve16(result);
21917
23095
  };
21918
- const child = spawn12("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
23096
+ const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
21919
23097
  const timer = setTimeout(() => {
21920
23098
  child.kill("SIGTERM");
21921
23099
  finish(empty());
@@ -22024,7 +23202,7 @@ function parseLine(line) {
22024
23202
  // src/outdated.ts
22025
23203
  init_util();
22026
23204
  init_win32_resolve();
22027
- import { spawn as spawn13 } from "node:child_process";
23205
+ import { spawn as spawn12 } from "node:child_process";
22028
23206
  import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
22029
23207
  var outdatedTool = {
22030
23208
  name: "outdated",
@@ -22144,7 +23322,7 @@ function runOutdated(manager, args, cwd, signal) {
22144
23322
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
22145
23323
  const spawnCmd = shim?.command ?? resolved;
22146
23324
  const spawnArgs = shim?.args ?? args;
22147
- const child = spawn13(spawnCmd, spawnArgs, {
23325
+ const child = spawn12(spawnCmd, spawnArgs, {
22148
23326
  cwd,
22149
23327
  signal,
22150
23328
  env: buildChildEnv7(),
@@ -22210,7 +23388,7 @@ function parseOutdatedOutput(json2, exitCode) {
22210
23388
 
22211
23389
  // src/patch.ts
22212
23390
  init_util();
22213
- import { spawn as spawn14 } from "node:child_process";
23391
+ import { spawn as spawn13 } from "node:child_process";
22214
23392
  import * as fs27 from "node:fs/promises";
22215
23393
  import * as os9 from "node:os";
22216
23394
  import * as path32 from "node:path";
@@ -22352,7 +23530,7 @@ function runPatch(args, cwd, signal) {
22352
23530
  let stdout = "";
22353
23531
  let stderr = "";
22354
23532
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
22355
- const child = spawn14("patch", args, {
23533
+ const child = spawn13("patch", args, {
22356
23534
  cwd,
22357
23535
  signal,
22358
23536
  env,
@@ -22949,7 +24127,7 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
22949
24127
  }
22950
24128
 
22951
24129
  // src/replace.ts
22952
- import { spawn as spawn15 } from "node:child_process";
24130
+ import { spawn as spawn14 } from "node:child_process";
22953
24131
  import * as fs29 from "node:fs/promises";
22954
24132
  import * as path33 from "node:path";
22955
24133
  import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
@@ -23134,7 +24312,7 @@ async function globFiles(pattern, base, extraGlob) {
23134
24312
  function checkRg() {
23135
24313
  return new Promise((resolve16) => {
23136
24314
  try {
23137
- const p = spawn15("rg", ["--version"], {
24315
+ const p = spawn14("rg", ["--version"], {
23138
24316
  env: buildChildEnv9(),
23139
24317
  stdio: "ignore",
23140
24318
  windowsHide: true
@@ -23148,7 +24326,7 @@ function checkRg() {
23148
24326
  }
23149
24327
  function spawnRgFind(pattern, base) {
23150
24328
  const args = ["--files", "--glob", pattern, base];
23151
- const child = spawn15("rg", args, {
24329
+ const child = spawn14("rg", args, {
23152
24330
  signal: AbortSignal.timeout(3e4),
23153
24331
  env: buildChildEnv9(),
23154
24332
  stdio: ["ignore", "pipe", "pipe"],
@@ -25187,7 +26365,7 @@ var writeTool = {
25187
26365
  required: ["path", "content"]
25188
26366
  },
25189
26367
  async execute(input, ctx, opts) {
25190
- return writeFile7(input, ctx, opts?.signal);
26368
+ return writeFile6(input, ctx, opts?.signal);
25191
26369
  },
25192
26370
  async *executeStream(input, ctx, opts) {
25193
26371
  const prepared = await prepareWrite(input, ctx);
@@ -25200,7 +26378,7 @@ var writeTool = {
25200
26378
  yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
25201
26379
  }
25202
26380
  };
25203
- async function writeFile7(input, ctx, signal) {
26381
+ async function writeFile6(input, ctx, signal) {
25204
26382
  return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
25205
26383
  }
25206
26384
  async function prepareWrite(input, ctx) {