@wrongstack/tools 0.296.2 → 0.296.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/browser/artifacts.d.ts +2 -0
  2. package/dist/browser/artifacts.d.ts.map +1 -1
  3. package/dist/browser/index.js +41 -0
  4. package/dist/browser/index.js.map +2 -2
  5. package/dist/builtin.js +1812 -1426
  6. package/dist/builtin.js.map +4 -4
  7. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  8. package/dist/codebase-index/dead-code-scan.d.ts +2 -2
  9. package/dist/codebase-index/dead-code-scan.d.ts.map +1 -1
  10. package/dist/codebase-index/index.d.ts +1 -0
  11. package/dist/codebase-index/index.d.ts.map +1 -1
  12. package/dist/codebase-index/index.js +1827 -1542
  13. package/dist/codebase-index/index.js.map +4 -4
  14. package/dist/codebase-index/project-server-client.d.ts +13 -1
  15. package/dist/codebase-index/project-server-client.d.ts.map +1 -1
  16. package/dist/codebase-index/project-server-endpoint.d.ts +29 -1
  17. package/dist/codebase-index/project-server-endpoint.d.ts.map +1 -1
  18. package/dist/codebase-index/project-server.js +226 -152
  19. package/dist/codebase-index/project-server.js.map +4 -4
  20. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  21. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  22. package/dist/codebase-index/worker.js +214 -145
  23. package/dist/codebase-index/worker.js.map +3 -3
  24. package/dist/codebase-index/writer.d.ts +19 -1
  25. package/dist/codebase-index/writer.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1851 -1462
  29. package/dist/index.js.map +4 -4
  30. package/dist/pack.js +1812 -1426
  31. package/dist/pack.js.map +4 -4
  32. package/dist/read.d.ts +31 -0
  33. package/dist/read.d.ts.map +1 -1
  34. package/dist/read.js +5465 -78
  35. package/dist/read.js.map +4 -4
  36. package/dist/tool-tier.js +1812 -1426
  37. package/dist/tool-tier.js.map +4 -4
  38. package/package.json +3 -3
package/dist/builtin.js CHANGED
@@ -4893,16 +4893,16 @@ var init_legacy_bridge = __esm({
4893
4893
  });
4894
4894
 
4895
4895
  // src/codebase-index/languages.ts
4896
- import * as path14 from "node:path";
4896
+ import * as path18 from "node:path";
4897
4897
  function detectLang(file) {
4898
- const base = path14.basename(file);
4898
+ const base = path18.basename(file);
4899
4899
  const lowerBase = base.toLowerCase();
4900
4900
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4901
4901
  return "ts";
4902
4902
  }
4903
4903
  const special = SPECIAL_FILENAMES[lowerBase];
4904
4904
  if (special) return special;
4905
- const ext = path14.extname(base).toLowerCase();
4905
+ const ext = path18.extname(base).toLowerCase();
4906
4906
  if (!ext) return null;
4907
4907
  return EXT_TO_LANG[ext] ?? null;
4908
4908
  }
@@ -5131,11 +5131,18 @@ async function parseSymbols(opts) {
5131
5131
  } else if (ts.isHeritageClause(node)) {
5132
5132
  for (const t of node.types) {
5133
5133
  const name = getTypeName(t.expression);
5134
- if (name) refs.push({ fromId: 0, toName: name, callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement", line: lineNum });
5134
+ if (name)
5135
+ refs.push({
5136
+ fromId: 0,
5137
+ toName: name,
5138
+ callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
5139
+ line: lineNum
5140
+ });
5135
5141
  }
5136
5142
  } else if (ts.isImportDeclaration(node)) {
5137
- const moduleName = getModuleName(node);
5138
- if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
5143
+ emitImportSpecifierRefs(node, refs, lineNum);
5144
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
5145
+ emitExportSpecifierRefs(node, refs, lineNum);
5139
5146
  }
5140
5147
  const scopeIdx = scopeParts.length;
5141
5148
  pushScopeName(node, scopeParts);
@@ -5151,11 +5158,6 @@ function getTypeName(name) {
5151
5158
  if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
5152
5159
  return "";
5153
5160
  }
5154
- function getModuleName(node) {
5155
- const moduleSpecifier = node.moduleSpecifier;
5156
- if (ts.isStringLiteral(moduleSpecifier)) return moduleSpecifier.text;
5157
- return "";
5158
- }
5159
5161
  function deduplicateRefs(refs) {
5160
5162
  const seen = /* @__PURE__ */ new Set();
5161
5163
  return refs.filter((r) => {
@@ -5165,6 +5167,44 @@ function deduplicateRefs(refs) {
5165
5167
  return true;
5166
5168
  });
5167
5169
  }
5170
+ function getImportSpecifierName(spec) {
5171
+ return spec.propertyName?.text ?? spec.name.text;
5172
+ }
5173
+ function emitImportSpecifierRefs(node, refs, lineNum) {
5174
+ const clause = node.importClause;
5175
+ if (!clause) return;
5176
+ if (clause.name) {
5177
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5178
+ }
5179
+ const bindings = clause.namedBindings;
5180
+ if (!bindings) return;
5181
+ if (ts.isNamedImports(bindings)) {
5182
+ for (const element of bindings.elements) {
5183
+ refs.push({
5184
+ fromId: 0,
5185
+ toName: getImportSpecifierName(element),
5186
+ callType: "import",
5187
+ line: lineNum
5188
+ });
5189
+ }
5190
+ } else if (ts.isNamespaceImport(bindings)) {
5191
+ refs.push({ fromId: 0, toName: bindings.name.text, callType: "import", line: lineNum });
5192
+ }
5193
+ }
5194
+ function emitExportSpecifierRefs(node, refs, lineNum) {
5195
+ const clause = node.exportClause;
5196
+ if (clause && ts.isNamespaceExport(clause)) {
5197
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
5198
+ return;
5199
+ }
5200
+ if (clause && ts.isNamedExports(clause)) {
5201
+ for (const element of clause.elements) {
5202
+ const originalName = element.propertyName?.text ?? element.name.text;
5203
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
5204
+ }
5205
+ return;
5206
+ }
5207
+ }
5168
5208
  var ts, tsLoad, kindMapCache;
5169
5209
  var init_ts_parser = __esm({
5170
5210
  "src/codebase-index/ts-parser.ts"() {
@@ -5198,10 +5238,10 @@ __export(go_parser_exports, {
5198
5238
  detectLang: () => detectLang,
5199
5239
  parseSymbols: () => parseSymbols2
5200
5240
  });
5201
- import { spawn as spawn4 } from "node:child_process";
5202
- import * as os5 from "node:os";
5203
- import * as path15 from "node:path";
5204
- import * as fs10 from "node:fs/promises";
5241
+ import { spawn as spawn5 } from "node:child_process";
5242
+ import * as os6 from "node:os";
5243
+ import * as path19 from "node:path";
5244
+ import * as fs14 from "node:fs/promises";
5205
5245
  async function parseSymbols2(opts) {
5206
5246
  const { file, content, lang } = opts;
5207
5247
  try {
@@ -5273,16 +5313,16 @@ async function syncGoParse(filePath, content, lang) {
5273
5313
  try {
5274
5314
  let scriptPath = _cachedGoScriptPath;
5275
5315
  if (!scriptPath) {
5276
- const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
5277
- scriptPath = path15.join(tmpDir, "parse.go");
5278
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5316
+ const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5317
+ scriptPath = path19.join(tmpDir, "parse.go");
5318
+ await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5279
5319
  _cachedGoScriptPath = scriptPath;
5280
5320
  }
5281
5321
  const goBinary = resolveWin32Command("go");
5282
5322
  const goResult = await new Promise(
5283
5323
  (resolve16, reject) => {
5284
5324
  let settled = false;
5285
- const proc = spawn4(goBinary, ["run", scriptPath], {
5325
+ const proc = spawn5(goBinary, ["run", scriptPath], {
5286
5326
  stdio: ["pipe", "pipe", "pipe"],
5287
5327
  windowsHide: true
5288
5328
  });
@@ -5885,10 +5925,10 @@ __export(py_parser_exports, {
5885
5925
  detectLang: () => detectLang,
5886
5926
  parseSymbols: () => parseSymbols4
5887
5927
  });
5888
- import { spawn as spawn5 } from "node:child_process";
5889
- import * as fs11 from "node:fs/promises";
5890
- import * as os6 from "node:os";
5891
- import * as path16 from "node:path";
5928
+ import { spawn as spawn6 } from "node:child_process";
5929
+ import * as fs15 from "node:fs/promises";
5930
+ import * as os7 from "node:os";
5931
+ import * as path20 from "node:path";
5892
5932
  async function parseSymbols4(opts) {
5893
5933
  const { file, content, lang } = opts;
5894
5934
  try {
@@ -5910,7 +5950,7 @@ async function resolvePython() {
5910
5950
  function commandIsAvailable(command) {
5911
5951
  return new Promise((resolve16) => {
5912
5952
  let settled = false;
5913
- const proc = spawn5(command, ["--version"], {
5953
+ const proc = spawn6(command, ["--version"], {
5914
5954
  stdio: "ignore",
5915
5955
  windowsHide: true
5916
5956
  });
@@ -5932,7 +5972,7 @@ function commandIsAvailable(command) {
5932
5972
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
5933
5973
  return new Promise((resolve16, reject) => {
5934
5974
  let settled = false;
5935
- const proc = spawn5(pyBinary, [scriptPath, filePath], {
5975
+ const proc = spawn6(pyBinary, [scriptPath, filePath], {
5936
5976
  stdio: ["pipe", "pipe", "pipe"],
5937
5977
  windowsHide: true
5938
5978
  });
@@ -5966,10 +6006,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
5966
6006
  async function syncPyParse(filePath, content, lang) {
5967
6007
  try {
5968
6008
  if (!_cachedScriptPath) {
5969
- const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
5970
- await fs11.mkdir(tmpDir, { recursive: true });
5971
- _cachedScriptPath = path16.join(tmpDir, "parse.py");
5972
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6009
+ const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6010
+ await fs15.mkdir(tmpDir, { recursive: true });
6011
+ _cachedScriptPath = path20.join(tmpDir, "parse.py");
6012
+ await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
5973
6013
  }
5974
6014
  cachedPyBinary ??= resolvePython();
5975
6015
  const pyBinary = await cachedPyBinary;
@@ -6223,10 +6263,10 @@ __export(rs_parser_exports, {
6223
6263
  detectLang: () => detectLang,
6224
6264
  parseSymbols: () => parseSymbols5
6225
6265
  });
6226
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
6227
- import { execFile, spawn as spawn6 } from "node:child_process";
6228
- import * as fs12 from "node:fs/promises";
6229
- import * as path17 from "node:path";
6266
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6267
+ import { execFile, spawn as spawn7 } from "node:child_process";
6268
+ import * as fs16 from "node:fs/promises";
6269
+ import * as path21 from "node:path";
6230
6270
  async function parseSymbols5(opts) {
6231
6271
  const { file, content, lang } = opts;
6232
6272
  const nativeAvailable = await checkNativeParser();
@@ -6248,7 +6288,7 @@ function checkNativeParser() {
6248
6288
  nativeParserAvailability ??= (async () => {
6249
6289
  try {
6250
6290
  await probe("rustc", ["--version"]);
6251
- const toolsDir = path17.join(process.cwd(), "tools");
6291
+ const toolsDir = path21.join(process.cwd(), "tools");
6252
6292
  await probe(
6253
6293
  "cargo",
6254
6294
  [
@@ -6257,7 +6297,7 @@ function checkNativeParser() {
6257
6297
  "--format-version",
6258
6298
  "1",
6259
6299
  "--manifest-path",
6260
- path17.join(toolsDir, "Cargo.toml")
6300
+ path21.join(toolsDir, "Cargo.toml")
6261
6301
  ]
6262
6302
  );
6263
6303
  return true;
@@ -6269,17 +6309,17 @@ function checkNativeParser() {
6269
6309
  }
6270
6310
  async function tryNativeParse(file, content) {
6271
6311
  try {
6272
- const toolsDir = path17.join(process.cwd(), "tools");
6273
- const crateDir = path17.join(toolsDir, "syn-parser");
6274
- const tmpFile = path17.join(crateDir, "src", "input.rs");
6275
- await fs12.writeFile(tmpFile, content, "utf8");
6312
+ const toolsDir = path21.join(process.cwd(), "tools");
6313
+ const crateDir = path21.join(toolsDir, "syn-parser");
6314
+ const tmpFile = path21.join(crateDir, "src", "input.rs");
6315
+ await fs16.writeFile(tmpFile, content, "utf8");
6276
6316
  const cargoBinary = resolveWin32Command("cargo");
6277
6317
  const result = await new Promise(
6278
6318
  (resolve16, reject) => {
6279
6319
  let settled = false;
6280
- const proc = spawn6(
6320
+ const proc = spawn7(
6281
6321
  cargoBinary,
6282
- ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
6322
+ ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6283
6323
  {
6284
6324
  cwd: process.cwd(),
6285
6325
  stdio: ["pipe", "pipe", "pipe"],
@@ -6338,7 +6378,7 @@ function regexParse(opts) {
6338
6378
  let hi = lineOffsets2.length - 1;
6339
6379
  while (lo < hi) {
6340
6380
  const mid = lo + hi + 1 >>> 1;
6341
- if (expectDefined2(lineOffsets2[mid]) <= offset) lo = mid;
6381
+ if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6342
6382
  else hi = mid - 1;
6343
6383
  }
6344
6384
  return lo + 1;
@@ -6350,7 +6390,7 @@ function regexParse(opts) {
6350
6390
  for (const pattern of RS_PATTERNS) {
6351
6391
  pattern.regex.lastIndex = 0;
6352
6392
  for (let match = pattern.regex.exec(content); match !== null; match = pattern.regex.exec(content)) {
6353
- const name = expectDefined2(match[1]);
6393
+ const name = expectDefined3(match[1]);
6354
6394
  const offset = match.index ?? 0;
6355
6395
  const line = lineFromOffset(offset);
6356
6396
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6407,8 +6447,8 @@ __export(json_parser_exports, {
6407
6447
  detectLang: () => detectLang,
6408
6448
  parseSymbols: () => parseSymbols6
6409
6449
  });
6410
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6411
- import * as path18 from "node:path";
6450
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
6451
+ import * as path22 from "node:path";
6412
6452
  function parseSymbols6(opts) {
6413
6453
  const { file, content, lang } = opts;
6414
6454
  try {
@@ -6420,7 +6460,7 @@ function parseSymbols6(opts) {
6420
6460
  function regexParse2(opts) {
6421
6461
  const { file, content, lang } = opts;
6422
6462
  const symbols = [];
6423
- const basename12 = path18.basename(file).toLowerCase();
6463
+ const basename12 = path22.basename(file).toLowerCase();
6424
6464
  const isPackageJson = basename12 === "package.json";
6425
6465
  const isTsconfig = basename12 === "tsconfig.json" || basename12 === "tsconfig.build.json";
6426
6466
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -6435,22 +6475,22 @@ function regexParse2(opts) {
6435
6475
  let hi = lineOffsets2.length - 1;
6436
6476
  while (lo < hi) {
6437
6477
  const mid = lo + hi + 1 >>> 1;
6438
- if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6478
+ if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6439
6479
  else hi = mid - 1;
6440
6480
  }
6441
6481
  return lo + 1;
6442
6482
  }
6443
6483
  const rootMatch = content.match(/^\s*\{/m);
6444
6484
  if (rootMatch) {
6445
- const offset = expectDefined3(rootMatch.index);
6485
+ const offset = expectDefined4(rootMatch.index);
6446
6486
  const line = lineFromOffset(offset);
6447
6487
  symbols.push(
6448
6488
  makeSymbol({
6449
- name: path18.basename(file),
6489
+ name: path22.basename(file),
6450
6490
  kind: "object",
6451
6491
  line,
6452
6492
  col: 0,
6453
- signature: `"${path18.basename(file)}" = { ... }`,
6493
+ signature: `"${path22.basename(file)}" = { ... }`,
6454
6494
  file,
6455
6495
  lang
6456
6496
  })
@@ -6458,7 +6498,7 @@ function regexParse2(opts) {
6458
6498
  }
6459
6499
  const topLevelKeyRegex = /^\s*"([^"]+)"\s*:/gm;
6460
6500
  for (let match = topLevelKeyRegex.exec(content); match !== null; match = topLevelKeyRegex.exec(content)) {
6461
- const key = expectDefined3(match[1]);
6501
+ const key = expectDefined4(match[1]);
6462
6502
  const offset = match.index ?? 0;
6463
6503
  const line = lineFromOffset(offset);
6464
6504
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6505,7 +6545,7 @@ function regexParse2(opts) {
6505
6545
  const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
6506
6546
  const defsMatch = defsRegex.exec(content);
6507
6547
  if (defsMatch !== null) {
6508
- const offset = expectDefined3(defsMatch.index);
6548
+ const offset = expectDefined4(defsMatch.index);
6509
6549
  const line = lineFromOffset(offset);
6510
6550
  symbols.push(
6511
6551
  makeSymbol({
@@ -6530,7 +6570,7 @@ function regexParse2(opts) {
6530
6570
  for (let match = pat.exec(content); match !== null; match = pat.exec(content)) {
6531
6571
  const offset = match.index ?? 0;
6532
6572
  const line = lineFromOffset(offset);
6533
- const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined3(match[0]);
6573
+ const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined4(match[0]);
6534
6574
  symbols.push(
6535
6575
  makeSymbol({
6536
6576
  name: key,
@@ -6549,12 +6589,12 @@ function regexParse2(opts) {
6549
6589
  function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
6550
6590
  const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
6551
6591
  for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
6552
- const blockContent = expectDefined3(match[0]);
6592
+ const blockContent = expectDefined4(match[0]);
6553
6593
  const blockOffset = match.index ?? 0;
6554
6594
  const scriptKeyRegex = /"(\w[\w-]*)"\s*:/g;
6555
6595
  for (let scriptMatch = scriptKeyRegex.exec(blockContent); scriptMatch !== null; scriptMatch = scriptKeyRegex.exec(blockContent)) {
6556
- const key = expectDefined3(scriptMatch[1]);
6557
- const keyOffset = blockOffset + expectDefined3(scriptMatch.index);
6596
+ const key = expectDefined4(scriptMatch[1]);
6597
+ const keyOffset = blockOffset + expectDefined4(scriptMatch.index);
6558
6598
  const line = lineFromOffset(keyOffset);
6559
6599
  symbols.push(
6560
6600
  makeSymbol({
@@ -6573,12 +6613,12 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineF
6573
6613
  function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
6574
6614
  const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
6575
6615
  for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
6576
- const blockContent = expectDefined3(match[0]);
6616
+ const blockContent = expectDefined4(match[0]);
6577
6617
  const blockOffset = match.index ?? 0;
6578
6618
  const optKeyRegex = /"(\w[\w]*)"\s*:/g;
6579
6619
  for (let optMatch = optKeyRegex.exec(blockContent); optMatch !== null; optMatch = optKeyRegex.exec(blockContent)) {
6580
- const key = expectDefined3(optMatch[1]);
6581
- const keyOffset = blockOffset + expectDefined3(optMatch.index);
6620
+ const key = expectDefined4(optMatch[1]);
6621
+ const keyOffset = blockOffset + expectDefined4(optMatch.index);
6582
6622
  const line = lineFromOffset(keyOffset);
6583
6623
  if (line <= parentLine) continue;
6584
6624
  symbols.push(
@@ -6623,7 +6663,7 @@ __export(yaml_parser_exports, {
6623
6663
  detectLang: () => detectLang,
6624
6664
  parseSymbols: () => parseSymbols7
6625
6665
  });
6626
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
6666
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
6627
6667
  function parseSymbols7(opts) {
6628
6668
  const { file, content, lang } = opts;
6629
6669
  try {
@@ -6645,14 +6685,14 @@ function regexParse3(opts) {
6645
6685
  let hi = lineOffsets2.length - 1;
6646
6686
  while (lo < hi) {
6647
6687
  const mid = lo + hi + 1 >>> 1;
6648
- if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6688
+ if (expectDefined5(lineOffsets2[mid]) <= offset) lo = mid;
6649
6689
  else hi = mid - 1;
6650
6690
  }
6651
6691
  return lo + 1;
6652
6692
  }
6653
6693
  const anchorRegex = /&(\w[\w-]*)/g;
6654
6694
  for (let match = anchorRegex.exec(content); match !== null; match = anchorRegex.exec(content)) {
6655
- const name = expectDefined4(match[1]);
6695
+ const name = expectDefined5(match[1]);
6656
6696
  const offset = match.index ?? 0;
6657
6697
  const line = lineFromOffset(offset);
6658
6698
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6670,7 +6710,7 @@ function regexParse3(opts) {
6670
6710
  }
6671
6711
  const aliasRegex = /\*(\w[\w-]*)/g;
6672
6712
  for (let match = aliasRegex.exec(content); match !== null; match = aliasRegex.exec(content)) {
6673
- const name = expectDefined4(match[1]);
6713
+ const name = expectDefined5(match[1]);
6674
6714
  const offset = match.index ?? 0;
6675
6715
  const line = lineFromOffset(offset);
6676
6716
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6705,7 +6745,7 @@ function regexParse3(opts) {
6705
6745
  }
6706
6746
  const listItemRegex = /^-(\s+)([^:#\s][^:#\s]*)\s*:/gm;
6707
6747
  for (let match = listItemRegex.exec(content); match !== null; match = listItemRegex.exec(content)) {
6708
- const key = expectDefined4(match[2]);
6748
+ const key = expectDefined5(match[2]);
6709
6749
  const offset = match.index ?? 0;
6710
6750
  const line = lineFromOffset(offset);
6711
6751
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6725,7 +6765,7 @@ function regexParse3(opts) {
6725
6765
  }
6726
6766
  const blockScalarRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:\s*[|>](\s|$)/gm;
6727
6767
  for (let match = blockScalarRegex.exec(content); match !== null; match = blockScalarRegex.exec(content)) {
6728
- const key = expectDefined4(match[2]);
6768
+ const key = expectDefined5(match[2]);
6729
6769
  const offset = match.index ?? 0;
6730
6770
  const line = lineFromOffset(offset);
6731
6771
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -8454,9 +8494,50 @@ import { createReadStream } from "node:fs";
8454
8494
  import * as fs7 from "node:fs/promises";
8455
8495
  import * as path10 from "node:path";
8456
8496
  import { atomicWrite, ulid } from "@wrongstack/core/utils";
8497
+ var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
8498
+ var sweptRoots = /* @__PURE__ */ new Set();
8499
+ function sweepOldArtifacts(root) {
8500
+ if (sweptRoots.has(root)) return;
8501
+ sweptRoots.add(root);
8502
+ void (async () => {
8503
+ const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
8504
+ let sessionDirs;
8505
+ try {
8506
+ const entries = await fs7.readdir(root, { withFileTypes: true });
8507
+ sessionDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8508
+ } catch {
8509
+ return;
8510
+ }
8511
+ for (const sessionDir of sessionDirs) {
8512
+ const dir = path10.join(root, sessionDir);
8513
+ let names;
8514
+ try {
8515
+ names = await fs7.readdir(dir);
8516
+ } catch {
8517
+ continue;
8518
+ }
8519
+ let removed = 0;
8520
+ for (const name of names) {
8521
+ const target = path10.join(dir, name);
8522
+ try {
8523
+ const stat17 = await fs7.stat(target);
8524
+ if (stat17.isFile() && stat17.mtimeMs < cutoff) {
8525
+ await fs7.rm(target, { force: true });
8526
+ removed++;
8527
+ }
8528
+ } catch {
8529
+ }
8530
+ }
8531
+ if (removed === names.length && names.length > 0) {
8532
+ await fs7.rmdir(dir).catch(() => void 0);
8533
+ }
8534
+ }
8535
+ })();
8536
+ }
8457
8537
  var BrowserArtifactStore = class {
8458
8538
  constructor(root) {
8459
8539
  this.root = root;
8540
+ sweepOldArtifacts(root);
8460
8541
  }
8461
8542
  root;
8462
8543
  async write(sessionId, kind, extension, mimeType, content) {
@@ -9689,10 +9770,12 @@ var browserTools = [
9689
9770
  for (const tool of browserTools) tool.icon = "web";
9690
9771
  for (const tool of browserTools) tool.timeoutMs ??= 6e4;
9691
9772
 
9692
- // src/codebase-index/background-indexer.ts
9693
- import * as fs18 from "node:fs";
9694
- import { fileURLToPath as fileURLToPath3 } from "node:url";
9695
- import { Worker } from "node:worker_threads";
9773
+ // src/codebase-index/project-server-client.ts
9774
+ import { spawn as spawn4 } from "node:child_process";
9775
+ import * as fs12 from "node:fs";
9776
+ import * as net3 from "node:net";
9777
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
9778
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
9696
9779
 
9697
9780
  // src/codebase-index/circuit-breaker.ts
9698
9781
  var CircuitOpenError = class extends Error {
@@ -9774,153 +9857,18 @@ var IndexCircuitBreaker = class {
9774
9857
  };
9775
9858
  var indexCircuitBreaker = new IndexCircuitBreaker();
9776
9859
 
9777
- // src/codebase-index/indexer.ts
9778
- import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9779
- import { execFile as execFile2 } from "node:child_process";
9780
- import * as fs15 from "node:fs/promises";
9781
- import { availableParallelism } from "node:os";
9782
- import * as path22 from "node:path";
9783
- import {
9784
- DEFAULT_WALK_IGNORE_DIRS,
9785
- indexParallelBatchSize,
9786
- isFrugalPerf
9787
- } from "@wrongstack/core/utils";
9788
-
9789
- // src/codebase-index/gitignore.ts
9790
- import * as fs9 from "node:fs/promises";
9791
- import * as path13 from "node:path";
9792
- import { compileGlob } from "@wrongstack/core/utils";
9793
- function globBody(glob) {
9794
- return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
9795
- }
9796
- function compileGitignore(lines) {
9797
- const rules = [];
9798
- for (const raw of lines) {
9799
- let line = raw.replace(/\r$/, "");
9800
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
9801
- line = line.trim();
9802
- let negated = false;
9803
- if (line.startsWith("!")) {
9804
- negated = true;
9805
- line = line.slice(1);
9806
- }
9807
- let dirOnly = false;
9808
- if (line.endsWith("/")) {
9809
- dirOnly = true;
9810
- line = line.slice(0, -1);
9811
- }
9812
- if (!line) continue;
9813
- const anchored = line.startsWith("/") || line.includes("/");
9814
- if (line.startsWith("/")) line = line.slice(1);
9815
- const body = globBody(line);
9816
- const prefix = anchored ? "^" : "(?:^|.*/)";
9817
- rules.push({
9818
- eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
9819
- under: new RegExp(`${prefix}${body}/.*$`),
9820
- negated,
9821
- dirOnly
9822
- });
9823
- }
9824
- return (relPath, isDir) => {
9825
- const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
9826
- let ignored = false;
9827
- for (const r of rules) {
9828
- const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
9829
- if (re.test(p)) ignored = !r.negated;
9830
- }
9831
- return ignored;
9832
- };
9833
- }
9834
- async function loadGitignoreMatcher(projectRoot) {
9835
- let lines = [];
9836
- try {
9837
- const raw = await fs9.readFile(path13.join(projectRoot, ".gitignore"), "utf8");
9838
- lines = raw.split("\n");
9839
- } catch {
9840
- }
9841
- return compileGitignore(lines);
9842
- }
9843
-
9844
- // src/codebase-index/indexer.ts
9845
- init_languages2();
9846
-
9847
- // src/codebase-index/parser-dispatch.ts
9848
- async function parseFileContent(file, content, lang) {
9849
- switch (lang) {
9850
- case "ts":
9851
- case "tsx":
9852
- case "js":
9853
- case "jsx": {
9854
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
9855
- return parseSymbols8({ file, content, lang });
9856
- }
9857
- case "go": {
9858
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
9859
- return parseSymbols8({ file, content, lang: "go" });
9860
- }
9861
- case "py": {
9862
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
9863
- return parseSymbols8({ file, content, lang: "py" });
9864
- }
9865
- case "rs": {
9866
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
9867
- return parseSymbols8({ file, content, lang: "rs" });
9868
- }
9869
- case "json": {
9870
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
9871
- return parseSymbols8({ file, content, lang: "json" });
9872
- }
9873
- case "yaml": {
9874
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
9875
- return parseSymbols8({ file, content, lang: "yaml" });
9876
- }
9877
- default: {
9878
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
9879
- return parseSymbols8({ file, content, lang });
9880
- }
9881
- }
9882
- }
9860
+ // src/codebase-index/project-server-endpoint.ts
9861
+ import { createHash as createHash4 } from "node:crypto";
9862
+ import * as fs11 from "node:fs";
9863
+ import * as os5 from "node:os";
9864
+ import * as path16 from "node:path";
9865
+ import { fileURLToPath } from "node:url";
9866
+ import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
9883
9867
 
9884
9868
  // src/codebase-index/writer.ts
9885
- import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
9886
- import * as fs14 from "node:fs";
9887
- import * as path21 from "node:path";
9888
-
9889
- // src/codebase-index/schema.ts
9890
- var SCHEMA_VERSION = 3;
9891
-
9892
- // src/codebase-index/lsp-kind.ts
9893
- function lspKindToInternalKind(k) {
9894
- switch (k) {
9895
- case 5 /* Class */:
9896
- return "class";
9897
- case 6 /* Method */:
9898
- return "method";
9899
- case 7 /* Property */:
9900
- case 8 /* Field */:
9901
- return "property";
9902
- case 9 /* Constructor */:
9903
- return "class";
9904
- case 10 /* Enum */:
9905
- return "enum";
9906
- case 11 /* Interface */:
9907
- return "interface";
9908
- case 12 /* Function */:
9909
- return "function";
9910
- case 13 /* Variable */:
9911
- return "var";
9912
- case 14 /* Constant */:
9913
- return "const";
9914
- case 22 /* EnumMember */:
9915
- return "enum";
9916
- case 26 /* TypeParameter */:
9917
- return "type";
9918
- case 3 /* Namespace */:
9919
- return "namespace";
9920
- default:
9921
- return null;
9922
- }
9923
- }
9869
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
9870
+ import * as fs10 from "node:fs";
9871
+ import * as path15 from "node:path";
9924
9872
 
9925
9873
  // src/codebase-index/bm25.ts
9926
9874
  var K1 = 1.5;
@@ -10011,6 +9959,42 @@ var Bm25Index = class {
10011
9959
  }
10012
9960
  };
10013
9961
 
9962
+ // src/codebase-index/lsp-kind.ts
9963
+ function lspKindToInternalKind(k) {
9964
+ switch (k) {
9965
+ case 5 /* Class */:
9966
+ return "class";
9967
+ case 6 /* Method */:
9968
+ return "method";
9969
+ case 7 /* Property */:
9970
+ case 8 /* Field */:
9971
+ return "property";
9972
+ case 9 /* Constructor */:
9973
+ return "class";
9974
+ case 10 /* Enum */:
9975
+ return "enum";
9976
+ case 11 /* Interface */:
9977
+ return "interface";
9978
+ case 12 /* Function */:
9979
+ return "function";
9980
+ case 13 /* Variable */:
9981
+ return "var";
9982
+ case 14 /* Constant */:
9983
+ return "const";
9984
+ case 22 /* EnumMember */:
9985
+ return "enum";
9986
+ case 26 /* TypeParameter */:
9987
+ return "type";
9988
+ case 3 /* Namespace */:
9989
+ return "namespace";
9990
+ default:
9991
+ return null;
9992
+ }
9993
+ }
9994
+
9995
+ // src/codebase-index/schema.ts
9996
+ var SCHEMA_VERSION = 3;
9997
+
10014
9998
  // src/codebase-index/sqlite-runtime.ts
10015
9999
  import { createRequire } from "node:module";
10016
10000
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
@@ -10078,99 +10062,9 @@ function runSqliteWithRetry(fn) {
10078
10062
  throw lastError;
10079
10063
  }
10080
10064
 
10081
- // src/codebase-index/writer-helpers.ts
10082
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
10083
- function escapeLike(value) {
10084
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
10085
- }
10086
- function assignRefsToSymbols(refs, symbols) {
10087
- if (refs.length === 0 || symbols.length === 0) return [];
10088
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10089
- const seen = /* @__PURE__ */ new Set();
10090
- const assigned = [];
10091
- for (const ref of refs) {
10092
- let owner2;
10093
- for (const symbol of ordered) {
10094
- if (symbol.line > ref.line) break;
10095
- owner2 = symbol;
10096
- }
10097
- if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10098
- if (!owner2 || owner2.id <= 0) continue;
10099
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10100
- if (seen.has(key)) continue;
10101
- seen.add(key);
10102
- assigned.push({ ...ref, fromId: owner2.id });
10103
- }
10104
- return assigned;
10105
- }
10106
- function resolveIndexDir(projectRoot, override) {
10107
- return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
10108
- }
10109
- function codebaseIndexDirOverride(ctx) {
10110
- const v = ctx.meta?.["codebaseIndexDir"];
10111
- return typeof v === "string" ? v : void 0;
10112
- }
10113
-
10114
- // src/codebase-index/writer-schema.ts
10115
- var METADATA_TABLE_SQL = `
10116
- CREATE TABLE IF NOT EXISTS metadata (
10117
- key TEXT PRIMARY KEY,
10118
- value TEXT NOT NULL
10119
- );
10120
- `;
10121
- var CORE_TABLES_SQL = `
10122
- CREATE TABLE IF NOT EXISTS files (
10123
- file TEXT PRIMARY KEY,
10124
- lang TEXT NOT NULL,
10125
- mtime_ms INTEGER NOT NULL,
10126
- symbol_count INTEGER NOT NULL DEFAULT 0,
10127
- last_indexed INTEGER NOT NULL
10128
- );
10129
- CREATE TABLE IF NOT EXISTS symbols (
10130
- id INTEGER PRIMARY KEY,
10131
- lang TEXT NOT NULL,
10132
- kind TEXT NOT NULL,
10133
- name TEXT NOT NULL,
10134
- file TEXT NOT NULL,
10135
- line INTEGER NOT NULL,
10136
- col INTEGER NOT NULL,
10137
- signature TEXT NOT NULL DEFAULT '',
10138
- doc_comment TEXT NOT NULL DEFAULT '',
10139
- scope TEXT NOT NULL DEFAULT '',
10140
- text TEXT NOT NULL DEFAULT '',
10141
- file_fk TEXT NOT NULL
10142
- );
10143
- `;
10144
- var SYMBOL_INDEX_SQL = [
10145
- "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10146
- "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
10147
- "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
10148
- "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
10149
- "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
10150
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
10151
- "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
10152
- ];
10153
- var REFS_TABLE_SQL = `
10154
- CREATE TABLE IF NOT EXISTS refs (
10155
- id INTEGER PRIMARY KEY,
10156
- from_id INTEGER NOT NULL,
10157
- to_name TEXT NOT NULL,
10158
- to_id INTEGER,
10159
- call_type TEXT NOT NULL,
10160
- line INTEGER NOT NULL
10161
- );
10162
- `;
10163
- var REFS_INDEX_SQL = [
10164
- "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10165
- "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10166
- "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10167
- "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
10168
- ];
10169
- var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10170
-
10171
10065
  // src/codebase-index/writer-admin.ts
10172
- import * as fs13 from "node:fs";
10173
- import * as path19 from "node:path";
10066
+ import * as fs9 from "node:fs";
10067
+ import * as path13 from "node:path";
10174
10068
  var DB_FILE = "index.db";
10175
10069
  function getAllIndexableWithStatement(stmt) {
10176
10070
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10229,7 +10123,7 @@ function getAllFileMetasWithStatement(stmt) {
10229
10123
  }
10230
10124
  function getIndexDbSizeBytes(indexDir) {
10231
10125
  try {
10232
- return fs13.statSync(path19.join(indexDir, DB_FILE)).size;
10126
+ return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10233
10127
  } catch {
10234
10128
  return 0;
10235
10129
  }
@@ -10296,7 +10190,7 @@ function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10296
10190
  }
10297
10191
 
10298
10192
  // src/codebase-index/writer-graph-helpers.ts
10299
- import * as path20 from "node:path";
10193
+ import * as path14 from "node:path";
10300
10194
  function derivePackage(filePath) {
10301
10195
  const f = filePath.replace(/\\/g, "/");
10302
10196
  const pkgsIdx = f.indexOf("/packages/");
@@ -10411,16 +10305,16 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10411
10305
  function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10412
10306
  if (!moduleName.startsWith(".")) return void 0;
10413
10307
  const normalizedFrom = fromFile.replace(/\\/g, "/");
10414
- const absolute = path20.posix.normalize(
10415
- path20.posix.join(path20.posix.dirname(normalizedFrom), moduleName)
10308
+ const absolute = path14.posix.normalize(
10309
+ path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10416
10310
  );
10417
- const extension = path20.posix.extname(absolute);
10311
+ const extension = path14.posix.extname(absolute);
10418
10312
  const base = extension ? absolute.slice(0, -extension.length) : absolute;
10419
10313
  const candidates = [
10420
10314
  absolute,
10421
10315
  ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10422
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(absolute, `index${ext}`)),
10423
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(base, `index${ext}`))
10316
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10317
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10424
10318
  ];
10425
10319
  const indexedByPortablePath = new Map(
10426
10320
  [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
@@ -10640,6 +10534,39 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
10640
10534
  return { nodes, edges };
10641
10535
  }
10642
10536
 
10537
+ // src/codebase-index/writer-helpers.ts
10538
+ import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
10539
+ function escapeLike(value) {
10540
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
10541
+ }
10542
+ function assignRefsToSymbols(refs, symbols) {
10543
+ if (refs.length === 0 || symbols.length === 0) return [];
10544
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10545
+ const seen = /* @__PURE__ */ new Set();
10546
+ const assigned = [];
10547
+ for (const ref of refs) {
10548
+ let owner2;
10549
+ for (const symbol of ordered) {
10550
+ if (symbol.line > ref.line) break;
10551
+ owner2 = symbol;
10552
+ }
10553
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10554
+ if (!owner2 || owner2.id <= 0) continue;
10555
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10556
+ if (seen.has(key)) continue;
10557
+ seen.add(key);
10558
+ assigned.push({ ...ref, fromId: owner2.id });
10559
+ }
10560
+ return assigned;
10561
+ }
10562
+ function resolveIndexDir(projectRoot, override) {
10563
+ return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
10564
+ }
10565
+ function codebaseIndexDirOverride(ctx) {
10566
+ const v = ctx.meta?.["codebaseIndexDir"];
10567
+ return typeof v === "string" ? v : void 0;
10568
+ }
10569
+
10643
10570
  // src/codebase-index/writer-pragmas.ts
10644
10571
  import { sqliteCachePragmas } from "@wrongstack/core/utils";
10645
10572
  function applyIndexStorePragmas(db) {
@@ -10658,6 +10585,63 @@ function applyIndexStorePragmas(db) {
10658
10585
  }
10659
10586
  }
10660
10587
 
10588
+ // src/codebase-index/writer-schema.ts
10589
+ var METADATA_TABLE_SQL = `
10590
+ CREATE TABLE IF NOT EXISTS metadata (
10591
+ key TEXT PRIMARY KEY,
10592
+ value TEXT NOT NULL
10593
+ );
10594
+ `;
10595
+ var CORE_TABLES_SQL = `
10596
+ CREATE TABLE IF NOT EXISTS files (
10597
+ file TEXT PRIMARY KEY,
10598
+ lang TEXT NOT NULL,
10599
+ mtime_ms INTEGER NOT NULL,
10600
+ symbol_count INTEGER NOT NULL DEFAULT 0,
10601
+ last_indexed INTEGER NOT NULL
10602
+ );
10603
+ CREATE TABLE IF NOT EXISTS symbols (
10604
+ id INTEGER PRIMARY KEY,
10605
+ lang TEXT NOT NULL,
10606
+ kind TEXT NOT NULL,
10607
+ name TEXT NOT NULL,
10608
+ file TEXT NOT NULL,
10609
+ line INTEGER NOT NULL,
10610
+ col INTEGER NOT NULL,
10611
+ signature TEXT NOT NULL DEFAULT '',
10612
+ doc_comment TEXT NOT NULL DEFAULT '',
10613
+ scope TEXT NOT NULL DEFAULT '',
10614
+ text TEXT NOT NULL DEFAULT '',
10615
+ file_fk TEXT NOT NULL
10616
+ );
10617
+ `;
10618
+ var SYMBOL_INDEX_SQL = [
10619
+ "CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
10620
+ "CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
10621
+ "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
10622
+ "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
10623
+ "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
10624
+ "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
10625
+ "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
10626
+ ];
10627
+ var REFS_TABLE_SQL = `
10628
+ CREATE TABLE IF NOT EXISTS refs (
10629
+ id INTEGER PRIMARY KEY,
10630
+ from_id INTEGER NOT NULL,
10631
+ to_name TEXT NOT NULL,
10632
+ to_id INTEGER,
10633
+ call_type TEXT NOT NULL,
10634
+ line INTEGER NOT NULL
10635
+ );
10636
+ `;
10637
+ var REFS_INDEX_SQL = [
10638
+ "CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
10639
+ "CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
10640
+ "CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
10641
+ "CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
10642
+ ];
10643
+ var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10644
+
10661
10645
  // src/codebase-index/writer-search-helpers.ts
10662
10646
  function normalizeSearchLimit(limit) {
10663
10647
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
@@ -10846,9 +10830,9 @@ var IndexStore = class _IndexStore {
10846
10830
  }
10847
10831
  constructor(projectRoot, opts = {}) {
10848
10832
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
10849
- fs14.mkdirSync(this.indexDir, { recursive: true });
10833
+ fs10.mkdirSync(this.indexDir, { recursive: true });
10850
10834
  const Database = loadDatabaseSync();
10851
- this.db = new Database(path21.join(this.indexDir, DB_FILE2));
10835
+ this.db = new Database(path15.join(this.indexDir, DB_FILE2));
10852
10836
  applyIndexStorePragmas(this.db);
10853
10837
  this.initSchema();
10854
10838
  }
@@ -10866,9 +10850,15 @@ var IndexStore = class _IndexStore {
10866
10850
  DROP TABLE IF EXISTS refs;
10867
10851
  `);
10868
10852
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
10869
- this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
10853
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(
10854
+ String(SCHEMA_VERSION),
10855
+ "version"
10856
+ );
10870
10857
  } else if (storedVersion === null) {
10871
- this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
10858
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run(
10859
+ "version",
10860
+ String(SCHEMA_VERSION)
10861
+ );
10872
10862
  }
10873
10863
  this.db.exec(CORE_TABLES_SQL);
10874
10864
  for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
@@ -10885,7 +10875,9 @@ var IndexStore = class _IndexStore {
10885
10875
  );
10886
10876
  if (symbolCount !== ftsCount) {
10887
10877
  this.db.exec("DELETE FROM symbols_fts");
10888
- const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
10878
+ const rows = this.stmt(
10879
+ "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
10880
+ ).all();
10889
10881
  bulkInsertFtsWithStatement(
10890
10882
  (sql) => this.stmt(sql),
10891
10883
  _IndexStore.MAX_SQL_VARS,
@@ -11107,7 +11099,9 @@ var IndexStore = class _IndexStore {
11107
11099
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
11108
11100
  const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
11109
11101
  const binds = limit !== void 0 ? [...values, limit] : values;
11110
- const rows = this.stmt(sql).all(...binds);
11102
+ const rows = this.stmt(sql).all(
11103
+ ...binds
11104
+ );
11111
11105
  return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
11112
11106
  }
11113
11107
  /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
@@ -11253,13 +11247,13 @@ var IndexStore = class _IndexStore {
11253
11247
  if (rankDiff !== 0) return rankDiff;
11254
11248
  const scoreDiff = b.score - a.score;
11255
11249
  if (scoreDiff !== 0) return scoreDiff;
11256
- const left = expectDefined5(candidateById.get(a.id));
11257
- const right = expectDefined5(candidateById.get(b.id));
11250
+ const left = expectDefined2(candidateById.get(a.id));
11251
+ const right = expectDefined2(candidateById.get(b.id));
11258
11252
  return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
11259
11253
  });
11260
11254
  const qTokens = tokenise(query);
11261
11255
  const results = scored.slice(0, limit).map(({ id, score }) => {
11262
- const c = expectDefined5(candidateById.get(id));
11256
+ const c = expectDefined2(candidateById.get(id));
11263
11257
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
11264
11258
  });
11265
11259
  return { results, total: candidates.length };
@@ -11283,7 +11277,9 @@ var IndexStore = class _IndexStore {
11283
11277
  }
11284
11278
  setLastIndexed(ts2) {
11285
11279
  this.runWithRetry(() => {
11286
- this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
11280
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
11281
+ String(ts2)
11282
+ );
11287
11283
  });
11288
11284
  }
11289
11285
  getMetadata(key) {
@@ -11394,7 +11390,9 @@ var IndexStore = class _IndexStore {
11394
11390
  this.stmt(
11395
11391
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
11396
11392
  ).run(...options.deleteForFiles);
11397
- this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
11393
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(
11394
+ ...options.deleteForFiles
11395
+ );
11398
11396
  }
11399
11397
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
11400
11398
  let nextId = this.allocateSymbolIds(totalSymbols);
@@ -11438,11 +11436,7 @@ var IndexStore = class _IndexStore {
11438
11436
  this.ftsAvailable,
11439
11437
  ftsRows
11440
11438
  );
11441
- bulkInsertRefsWithStatement(
11442
- (sql) => this.stmt(sql),
11443
- _IndexStore.MAX_SQL_VARS,
11444
- refsToInsert
11445
- );
11439
+ bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
11446
11440
  const upsertStmt = this.stmt(
11447
11441
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
11448
11442
  VALUES (?, ?, ?, ?, ?)
@@ -11471,7 +11465,9 @@ var IndexStore = class _IndexStore {
11471
11465
  */
11472
11466
  deleteRefsForFile(file) {
11473
11467
  this.runWithRetry(() => {
11474
- this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
11468
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
11469
+ file
11470
+ );
11475
11471
  });
11476
11472
  }
11477
11473
  /**
@@ -11626,9 +11622,7 @@ var IndexStore = class _IndexStore {
11626
11622
  * build the full symbol universe for the reachability scan.
11627
11623
  */
11628
11624
  getAllSymbols() {
11629
- return this.stmt(
11630
- "SELECT id, name, file, kind, line FROM symbols ORDER BY id"
11631
- ).all().map((r) => ({ ...r, kind: r.kind }));
11625
+ return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
11632
11626
  }
11633
11627
  /**
11634
11628
  * Returns every resolved reference (to_id IS NOT NULL). Used by
@@ -11640,6 +11634,25 @@ var IndexStore = class _IndexStore {
11640
11634
  "SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
11641
11635
  ).all();
11642
11636
  }
11637
+ /**
11638
+ * Returns ALL import refs (including unresolved) with their source-file
11639
+ * path and resolved target id. Used by the dead-code scan's file-level
11640
+ * graph traversal to handle barrel-only entry points where no symbol
11641
+ * carries the ref.
11642
+ *
11643
+ * Refs whose `from_id` doesn't match a known symbol (e.g. pure-barrel
11644
+ * files with no declarations) will have `sourceFile === null`.
11645
+ */
11646
+ getAllImportRefs() {
11647
+ return this.stmt(
11648
+ `SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
11649
+ r.call_type AS callType, r.line
11650
+ FROM refs r
11651
+ LEFT JOIN symbols s ON r.from_id = s.id
11652
+ WHERE r.call_type = 'import'
11653
+ ORDER BY r.line`
11654
+ ).all();
11655
+ }
11643
11656
  close() {
11644
11657
  this.stmtCache.clear();
11645
11658
  this.bm25Dirty = true;
@@ -11654,1134 +11667,1247 @@ var indexStorePool = new StorePool(
11654
11667
  (projectRoot, opts) => new IndexStore(projectRoot, opts)
11655
11668
  );
11656
11669
 
11657
- // src/codebase-index/indexer.ts
11658
- var YIELD_EVERY_N = 50;
11659
- function resolveParallelBatch() {
11660
- return indexParallelBatchSize(availableParallelism());
11661
- }
11662
- function yieldEventLoop() {
11663
- return new Promise((resolve16) => setImmediate(resolve16));
11670
+ // src/codebase-index/project-server-endpoint.ts
11671
+ var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
11672
+ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
11673
+ var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
11674
+ var buildIdCache;
11675
+ function projectIndexServerBuildId(entrypoint) {
11676
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
11677
+ try {
11678
+ const stat17 = fs11.statSync(file);
11679
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat17.mtimeMs && buildIdCache.size === stat17.size) {
11680
+ return buildIdCache.buildId;
11681
+ }
11682
+ const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
11683
+ buildIdCache = { file, mtimeMs: stat17.mtimeMs, size: stat17.size, buildId };
11684
+ return buildId;
11685
+ } catch {
11686
+ return `unreadable:${path16.basename(file)}`;
11687
+ }
11664
11688
  }
11665
- function throwIfAborted(signal) {
11666
- if (!signal?.aborted) return;
11667
- if (signal.reason instanceof Error) throw signal.reason;
11668
- throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
11689
+ function normalizeLocalPath(value) {
11690
+ const resolved = path16.resolve(value);
11691
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11669
11692
  }
11670
- function isAbortError(err) {
11671
- return err instanceof DOMException && err.name === "AbortError";
11693
+ function projectIndexServerKey(projectRoot, indexDir) {
11694
+ const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
11695
+ return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
11672
11696
  }
11673
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
11674
- var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
11675
- var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
11676
- var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
11677
- var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
11678
- function isWithinProject(projectRoot, file) {
11679
- const rel = path22.relative(projectRoot, file);
11680
- return rel !== "" && !rel.startsWith(`..${path22.sep}`) && rel !== ".." && !path22.isAbsolute(rel);
11697
+ function projectIndexServerEndpoint(projectRoot, indexDir) {
11698
+ const key = projectIndexServerKey(projectRoot, indexDir);
11699
+ if (process.platform === "win32") {
11700
+ return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
11701
+ }
11702
+ return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
11681
11703
  }
11682
- function isMissingPathError(err) {
11683
- const code = err?.code;
11684
- return code === "ENOENT" || code === "ENOTDIR";
11704
+ function projectIndexServerMetadataPath(projectRoot, indexDir) {
11705
+ return path16.join(
11706
+ path16.resolve(resolveIndexDir(projectRoot, indexDir)),
11707
+ PROJECT_INDEX_SERVER_METADATA_FILE
11708
+ );
11685
11709
  }
11686
- function normalizeComparablePath(value) {
11687
- const resolved = path22.resolve(value);
11688
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11710
+
11711
+ // src/codebase-index/project-server-protocol.ts
11712
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
11713
+ function encodeProjectServerMessage(message) {
11714
+ return `${JSON.stringify(message)}
11715
+ `;
11689
11716
  }
11690
- function gitOutput(projectRoot, args) {
11691
- return new Promise((resolve16, reject) => {
11692
- execFile2(
11693
- "git",
11694
- ["-C", projectRoot, ...args],
11695
- {
11696
- encoding: "buffer",
11697
- maxBuffer: MAX_GIT_FILE_LIST_BYTES,
11698
- windowsHide: true
11699
- },
11700
- (error, stdout) => {
11701
- if (error) reject(error);
11702
- else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
11703
- }
11704
- );
11705
- });
11706
- }
11707
- async function findGitSourceFiles(projectRoot, ignore, signal) {
11708
- try {
11709
- throwIfAborted(signal);
11710
- const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
11711
- if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
11712
- throwIfAborted(signal);
11713
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
11714
- const [output, statusOutput] = await Promise.all([
11715
- gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
11716
- gitOutput(projectRoot, [
11717
- "status",
11718
- "--porcelain=v1",
11719
- "-z",
11720
- "--untracked-files=all",
11721
- "--ignored=no"
11722
- ])
11723
- ]);
11724
- throwIfAborted(signal);
11725
- const dirty = /* @__PURE__ */ new Set();
11726
- const deleted = /* @__PURE__ */ new Set();
11727
- const statusRecords = statusOutput.toString("utf8").split("\0");
11728
- for (let i = 0; i < statusRecords.length; i++) {
11729
- const record = statusRecords[i];
11730
- if (!record) continue;
11731
- const status = record.slice(0, 2);
11732
- const changedPath = path22.resolve(projectRoot, record.slice(3));
11733
- dirty.add(changedPath);
11734
- if (status.includes("D")) deleted.add(changedPath);
11735
- if (status.includes("R") || status.includes("C")) {
11736
- const source = statusRecords[++i];
11737
- if (source) dirty.add(path22.resolve(projectRoot, source));
11717
+
11718
+ // src/codebase-index/project-server-client.ts
11719
+ var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
11720
+ var SERVER_START_TIMEOUT_MS = 1e4;
11721
+ var SERVER_CONTROL_TIMEOUT_MS = 5e3;
11722
+ var SERVER_HEALTH_TIMEOUT_MS = 3e3;
11723
+ var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
11724
+ var StaleProjectIndexServerError = class extends Error {
11725
+ constructor(message, pid) {
11726
+ super(message);
11727
+ this.pid = pid;
11728
+ }
11729
+ pid;
11730
+ name = "StaleProjectIndexServerError";
11731
+ };
11732
+ var connectionStates = /* @__PURE__ */ new Map();
11733
+ var connectionStateListeners = /* @__PURE__ */ new Set();
11734
+ var latestConnectionState = {
11735
+ status: "offline",
11736
+ connected: false
11737
+ };
11738
+ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
11739
+ if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
11740
+ return { kind: "inline-requested" };
11741
+ }
11742
+ let builtUrl = null;
11743
+ for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
11744
+ try {
11745
+ const url = new URL(rel, import.meta.url);
11746
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
11747
+ builtUrl = url;
11748
+ break;
11738
11749
  }
11750
+ } catch {
11739
11751
  }
11740
- const files = [];
11741
- for (const relative12 of output.toString("utf8").split("\0")) {
11742
- if (!relative12) continue;
11743
- const portable = relative12.replace(/\\/g, "/");
11744
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path22.posix.basename(portable))) {
11745
- continue;
11746
- }
11747
- const full = path22.resolve(projectRoot, relative12);
11748
- if (deleted.has(full)) continue;
11749
- const ext = path22.extname(relative12).toLowerCase();
11750
- if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
11752
+ }
11753
+ if (builtUrl === null) return { kind: "missing-build" };
11754
+ if (projectRoot !== void 0) {
11755
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
11756
+ const check = checkUnixSocketPath(endpoint);
11757
+ if (!check.ok) {
11758
+ return {
11759
+ kind: "endpoint-invalid",
11760
+ endpoint,
11761
+ byteLength: check.byteLength,
11762
+ maxBytes: check.maxBytes
11763
+ };
11751
11764
  }
11752
- return {
11753
- files,
11754
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
11755
- };
11756
- } catch {
11757
- return null;
11758
11765
  }
11766
+ return { kind: "available", url: builtUrl };
11759
11767
  }
11760
- async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
11761
- const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
11762
- if (gitFiles) {
11768
+ function resolveProjectServerUrl() {
11769
+ const availability = resolveProjectIndexDaemonAvailability();
11770
+ return availability.kind === "available" ? availability.url : null;
11771
+ }
11772
+ function projectIndexServerExpectedBuildId() {
11773
+ const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
11774
+ if (override) return override;
11775
+ const url = resolveProjectServerUrl();
11776
+ return url ? projectIndexServerBuildId(url) : null;
11777
+ }
11778
+ function isProjectIndexServerAvailable() {
11779
+ return resolveProjectServerUrl() !== null;
11780
+ }
11781
+ function publishConnectionState(endpoint, state) {
11782
+ connectionStates.set(endpoint, state);
11783
+ latestConnectionState = state;
11784
+ for (const listener of connectionStateListeners) listener(state);
11785
+ }
11786
+ function getProjectIndexServerConnectionState(projectRoot, indexDir) {
11787
+ if (projectRoot) {
11788
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
11789
+ const existing = connectionStates.get(endpoint);
11790
+ if (existing) return existing;
11791
+ if (!isProjectIndexServerAvailable()) {
11792
+ return { status: "unavailable", connected: false };
11793
+ }
11763
11794
  return {
11764
- files: gitFiles.files,
11765
- complete: true,
11766
- errors: [],
11767
- trustedUnchanged: gitFiles.trustedUnchanged
11795
+ status: "offline",
11796
+ connected: false,
11797
+ projectRoot,
11798
+ indexDir,
11799
+ endpoint
11768
11800
  };
11769
11801
  }
11770
- const results = [];
11771
- const errors = [];
11772
- let complete = true;
11773
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
11774
- const indexableExts = new Set(INDEXABLE_EXTENSIONS);
11775
- let dirCount = 0;
11776
- const walk = async (dir) => {
11777
- throwIfAborted(signal);
11778
- if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
11779
- await yieldEventLoop();
11780
- throwIfAborted(signal);
11781
- }
11782
- let entries;
11783
- try {
11784
- entries = await fs15.readdir(dir, { withFileTypes: true });
11785
- } catch (err) {
11786
- complete = false;
11787
- errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
11788
- return;
11789
- }
11790
- dirCount++;
11791
- for (const e of entries) {
11792
- if (ignoreSet.has(e.name)) continue;
11793
- const full = path22.join(dir, e.name);
11794
- const rel = path22.relative(projectRoot, full).replace(/\\/g, "/");
11795
- if (e.isDirectory()) {
11796
- if (isGitIgnored(rel, true)) continue;
11797
- await walk(full);
11798
- } else if (e.isFile()) {
11799
- if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
11800
- const ext = path22.extname(e.name).toLowerCase();
11801
- if (indexableExts.has(ext) || detectLang(full) !== null) {
11802
- results.push(full);
11803
- }
11804
- }
11805
- }
11806
- };
11807
- await walk(projectRoot);
11808
- return { files: results, complete, errors };
11802
+ if (latestConnectionState.endpoint) return latestConnectionState;
11803
+ if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
11804
+ return latestConnectionState;
11809
11805
  }
11810
- function assignRefsToSymbols2(refs, symbols) {
11811
- if (refs.length === 0 || symbols.length === 0) return [];
11812
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
11813
- const seen = /* @__PURE__ */ new Set();
11814
- const assigned = [];
11815
- for (const ref of refs) {
11816
- let owner2;
11817
- for (const symbol of ordered) {
11818
- if (symbol.line > ref.line) break;
11819
- owner2 = symbol;
11820
- }
11821
- if (!owner2 && ref.callType === "import") owner2 = ordered[0];
11822
- if (!owner2 || owner2.id <= 0) continue;
11823
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
11824
- if (seen.has(key)) continue;
11825
- seen.add(key);
11826
- assigned.push({ ...ref, fromId: owner2.id });
11827
- }
11828
- return assigned;
11806
+ function onProjectIndexServerConnectionStateChange(listener) {
11807
+ connectionStateListeners.add(listener);
11808
+ return () => connectionStateListeners.delete(listener);
11829
11809
  }
11830
- async function runIndexerWithStore(store, opts) {
11831
- const { projectRoot, langs, ignore = [], signal } = opts;
11832
- const relationGraphVersion = "2";
11833
- const refResolutionVersion = "2";
11834
- const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
11835
- const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
11836
- const startMs = Date.now();
11837
- const errors = [];
11838
- const langStats = {};
11839
- let filesIndexed = 0;
11840
- let symbolsIndexed = 0;
11841
- const isGitIgnored = await loadGitignoreMatcher(projectRoot);
11842
- let files;
11843
- let discoveredFiles = null;
11844
- let discoveryComplete = true;
11845
- let trustedUnchanged;
11846
- if (opts.files && opts.files.length > 0) {
11847
- files = opts.files.map((f) => path22.resolve(projectRoot, f)).filter((f) => {
11848
- if (!isWithinProject(projectRoot, f)) return false;
11849
- const rel = path22.relative(projectRoot, f).replace(/\\/g, "/");
11850
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path22.basename(f)) && !isGitIgnored(rel, false);
11851
- });
11852
- } else {
11853
- const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
11854
- files = discovery.files;
11855
- errors.push(...discovery.errors);
11856
- discoveryComplete = discovery.complete;
11857
- discoveredFiles = new Set(files);
11858
- trustedUnchanged = discovery.trustedUnchanged;
11859
- }
11860
- if (langs && langs.length > 0) {
11861
- const langSet = new Set(langs);
11862
- files = files.filter((f) => {
11863
- const lang = detectLang(f);
11864
- return lang ? langSet.has(lang) : false;
11865
- });
11866
- }
11867
- if (force) store.clearAll();
11868
- const existingMeta = /* @__PURE__ */ new Map();
11869
- if (!force) {
11870
- for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
11810
+ function remoteError(message, name) {
11811
+ if (name === "LockError") return new LockError(message);
11812
+ if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
11813
+ const error = new Error(message);
11814
+ if (name && name !== "Error") error.name = name;
11815
+ return error;
11816
+ }
11817
+ function isProjectIndexServerHealth(value) {
11818
+ if (!value || typeof value !== "object") return false;
11819
+ const health = value;
11820
+ const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
11821
+ const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
11822
+ return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
11823
+ }
11824
+ function delay(ms) {
11825
+ return new Promise((resolve16) => {
11826
+ const timer = setTimeout(resolve16, ms);
11827
+ timer.unref?.();
11828
+ });
11829
+ }
11830
+ function cancellationError(signal) {
11831
+ return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
11832
+ }
11833
+ var ProjectServerConnection = class {
11834
+ constructor(projectRoot, indexDir, endpoint) {
11835
+ this.projectRoot = projectRoot;
11836
+ this.indexDir = indexDir;
11837
+ this.endpoint = endpoint;
11838
+ this.transition("offline");
11871
11839
  }
11872
- const totalFilesForProgress = files.length;
11873
- let filesPreSkipped = 0;
11874
- if (!force && trustedUnchanged) {
11875
- files = files.filter((file) => {
11876
- const meta = existingMeta.get(file);
11877
- if (!meta || !trustedUnchanged.has(file)) return true;
11878
- langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
11879
- symbolsIndexed += meta.symbolCount;
11880
- filesIndexed++;
11881
- filesPreSkipped++;
11882
- return false;
11840
+ projectRoot;
11841
+ indexDir;
11842
+ endpoint;
11843
+ socket = null;
11844
+ buffer = "";
11845
+ info = null;
11846
+ activity = null;
11847
+ health = null;
11848
+ healthCheck = null;
11849
+ connecting = null;
11850
+ connectResolve = null;
11851
+ connectReject = null;
11852
+ nextId = 1;
11853
+ pending = /* @__PURE__ */ new Map();
11854
+ transition(status, options = {}) {
11855
+ const previous = connectionStates.get(this.endpoint);
11856
+ const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
11857
+ const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
11858
+ publishConnectionState(this.endpoint, {
11859
+ status,
11860
+ connected: status === "connected" || status === "degraded" || status === "unresponsive",
11861
+ projectRoot: this.projectRoot,
11862
+ indexDir: this.indexDir,
11863
+ endpoint: this.endpoint,
11864
+ pid,
11865
+ lastError,
11866
+ ...this.activity ? { activity: this.activity } : {},
11867
+ ...this.health ? { health: this.health } : {}
11883
11868
  });
11884
- if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
11885
11869
  }
11886
- const parallelBatch = resolveParallelBatch();
11887
- let filesSinceLastYield = 0;
11888
- for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11889
- const batchEnd = Math.min(batchStart + parallelBatch, files.length);
11890
- const batchFiles = files.slice(batchStart, batchEnd);
11891
- opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
11892
- filesSinceLastYield += batchFiles.length;
11893
- if (filesSinceLastYield >= YIELD_EVERY_N) {
11894
- filesSinceLastYield = 0;
11895
- await yieldEventLoop();
11896
- if (isFrugalPerf()) {
11897
- await new Promise((r) => setTimeout(r, 8));
11870
+ isConnected() {
11871
+ return this.socket !== null && !this.socket.destroyed && this.info !== null;
11872
+ }
11873
+ async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
11874
+ await this.ensureConnected(spawnIfMissing);
11875
+ if (this.healthCheck) return this.healthCheck;
11876
+ const startedAt = Date.now();
11877
+ this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
11878
+ const now = Date.now();
11879
+ this.health = {
11880
+ status: "healthy",
11881
+ checkedAt: now,
11882
+ lastHealthyAt: now,
11883
+ latencyMs: Math.max(0, now - startedAt),
11884
+ missedHeartbeats: 0,
11885
+ ...isProjectIndexServerHealth(server) ? { server } : {}
11886
+ };
11887
+ this.transition("connected", { pid: this.info?.pid });
11888
+ return this.health;
11889
+ }).catch((error) => {
11890
+ if (!this.isConnected()) throw error;
11891
+ if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
11892
+ const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
11893
+ const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
11894
+ this.health = {
11895
+ status,
11896
+ checkedAt: Date.now(),
11897
+ lastHealthyAt: this.health?.lastHealthyAt ?? null,
11898
+ latencyMs: null,
11899
+ missedHeartbeats,
11900
+ ...this.health?.server ? { server: this.health.server } : {}
11901
+ };
11902
+ this.transition(status, { pid: this.info?.pid, error });
11903
+ return this.health;
11904
+ }).finally(() => {
11905
+ this.healthCheck = null;
11906
+ });
11907
+ return this.healthCheck;
11908
+ }
11909
+ markResponsive() {
11910
+ const now = Date.now();
11911
+ this.health = {
11912
+ status: "healthy",
11913
+ checkedAt: now,
11914
+ lastHealthyAt: now,
11915
+ latencyMs: this.health?.latencyMs ?? null,
11916
+ missedHeartbeats: 0,
11917
+ ...this.health?.server ? { server: this.health.server } : {}
11918
+ };
11919
+ }
11920
+ async call(op, args, options) {
11921
+ if (options.signal?.aborted) throw cancellationError(options.signal);
11922
+ await this.ensureConnected(true);
11923
+ if (options.signal?.aborted) throw cancellationError(options.signal);
11924
+ return this.request({ type: "request", op, args }, options);
11925
+ }
11926
+ async shutdownRemote(reason) {
11927
+ try {
11928
+ await this.ensureConnected(false);
11929
+ } catch {
11930
+ return { stopped: false, reason: "not-running" };
11931
+ }
11932
+ const pid = this.info?.pid;
11933
+ try {
11934
+ this.transition("stopping", { pid });
11935
+ await this.request(
11936
+ { type: "shutdown", reason },
11937
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
11938
+ );
11939
+ return { stopped: true, pid };
11940
+ } catch (error) {
11941
+ const forceKilled = this.forceKillKnownServer();
11942
+ return {
11943
+ stopped: forceKilled,
11944
+ pid,
11945
+ reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
11946
+ };
11947
+ } finally {
11948
+ this.close();
11949
+ }
11950
+ }
11951
+ async configure(watchExternal, debounceMs) {
11952
+ await this.ensureConnected(true);
11953
+ const startedAt = Date.now();
11954
+ const result = await this.request(
11955
+ { type: "configure", watchExternal, debounceMs },
11956
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
11957
+ );
11958
+ if (isProjectIndexServerHealth(result.health)) {
11959
+ const now = Date.now();
11960
+ this.health = {
11961
+ status: "healthy",
11962
+ checkedAt: now,
11963
+ lastHealthyAt: now,
11964
+ latencyMs: Math.max(0, now - startedAt),
11965
+ missedHeartbeats: 0,
11966
+ server: result.health
11967
+ };
11968
+ this.transition("connected", { pid: this.info?.pid });
11969
+ }
11970
+ }
11971
+ close() {
11972
+ const socket = this.socket;
11973
+ this.socket = null;
11974
+ this.info = null;
11975
+ this.activity = null;
11976
+ this.health = null;
11977
+ this.connectReject?.(new Error("codebase-index client disconnected"));
11978
+ this.connectResolve = null;
11979
+ this.connectReject = null;
11980
+ if (socket && !socket.destroyed) socket.destroy();
11981
+ this.rejectPending(new Error("codebase-index client disconnected"));
11982
+ this.transition("offline");
11983
+ maybeStopHeartbeatLoop();
11984
+ }
11985
+ request(message, options) {
11986
+ const socket = this.socket;
11987
+ if (!socket || socket.destroyed) {
11988
+ return Promise.reject(new Error("codebase-index server connection is not available"));
11989
+ }
11990
+ const id = this.nextId++;
11991
+ return new Promise((resolve16, reject) => {
11992
+ const timer = setTimeout(() => {
11993
+ const entry = this.pending.get(id);
11994
+ if (!entry) return;
11995
+ this.pending.delete(id);
11996
+ this.write({ type: "cancel", id });
11997
+ const error = new IndexTimeoutError(
11998
+ `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
11999
+ );
12000
+ this.cleanupPending(entry);
12001
+ entry.reject(error);
12002
+ }, options.timeoutMs);
12003
+ timer.unref?.();
12004
+ const signal = options.signal;
12005
+ const onAbort = signal ? () => {
12006
+ const entry = this.pending.get(id);
12007
+ if (!entry) return;
12008
+ this.pending.delete(id);
12009
+ this.write({ type: "cancel", id });
12010
+ this.cleanupPending(entry);
12011
+ entry.reject(cancellationError(signal));
12012
+ } : void 0;
12013
+ this.pending.set(id, {
12014
+ resolve: resolve16,
12015
+ reject,
12016
+ timer,
12017
+ signal,
12018
+ onAbort,
12019
+ onProgress: options.onProgress
12020
+ });
12021
+ if (signal && onAbort) {
12022
+ signal.addEventListener("abort", onAbort, { once: true });
12023
+ if (signal.aborted) {
12024
+ onAbort();
12025
+ return;
12026
+ }
11898
12027
  }
11899
- throwIfAborted(signal);
12028
+ this.write({ ...message, id });
12029
+ });
12030
+ }
12031
+ async ensureConnected(spawnIfMissing) {
12032
+ if (this.socket && !this.socket.destroyed && this.info) return;
12033
+ if (this.connecting) return this.connecting;
12034
+ this.transition("connecting");
12035
+ this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12036
+ this.transition("error", { error });
12037
+ throw error;
12038
+ }).finally(() => {
12039
+ this.connecting = null;
12040
+ });
12041
+ return this.connecting;
12042
+ }
12043
+ async connectWithElection(spawnIfMissing) {
12044
+ const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12045
+ let spawned = false;
12046
+ let staleAttempts = 0;
12047
+ let lastError = new Error("codebase-index server unavailable");
12048
+ while (Date.now() < deadline) {
12049
+ try {
12050
+ await this.connectOnce();
12051
+ return;
12052
+ } catch (error) {
12053
+ lastError = error;
12054
+ if (error instanceof StaleProjectIndexServerError) {
12055
+ staleAttempts++;
12056
+ if (!spawnIfMissing) break;
12057
+ if (staleAttempts >= 3) this.forceKillServer(error.pid);
12058
+ spawned = false;
12059
+ await delay(100);
12060
+ continue;
12061
+ }
12062
+ }
12063
+ if (!spawnIfMissing) break;
12064
+ if (!spawned) {
12065
+ this.spawnDetachedServer();
12066
+ spawned = true;
12067
+ }
12068
+ await delay(75);
11900
12069
  }
11901
- const statOpts = signal ? { signal } : {};
11902
- const statReadParse = await Promise.allSettled(
11903
- batchFiles.map(
11904
- async (file) => {
11905
- let stat17;
11906
- try {
11907
- stat17 = await fs15.stat(file, statOpts);
11908
- } catch (e) {
11909
- if (isAbortError(e)) throw e;
11910
- return {
11911
- file,
11912
- stat: null,
11913
- lang: "",
11914
- parsed: null,
11915
- error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
11916
- missing: isMissingPathError(e)
11917
- };
11918
- }
11919
- if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
11920
- const lang = detectLang(file);
11921
- if (!lang) return { file, stat: stat17, lang: "", parsed: null };
11922
- if (stat17.size > MAX_INDEX_FILE_BYTES) {
11923
- return {
11924
- file,
11925
- stat: stat17,
11926
- lang,
11927
- parsed: null,
11928
- error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
11929
- };
11930
- }
11931
- const meta = existingMeta.get(file);
11932
- if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
11933
- return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
11934
- }
11935
- let content;
11936
- try {
11937
- content = await fs15.readFile(file, { encoding: "utf8", signal });
11938
- } catch (e) {
11939
- if (isAbortError(e)) throw e;
11940
- return {
11941
- file,
11942
- stat: stat17,
11943
- lang,
11944
- parsed: null,
11945
- error: `read error: ${e instanceof Error ? e.message : String(e)}`
11946
- };
11947
- }
11948
- let parsed;
11949
- try {
11950
- parsed = await parseFileContent(file, content, lang);
11951
- } catch (e) {
11952
- return {
11953
- file,
11954
- stat: stat17,
11955
- lang,
11956
- parsed: null,
11957
- error: `parse error: ${e instanceof Error ? e.message : String(e)}`
11958
- };
11959
- }
11960
- return { file, stat: stat17, lang, parsed, content };
12070
+ throw lastError;
12071
+ }
12072
+ connectOnce() {
12073
+ this.socket?.destroy();
12074
+ this.socket = null;
12075
+ this.info = null;
12076
+ this.activity = null;
12077
+ this.health = null;
12078
+ this.buffer = "";
12079
+ return new Promise((resolve16, reject) => {
12080
+ const socket = net3.createConnection(this.endpoint);
12081
+ this.socket = socket;
12082
+ socket.setEncoding("utf8");
12083
+ const timer = setTimeout(() => {
12084
+ reject(new Error("codebase-index server handshake timed out"));
12085
+ socket.destroy();
12086
+ }, CONNECT_ATTEMPT_TIMEOUT_MS);
12087
+ timer.unref?.();
12088
+ const finishResolve = () => {
12089
+ clearTimeout(timer);
12090
+ this.connectResolve = null;
12091
+ this.connectReject = null;
12092
+ resolve16();
12093
+ };
12094
+ const finishReject = (error) => {
12095
+ clearTimeout(timer);
12096
+ this.connectResolve = null;
12097
+ this.connectReject = null;
12098
+ reject(error);
12099
+ };
12100
+ this.connectResolve = finishResolve;
12101
+ this.connectReject = finishReject;
12102
+ socket.on("data", (chunk) => this.onData(socket, chunk));
12103
+ socket.on("error", (error) => {
12104
+ if (!this.info) finishReject(error);
12105
+ });
12106
+ socket.on("close", () => this.onClose(socket));
12107
+ });
12108
+ }
12109
+ onData(socket, chunk) {
12110
+ if (socket !== this.socket) return;
12111
+ this.buffer += chunk;
12112
+ while (true) {
12113
+ const newline = this.buffer.indexOf("\n");
12114
+ if (newline < 0) {
12115
+ if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12116
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
11961
12117
  }
11962
- )
11963
- );
11964
- const batchEntries = [];
11965
- const deleteForFiles = [];
11966
- for (let fi = 0; fi < statReadParse.length; fi++) {
11967
- const settled = statReadParse[fi];
11968
- const file = expectDefined6(batchFiles[fi]);
11969
- if (settled.status === "rejected") {
11970
- const err = settled.reason;
11971
- if (err instanceof Error && isAbortError(err)) throw err;
11972
- errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
11973
- continue;
12118
+ return;
11974
12119
  }
11975
- const result = settled.value;
11976
- if (result.error) {
11977
- if (result.missing) store.deleteFile(file);
11978
- errors.push(`${file}: ${result.error}`);
11979
- continue;
12120
+ if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12121
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12122
+ return;
11980
12123
  }
11981
- const { stat: stat17, lang, parsed } = result;
11982
- if (result.skippedMeta) {
11983
- langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
11984
- symbolsIndexed += result.skippedMeta.symbolCount;
11985
- filesIndexed++;
11986
- continue;
12124
+ const line = this.buffer.slice(0, newline);
12125
+ this.buffer = this.buffer.slice(newline + 1);
12126
+ if (!line) continue;
12127
+ let message;
12128
+ try {
12129
+ message = JSON.parse(line);
12130
+ } catch {
12131
+ socket.destroy(new Error("invalid codebase-index server response"));
12132
+ return;
11987
12133
  }
11988
- if (!lang || !parsed) {
11989
- if (lang) {
11990
- store.upsertFile({
11991
- file,
11992
- lang,
11993
- mtimeMs: Math.floor(stat17.mtimeMs),
11994
- symbolCount: 0,
11995
- lastIndexed: Date.now()
11996
- });
11997
- filesIndexed++;
11998
- }
11999
- continue;
12134
+ this.onMessage(message);
12135
+ }
12136
+ }
12137
+ onMessage(message) {
12138
+ if (message.type === "hello") {
12139
+ if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12140
+ this.rejectStaleServer(
12141
+ message,
12142
+ `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12143
+ );
12144
+ return;
12000
12145
  }
12001
- if (parsed.symbols.length === 0) {
12002
- store.replaceEmptyFile({
12003
- file,
12004
- lang,
12005
- mtimeMs: Math.floor(stat17.mtimeMs),
12006
- symbolCount: 0,
12007
- lastIndexed: Date.now()
12008
- });
12009
- filesIndexed++;
12010
- continue;
12146
+ const expectedBuildId = projectIndexServerExpectedBuildId();
12147
+ if (expectedBuildId && message.buildId !== expectedBuildId) {
12148
+ this.rejectStaleServer(
12149
+ message,
12150
+ `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12151
+ );
12152
+ return;
12011
12153
  }
12012
- batchEntries.push({
12013
- file,
12014
- lang,
12015
- symbols: parsed.symbols,
12016
- refs: parsed.refs ?? [],
12017
- mtimeMs: Math.floor(stat17.mtimeMs),
12018
- symbolCount: parsed.symbols.length
12019
- });
12020
- deleteForFiles.push(file);
12154
+ this.info = message;
12155
+ this.markResponsive();
12156
+ this.transition("connected", { pid: message.pid });
12157
+ ensureHeartbeatLoop();
12158
+ this.connectResolve?.();
12159
+ return;
12021
12160
  }
12022
- if (batchEntries.length > 0) {
12023
- try {
12024
- store.commitBatch(batchEntries, { deleteForFiles });
12025
- for (const entry of batchEntries) {
12026
- const count = entry.symbols.length;
12027
- symbolsIndexed += count;
12028
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
12029
- filesIndexed++;
12030
- }
12031
- } catch (err) {
12032
- const message = err instanceof Error ? err.message : String(err);
12033
- errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
12034
- for (const entry of batchEntries) {
12035
- try {
12036
- store.deleteRefsForFile(entry.file);
12037
- store.deleteSymbolsForFile(entry.file);
12038
- const symbolsWithIds = store.insertSymbols(entry.symbols);
12039
- symbolsIndexed += symbolsWithIds.length;
12040
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
12041
- filesIndexed++;
12042
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
12043
- const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
12044
- if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
12045
- }
12046
- store.resolveRefsForNames([
12047
- ...entry.symbols.map((symbol) => symbol.name),
12048
- ...entry.refs.map((ref) => ref.toName)
12049
- ]);
12050
- store.upsertFile({
12051
- file: entry.file,
12052
- lang: entry.lang,
12053
- mtimeMs: entry.mtimeMs,
12054
- symbolCount: entry.symbolCount,
12055
- lastIndexed: Date.now()
12056
- });
12057
- } catch (innerErr) {
12058
- errors.push(
12059
- `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
12060
- );
12061
- }
12062
- }
12063
- }
12161
+ if (message.type === "index-state") {
12162
+ this.activity = message.state;
12163
+ this.markResponsive();
12164
+ this.transition("connected", { pid: this.info?.pid });
12165
+ return;
12166
+ }
12167
+ const entry = this.pending.get(message.id);
12168
+ if (!entry) return;
12169
+ this.markResponsive();
12170
+ const status = connectionStates.get(this.endpoint)?.status;
12171
+ if (status === "degraded" || status === "unresponsive") {
12172
+ this.transition("connected", { pid: this.info?.pid });
12173
+ }
12174
+ if (message.type === "progress") {
12175
+ entry.onProgress?.(message.current, message.total);
12176
+ return;
12064
12177
  }
12178
+ this.pending.delete(message.id);
12179
+ this.cleanupPending(entry);
12180
+ if (message.ok) entry.resolve(message.result);
12181
+ else entry.reject(remoteError(message.error, message.errorName));
12065
12182
  }
12066
- if (discoveredFiles && discoveryComplete) {
12067
- for (const [file_] of existingMeta) {
12068
- if (!discoveredFiles.has(file_)) {
12069
- store.deleteFile(file_);
12070
- }
12183
+ onClose(socket) {
12184
+ if (socket !== this.socket) return;
12185
+ const wasConnected = this.info !== null;
12186
+ this.socket = null;
12187
+ this.info = null;
12188
+ this.activity = null;
12189
+ this.health = null;
12190
+ const error = new Error("codebase-index server connection closed");
12191
+ this.connectReject?.(error);
12192
+ this.connectResolve = null;
12193
+ this.connectReject = null;
12194
+ this.rejectPending(error);
12195
+ if (wasConnected) this.transition("error", { error });
12196
+ maybeStopHeartbeatLoop();
12197
+ }
12198
+ cleanupPending(entry) {
12199
+ clearTimeout(entry.timer);
12200
+ if (entry.signal && entry.onAbort) {
12201
+ entry.signal.removeEventListener("abort", entry.onAbort);
12071
12202
  }
12072
12203
  }
12073
- if (needsFullRefResolution) store.resolveRefs();
12074
- store.setMetadata("ref_resolution_version", refResolutionVersion);
12075
- store.setMetadata("relation_graph_version", relationGraphVersion);
12076
- if (!opts.files || filesIndexed >= 50) store.optimize();
12077
- store.setLastIndexed(Date.now());
12078
- if (!opts.files) store.compactIfNeeded();
12079
- const durationMs = Date.now() - startMs;
12080
- return {
12081
- filesIndexed,
12082
- symbolsIndexed,
12083
- langStats,
12084
- durationMs,
12085
- errors
12086
- };
12087
- }
12088
-
12089
- // src/codebase-index/index-service.ts
12090
- async function indexService(args, hooks = {}) {
12091
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12092
- try {
12093
- return await runIndexerWithStore(store, {
12094
- projectRoot: args.projectRoot,
12095
- indexDir: args.indexDir,
12096
- files: args.files,
12097
- force: args.force,
12098
- langs: args.langs,
12099
- ignore: args.ignore,
12100
- signal: hooks.signal,
12101
- onProgress: hooks.onProgress
12102
- });
12103
- } finally {
12104
- indexStorePool.release(store);
12204
+ rejectPending(error) {
12205
+ const entries = [...this.pending.values()];
12206
+ this.pending.clear();
12207
+ for (const entry of entries) {
12208
+ this.cleanupPending(entry);
12209
+ entry.reject(error);
12210
+ }
12105
12211
  }
12106
- }
12107
- function searchService(args) {
12108
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12109
- try {
12110
- return store.searchRanked(
12111
- args.query,
12112
- {
12113
- kind: args.kind,
12114
- lang: args.lang,
12115
- file: args.file,
12116
- lspKind: args.lspKind
12117
- },
12118
- args.limit
12119
- );
12120
- } finally {
12121
- indexStorePool.release(store);
12212
+ write(message) {
12213
+ const socket = this.socket;
12214
+ if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
12215
+ }
12216
+ rejectStaleServer(message, reason) {
12217
+ const socket = this.socket;
12218
+ if (socket && !socket.destroyed) {
12219
+ socket.write(
12220
+ encodeProjectServerMessage({
12221
+ type: "shutdown",
12222
+ id: 0,
12223
+ reason: "stale-build-replacement"
12224
+ })
12225
+ );
12226
+ const timer = setTimeout(() => socket.destroy(), 25);
12227
+ timer.unref?.();
12228
+ }
12229
+ this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
12230
+ }
12231
+ spawnDetachedServer() {
12232
+ const url = resolveProjectServerUrl();
12233
+ if (!url) throw new Error("built codebase-index project server is unavailable");
12234
+ if (process.platform !== "win32") {
12235
+ try {
12236
+ fs12.rmSync(this.endpoint, { force: true });
12237
+ } catch {
12238
+ }
12239
+ }
12240
+ const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
12241
+ if (this.indexDir) args.push("--index-dir", this.indexDir);
12242
+ const child = spawn4(process.execPath, args, {
12243
+ detached: true,
12244
+ stdio: "ignore",
12245
+ windowsHide: true,
12246
+ env: process.env
12247
+ });
12248
+ child.unref();
12122
12249
  }
12123
- }
12124
- function statsService(args) {
12125
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12126
- try {
12127
- return store.getStats();
12128
- } finally {
12129
- indexStorePool.release(store);
12250
+ forceKillKnownServer() {
12251
+ const pid = this.info?.pid;
12252
+ return pid ? this.forceKillServer(pid) : false;
12130
12253
  }
12131
- }
12132
- function packageGraphService(args) {
12133
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12134
- try {
12135
- return store.getPackageGraph();
12136
- } finally {
12137
- indexStorePool.release(store);
12254
+ forceKillServer(pid) {
12255
+ if (pid === process.pid) return false;
12256
+ try {
12257
+ process.kill(pid);
12258
+ const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12259
+ try {
12260
+ const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
12261
+ if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
12262
+ } catch {
12263
+ }
12264
+ return true;
12265
+ } catch {
12266
+ return false;
12267
+ }
12138
12268
  }
12269
+ };
12270
+ var connections = /* @__PURE__ */ new Map();
12271
+ var heartbeatTimer;
12272
+ function ensureHeartbeatLoop() {
12273
+ if (heartbeatTimer) return;
12274
+ heartbeatTimer = setInterval(() => {
12275
+ for (const connection of connections.values()) {
12276
+ if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
12277
+ });
12278
+ }
12279
+ }, SERVER_HEARTBEAT_INTERVAL_MS);
12280
+ heartbeatTimer.unref?.();
12139
12281
  }
12140
- function fileGraphService(args) {
12141
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12142
- try {
12143
- return store.getFileGraph(args.packageFilter);
12144
- } finally {
12145
- indexStorePool.release(store);
12146
- }
12282
+ function maybeStopHeartbeatLoop() {
12283
+ if (!heartbeatTimer) return;
12284
+ if ([...connections.values()].some((connection) => connection.isConnected())) return;
12285
+ clearInterval(heartbeatTimer);
12286
+ heartbeatTimer = void 0;
12147
12287
  }
12148
- function symbolGraphService(args) {
12149
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12150
- try {
12151
- return store.getSymbolGraph(args.fileFilter);
12152
- } finally {
12153
- indexStorePool.release(store);
12288
+ function connectionFor(projectRoot, indexDir) {
12289
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12290
+ let connection = connections.get(endpoint);
12291
+ if (!connection) {
12292
+ connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
12293
+ connections.set(endpoint, connection);
12154
12294
  }
12295
+ return connection;
12296
+ }
12297
+ function callProjectIndexServer(op, args, options) {
12298
+ return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
12155
12299
  }
12156
12300
 
12157
- // src/codebase-index/project-server-client.ts
12158
- import { spawn as spawn7 } from "node:child_process";
12159
- import * as fs17 from "node:fs";
12160
- import * as net3 from "node:net";
12161
- import { fileURLToPath as fileURLToPath2 } from "node:url";
12301
+ // src/codebase-index/background-indexer.ts
12302
+ import * as fs18 from "node:fs";
12303
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
12304
+ import { Worker } from "node:worker_threads";
12162
12305
 
12163
- // src/codebase-index/project-server-endpoint.ts
12164
- import { createHash as createHash4 } from "node:crypto";
12165
- import * as fs16 from "node:fs";
12166
- import * as os7 from "node:os";
12306
+ // src/codebase-index/indexer.ts
12307
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
12308
+ import { execFile as execFile2 } from "node:child_process";
12309
+ import * as fs17 from "node:fs/promises";
12310
+ import { availableParallelism } from "node:os";
12167
12311
  import * as path23 from "node:path";
12168
- import { fileURLToPath } from "node:url";
12169
- var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
12170
- var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12171
- var buildIdCache;
12172
- function projectIndexServerBuildId(entrypoint) {
12173
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path23.resolve(entrypoint);
12174
- try {
12175
- const stat17 = fs16.statSync(file);
12176
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat17.mtimeMs && buildIdCache.size === stat17.size) {
12177
- return buildIdCache.buildId;
12312
+ import {
12313
+ DEFAULT_WALK_IGNORE_DIRS,
12314
+ indexParallelBatchSize,
12315
+ isFrugalPerf
12316
+ } from "@wrongstack/core/utils";
12317
+
12318
+ // src/codebase-index/gitignore.ts
12319
+ import * as fs13 from "node:fs/promises";
12320
+ import * as path17 from "node:path";
12321
+ import { compileGlob } from "@wrongstack/core/utils";
12322
+ function globBody(glob) {
12323
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
12324
+ }
12325
+ function compileGitignore(lines) {
12326
+ const rules = [];
12327
+ for (const raw of lines) {
12328
+ let line = raw.replace(/\r$/, "");
12329
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
12330
+ line = line.trim();
12331
+ let negated = false;
12332
+ if (line.startsWith("!")) {
12333
+ negated = true;
12334
+ line = line.slice(1);
12178
12335
  }
12179
- const buildId = createHash4("sha256").update(fs16.readFileSync(file)).digest("hex").slice(0, 24);
12180
- buildIdCache = { file, mtimeMs: stat17.mtimeMs, size: stat17.size, buildId };
12181
- return buildId;
12182
- } catch {
12183
- return `unreadable:${path23.basename(file)}`;
12336
+ let dirOnly = false;
12337
+ if (line.endsWith("/")) {
12338
+ dirOnly = true;
12339
+ line = line.slice(0, -1);
12340
+ }
12341
+ if (!line) continue;
12342
+ const anchored = line.startsWith("/") || line.includes("/");
12343
+ if (line.startsWith("/")) line = line.slice(1);
12344
+ const body = globBody(line);
12345
+ const prefix = anchored ? "^" : "(?:^|.*/)";
12346
+ rules.push({
12347
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
12348
+ under: new RegExp(`${prefix}${body}/.*$`),
12349
+ negated,
12350
+ dirOnly
12351
+ });
12184
12352
  }
12353
+ return (relPath, isDir) => {
12354
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
12355
+ let ignored = false;
12356
+ for (const r of rules) {
12357
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
12358
+ if (re.test(p)) ignored = !r.negated;
12359
+ }
12360
+ return ignored;
12361
+ };
12185
12362
  }
12186
- function normalizeLocalPath(value) {
12187
- const resolved = path23.resolve(value);
12188
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12189
- }
12190
- function projectIndexServerKey(projectRoot, indexDir) {
12191
- const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
12192
- return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
12193
- }
12194
- function projectIndexServerEndpoint(projectRoot, indexDir) {
12195
- const key = projectIndexServerKey(projectRoot, indexDir);
12196
- if (process.platform === "win32") {
12197
- return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12363
+ async function loadGitignoreMatcher(projectRoot) {
12364
+ let lines = [];
12365
+ try {
12366
+ const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
12367
+ lines = raw.split("\n");
12368
+ } catch {
12198
12369
  }
12199
- return path23.join(
12200
- os7.tmpdir(),
12201
- `wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`,
12202
- `${key}.sock`
12203
- );
12204
- }
12205
- function projectIndexServerMetadataPath(projectRoot, indexDir) {
12206
- return path23.join(
12207
- path23.resolve(resolveIndexDir(projectRoot, indexDir)),
12208
- PROJECT_INDEX_SERVER_METADATA_FILE
12209
- );
12370
+ return compileGitignore(lines);
12210
12371
  }
12211
12372
 
12212
- // src/codebase-index/project-server-protocol.ts
12213
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
12214
- function encodeProjectServerMessage(message) {
12215
- return `${JSON.stringify(message)}
12216
- `;
12217
- }
12373
+ // src/codebase-index/indexer.ts
12374
+ init_languages2();
12218
12375
 
12219
- // src/codebase-index/project-server-client.ts
12220
- var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
12221
- var SERVER_START_TIMEOUT_MS = 1e4;
12222
- var SERVER_CONTROL_TIMEOUT_MS = 5e3;
12223
- var SERVER_HEALTH_TIMEOUT_MS = 3e3;
12224
- var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
12225
- var StaleProjectIndexServerError = class extends Error {
12226
- constructor(message, pid) {
12227
- super(message);
12228
- this.pid = pid;
12229
- }
12230
- pid;
12231
- name = "StaleProjectIndexServerError";
12232
- };
12233
- var connectionStates = /* @__PURE__ */ new Map();
12234
- var connectionStateListeners = /* @__PURE__ */ new Set();
12235
- var latestConnectionState = {
12236
- status: "offline",
12237
- connected: false
12238
- };
12239
- function resolveProjectIndexDaemonAvailability() {
12240
- if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
12241
- return { kind: "inline-requested" };
12242
- }
12243
- for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12244
- try {
12245
- const url = new URL(rel, import.meta.url);
12246
- if (url.protocol === "file:" && fs17.existsSync(fileURLToPath2(url))) {
12247
- return { kind: "available", url };
12248
- }
12249
- } catch {
12376
+ // src/codebase-index/parser-dispatch.ts
12377
+ async function parseFileContent(file, content, lang) {
12378
+ switch (lang) {
12379
+ case "ts":
12380
+ case "tsx":
12381
+ case "js":
12382
+ case "jsx": {
12383
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
12384
+ return parseSymbols8({ file, content, lang });
12250
12385
  }
12251
- }
12252
- return { kind: "missing-build" };
12253
- }
12254
- function resolveProjectServerUrl() {
12255
- const availability = resolveProjectIndexDaemonAvailability();
12256
- return availability.kind === "available" ? availability.url : null;
12257
- }
12258
- function projectIndexServerExpectedBuildId() {
12259
- const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
12260
- if (override) return override;
12261
- const url = resolveProjectServerUrl();
12262
- return url ? projectIndexServerBuildId(url) : null;
12386
+ case "go": {
12387
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
12388
+ return parseSymbols8({ file, content, lang: "go" });
12389
+ }
12390
+ case "py": {
12391
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
12392
+ return parseSymbols8({ file, content, lang: "py" });
12393
+ }
12394
+ case "rs": {
12395
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
12396
+ return parseSymbols8({ file, content, lang: "rs" });
12397
+ }
12398
+ case "json": {
12399
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
12400
+ return parseSymbols8({ file, content, lang: "json" });
12401
+ }
12402
+ case "yaml": {
12403
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
12404
+ return parseSymbols8({ file, content, lang: "yaml" });
12405
+ }
12406
+ default: {
12407
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
12408
+ return parseSymbols8({ file, content, lang });
12409
+ }
12410
+ }
12263
12411
  }
12264
- function isProjectIndexServerAvailable() {
12265
- return resolveProjectServerUrl() !== null;
12412
+
12413
+ // src/codebase-index/indexer.ts
12414
+ var YIELD_EVERY_N = 50;
12415
+ function resolveParallelBatch() {
12416
+ return indexParallelBatchSize(availableParallelism());
12266
12417
  }
12267
- function publishConnectionState(endpoint, state) {
12268
- connectionStates.set(endpoint, state);
12269
- latestConnectionState = state;
12270
- for (const listener of connectionStateListeners) listener(state);
12418
+ function yieldEventLoop() {
12419
+ return new Promise((resolve16) => setImmediate(resolve16));
12271
12420
  }
12272
- function getProjectIndexServerConnectionState(projectRoot, indexDir) {
12273
- if (projectRoot) {
12274
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12275
- const existing = connectionStates.get(endpoint);
12276
- if (existing) return existing;
12277
- if (!isProjectIndexServerAvailable()) {
12278
- return { status: "unavailable", connected: false };
12279
- }
12280
- return {
12281
- status: "offline",
12282
- connected: false,
12283
- projectRoot,
12284
- indexDir,
12285
- endpoint
12286
- };
12287
- }
12288
- if (latestConnectionState.endpoint) return latestConnectionState;
12289
- if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
12290
- return latestConnectionState;
12421
+ function throwIfAborted(signal) {
12422
+ if (!signal?.aborted) return;
12423
+ if (signal.reason instanceof Error) throw signal.reason;
12424
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
12291
12425
  }
12292
- function onProjectIndexServerConnectionStateChange(listener) {
12293
- connectionStateListeners.add(listener);
12294
- return () => connectionStateListeners.delete(listener);
12426
+ function isAbortError(err) {
12427
+ return err instanceof DOMException && err.name === "AbortError";
12295
12428
  }
12296
- function remoteError(message, name) {
12297
- if (name === "LockError") return new LockError(message);
12298
- if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
12299
- const error = new Error(message);
12300
- if (name && name !== "Error") error.name = name;
12301
- return error;
12429
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
12430
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
12431
+ var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
12432
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
12433
+ var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
12434
+ function isWithinProject(projectRoot, file) {
12435
+ const rel = path23.relative(projectRoot, file);
12436
+ return rel !== "" && !rel.startsWith(`..${path23.sep}`) && rel !== ".." && !path23.isAbsolute(rel);
12302
12437
  }
12303
- function isProjectIndexServerHealth(value) {
12304
- if (!value || typeof value !== "object") return false;
12305
- const health = value;
12306
- const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
12307
- const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
12308
- return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
12438
+ function isMissingPathError(err) {
12439
+ const code = err?.code;
12440
+ return code === "ENOENT" || code === "ENOTDIR";
12309
12441
  }
12310
- function delay(ms) {
12311
- return new Promise((resolve16) => {
12312
- const timer = setTimeout(resolve16, ms);
12313
- timer.unref?.();
12314
- });
12442
+ function normalizeComparablePath(value) {
12443
+ const resolved = path23.resolve(value);
12444
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12315
12445
  }
12316
- function cancellationError(signal) {
12317
- return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
12446
+ function gitOutput(projectRoot, args) {
12447
+ return new Promise((resolve16, reject) => {
12448
+ execFile2(
12449
+ "git",
12450
+ ["-C", projectRoot, ...args],
12451
+ {
12452
+ encoding: "buffer",
12453
+ maxBuffer: MAX_GIT_FILE_LIST_BYTES,
12454
+ windowsHide: true
12455
+ },
12456
+ (error, stdout) => {
12457
+ if (error) reject(error);
12458
+ else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
12459
+ }
12460
+ );
12461
+ });
12318
12462
  }
12319
- var ProjectServerConnection = class {
12320
- constructor(projectRoot, indexDir, endpoint) {
12321
- this.projectRoot = projectRoot;
12322
- this.indexDir = indexDir;
12323
- this.endpoint = endpoint;
12324
- this.transition("offline");
12325
- }
12326
- projectRoot;
12327
- indexDir;
12328
- endpoint;
12329
- socket = null;
12330
- buffer = "";
12331
- info = null;
12332
- activity = null;
12333
- health = null;
12334
- healthCheck = null;
12335
- connecting = null;
12336
- connectResolve = null;
12337
- connectReject = null;
12338
- nextId = 1;
12339
- pending = /* @__PURE__ */ new Map();
12340
- transition(status, options = {}) {
12341
- const previous = connectionStates.get(this.endpoint);
12342
- const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
12343
- const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
12344
- publishConnectionState(this.endpoint, {
12345
- status,
12346
- connected: status === "connected" || status === "degraded" || status === "unresponsive",
12347
- projectRoot: this.projectRoot,
12348
- indexDir: this.indexDir,
12349
- endpoint: this.endpoint,
12350
- pid,
12351
- lastError,
12352
- ...this.activity ? { activity: this.activity } : {},
12353
- ...this.health ? { health: this.health } : {}
12354
- });
12355
- }
12356
- isConnected() {
12357
- return this.socket !== null && !this.socket.destroyed && this.info !== null;
12358
- }
12359
- async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
12360
- await this.ensureConnected(spawnIfMissing);
12361
- if (this.healthCheck) return this.healthCheck;
12362
- const startedAt = Date.now();
12363
- this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
12364
- const now = Date.now();
12365
- this.health = {
12366
- status: "healthy",
12367
- checkedAt: now,
12368
- lastHealthyAt: now,
12369
- latencyMs: Math.max(0, now - startedAt),
12370
- missedHeartbeats: 0,
12371
- ...isProjectIndexServerHealth(server) ? { server } : {}
12372
- };
12373
- this.transition("connected", { pid: this.info?.pid });
12374
- return this.health;
12375
- }).catch((error) => {
12376
- if (!this.isConnected()) throw error;
12377
- if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
12378
- const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
12379
- const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
12380
- this.health = {
12381
- status,
12382
- checkedAt: Date.now(),
12383
- lastHealthyAt: this.health?.lastHealthyAt ?? null,
12384
- latencyMs: null,
12385
- missedHeartbeats,
12386
- ...this.health?.server ? { server: this.health.server } : {}
12387
- };
12388
- this.transition(status, { pid: this.info?.pid, error });
12389
- return this.health;
12390
- }).finally(() => {
12391
- this.healthCheck = null;
12392
- });
12393
- return this.healthCheck;
12394
- }
12395
- markResponsive() {
12396
- const now = Date.now();
12397
- this.health = {
12398
- status: "healthy",
12399
- checkedAt: now,
12400
- lastHealthyAt: now,
12401
- latencyMs: this.health?.latencyMs ?? null,
12402
- missedHeartbeats: 0,
12403
- ...this.health?.server ? { server: this.health.server } : {}
12404
- };
12405
- }
12406
- async call(op, args, options) {
12407
- if (options.signal?.aborted) throw cancellationError(options.signal);
12408
- await this.ensureConnected(true);
12409
- if (options.signal?.aborted) throw cancellationError(options.signal);
12410
- return this.request({ type: "request", op, args }, options);
12411
- }
12412
- async shutdownRemote(reason) {
12413
- try {
12414
- await this.ensureConnected(false);
12415
- } catch {
12416
- return { stopped: false, reason: "not-running" };
12417
- }
12418
- const pid = this.info?.pid;
12419
- try {
12420
- this.transition("stopping", { pid });
12421
- await this.request(
12422
- { type: "shutdown", reason },
12423
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12424
- );
12425
- return { stopped: true, pid };
12426
- } catch (error) {
12427
- const forceKilled = this.forceKillKnownServer();
12428
- return {
12429
- stopped: forceKilled,
12430
- pid,
12431
- reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
12432
- };
12433
- } finally {
12434
- this.close();
12463
+ async function findGitSourceFiles(projectRoot, ignore, signal) {
12464
+ try {
12465
+ throwIfAborted(signal);
12466
+ const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
12467
+ if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
12468
+ throwIfAborted(signal);
12469
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12470
+ const [output, statusOutput] = await Promise.all([
12471
+ gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
12472
+ gitOutput(projectRoot, [
12473
+ "status",
12474
+ "--porcelain=v1",
12475
+ "-z",
12476
+ "--untracked-files=all",
12477
+ "--ignored=no"
12478
+ ])
12479
+ ]);
12480
+ throwIfAborted(signal);
12481
+ const dirty = /* @__PURE__ */ new Set();
12482
+ const deleted = /* @__PURE__ */ new Set();
12483
+ const statusRecords = statusOutput.toString("utf8").split("\0");
12484
+ for (let i = 0; i < statusRecords.length; i++) {
12485
+ const record = statusRecords[i];
12486
+ if (!record) continue;
12487
+ const status = record.slice(0, 2);
12488
+ const changedPath = path23.resolve(projectRoot, record.slice(3));
12489
+ dirty.add(changedPath);
12490
+ if (status.includes("D")) deleted.add(changedPath);
12491
+ if (status.includes("R") || status.includes("C")) {
12492
+ const source = statusRecords[++i];
12493
+ if (source) dirty.add(path23.resolve(projectRoot, source));
12494
+ }
12435
12495
  }
12436
- }
12437
- async configure(watchExternal, debounceMs) {
12438
- await this.ensureConnected(true);
12439
- const startedAt = Date.now();
12440
- const result = await this.request(
12441
- { type: "configure", watchExternal, debounceMs },
12442
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12443
- );
12444
- if (isProjectIndexServerHealth(result.health)) {
12445
- const now = Date.now();
12446
- this.health = {
12447
- status: "healthy",
12448
- checkedAt: now,
12449
- lastHealthyAt: now,
12450
- latencyMs: Math.max(0, now - startedAt),
12451
- missedHeartbeats: 0,
12452
- server: result.health
12453
- };
12454
- this.transition("connected", { pid: this.info?.pid });
12496
+ const files = [];
12497
+ for (const relative12 of output.toString("utf8").split("\0")) {
12498
+ if (!relative12) continue;
12499
+ const portable = relative12.replace(/\\/g, "/");
12500
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path23.posix.basename(portable))) {
12501
+ continue;
12502
+ }
12503
+ const full = path23.resolve(projectRoot, relative12);
12504
+ if (deleted.has(full)) continue;
12505
+ const ext = path23.extname(relative12).toLowerCase();
12506
+ if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
12455
12507
  }
12508
+ return {
12509
+ files,
12510
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
12511
+ };
12512
+ } catch {
12513
+ return null;
12456
12514
  }
12457
- close() {
12458
- const socket = this.socket;
12459
- this.socket = null;
12460
- this.info = null;
12461
- this.activity = null;
12462
- this.health = null;
12463
- this.connectReject?.(new Error("codebase-index client disconnected"));
12464
- this.connectResolve = null;
12465
- this.connectReject = null;
12466
- if (socket && !socket.destroyed) socket.destroy();
12467
- this.rejectPending(new Error("codebase-index client disconnected"));
12468
- this.transition("offline");
12469
- maybeStopHeartbeatLoop();
12515
+ }
12516
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
12517
+ const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
12518
+ if (gitFiles) {
12519
+ return {
12520
+ files: gitFiles.files,
12521
+ complete: true,
12522
+ errors: [],
12523
+ trustedUnchanged: gitFiles.trustedUnchanged
12524
+ };
12470
12525
  }
12471
- request(message, options) {
12472
- const socket = this.socket;
12473
- if (!socket || socket.destroyed) {
12474
- return Promise.reject(new Error("codebase-index server connection is not available"));
12526
+ const results = [];
12527
+ const errors = [];
12528
+ let complete = true;
12529
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12530
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
12531
+ let dirCount = 0;
12532
+ const walk = async (dir) => {
12533
+ throwIfAborted(signal);
12534
+ if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
12535
+ await yieldEventLoop();
12536
+ throwIfAborted(signal);
12475
12537
  }
12476
- const id = this.nextId++;
12477
- return new Promise((resolve16, reject) => {
12478
- const timer = setTimeout(() => {
12479
- const entry = this.pending.get(id);
12480
- if (!entry) return;
12481
- this.pending.delete(id);
12482
- this.write({ type: "cancel", id });
12483
- const error = new IndexTimeoutError(
12484
- `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
12485
- );
12486
- this.cleanupPending(entry);
12487
- entry.reject(error);
12488
- }, options.timeoutMs);
12489
- timer.unref?.();
12490
- const signal = options.signal;
12491
- const onAbort = signal ? () => {
12492
- const entry = this.pending.get(id);
12493
- if (!entry) return;
12494
- this.pending.delete(id);
12495
- this.write({ type: "cancel", id });
12496
- this.cleanupPending(entry);
12497
- entry.reject(cancellationError(signal));
12498
- } : void 0;
12499
- this.pending.set(id, {
12500
- resolve: resolve16,
12501
- reject,
12502
- timer,
12503
- signal,
12504
- onAbort,
12505
- onProgress: options.onProgress
12506
- });
12507
- if (signal && onAbort) {
12508
- signal.addEventListener("abort", onAbort, { once: true });
12509
- if (signal.aborted) {
12510
- onAbort();
12511
- return;
12538
+ let entries;
12539
+ try {
12540
+ entries = await fs17.readdir(dir, { withFileTypes: true });
12541
+ } catch (err) {
12542
+ complete = false;
12543
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
12544
+ return;
12545
+ }
12546
+ dirCount++;
12547
+ for (const e of entries) {
12548
+ if (ignoreSet.has(e.name)) continue;
12549
+ const full = path23.join(dir, e.name);
12550
+ const rel = path23.relative(projectRoot, full).replace(/\\/g, "/");
12551
+ if (e.isDirectory()) {
12552
+ if (isGitIgnored(rel, true)) continue;
12553
+ await walk(full);
12554
+ } else if (e.isFile()) {
12555
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
12556
+ const ext = path23.extname(e.name).toLowerCase();
12557
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
12558
+ results.push(full);
12512
12559
  }
12513
12560
  }
12514
- this.write({ ...message, id });
12515
- });
12561
+ }
12562
+ };
12563
+ await walk(projectRoot);
12564
+ return { files: results, complete, errors };
12565
+ }
12566
+ function assignRefsToSymbols2(refs, symbols) {
12567
+ if (refs.length === 0 || symbols.length === 0) return [];
12568
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
12569
+ const seen = /* @__PURE__ */ new Set();
12570
+ const assigned = [];
12571
+ for (const ref of refs) {
12572
+ let owner2;
12573
+ for (const symbol of ordered) {
12574
+ if (symbol.line > ref.line) break;
12575
+ owner2 = symbol;
12576
+ }
12577
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12578
+ if (!owner2 || owner2.id <= 0) continue;
12579
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
12580
+ if (seen.has(key)) continue;
12581
+ seen.add(key);
12582
+ assigned.push({ ...ref, fromId: owner2.id });
12516
12583
  }
12517
- async ensureConnected(spawnIfMissing) {
12518
- if (this.socket && !this.socket.destroyed && this.info) return;
12519
- if (this.connecting) return this.connecting;
12520
- this.transition("connecting");
12521
- this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12522
- this.transition("error", { error });
12523
- throw error;
12524
- }).finally(() => {
12525
- this.connecting = null;
12584
+ return assigned;
12585
+ }
12586
+ async function runIndexerWithStore(store, opts) {
12587
+ const { projectRoot, langs, ignore = [], signal } = opts;
12588
+ const relationGraphVersion = "2";
12589
+ const refResolutionVersion = "2";
12590
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
12591
+ const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
12592
+ const startMs = Date.now();
12593
+ const errors = [];
12594
+ const langStats = {};
12595
+ let filesIndexed = 0;
12596
+ let symbolsIndexed = 0;
12597
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
12598
+ let files;
12599
+ let discoveredFiles = null;
12600
+ let discoveryComplete = true;
12601
+ let trustedUnchanged;
12602
+ if (opts.files && opts.files.length > 0) {
12603
+ files = opts.files.map((f) => path23.resolve(projectRoot, f)).filter((f) => {
12604
+ if (!isWithinProject(projectRoot, f)) return false;
12605
+ const rel = path23.relative(projectRoot, f).replace(/\\/g, "/");
12606
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path23.basename(f)) && !isGitIgnored(rel, false);
12526
12607
  });
12527
- return this.connecting;
12528
- }
12529
- async connectWithElection(spawnIfMissing) {
12530
- const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12531
- let spawned = false;
12532
- let staleAttempts = 0;
12533
- let lastError = new Error("codebase-index server unavailable");
12534
- while (Date.now() < deadline) {
12535
- try {
12536
- await this.connectOnce();
12537
- return;
12538
- } catch (error) {
12539
- lastError = error;
12540
- if (error instanceof StaleProjectIndexServerError) {
12541
- staleAttempts++;
12542
- if (!spawnIfMissing) break;
12543
- if (staleAttempts >= 3) this.forceKillServer(error.pid);
12544
- spawned = false;
12545
- await delay(100);
12546
- continue;
12547
- }
12548
- }
12549
- if (!spawnIfMissing) break;
12550
- if (!spawned) {
12551
- this.spawnDetachedServer();
12552
- spawned = true;
12553
- }
12554
- await delay(75);
12555
- }
12556
- throw lastError;
12608
+ } else {
12609
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
12610
+ files = discovery.files;
12611
+ errors.push(...discovery.errors);
12612
+ discoveryComplete = discovery.complete;
12613
+ discoveredFiles = new Set(files);
12614
+ trustedUnchanged = discovery.trustedUnchanged;
12557
12615
  }
12558
- connectOnce() {
12559
- this.socket?.destroy();
12560
- this.socket = null;
12561
- this.info = null;
12562
- this.activity = null;
12563
- this.health = null;
12564
- this.buffer = "";
12565
- return new Promise((resolve16, reject) => {
12566
- const socket = net3.createConnection(this.endpoint);
12567
- this.socket = socket;
12568
- socket.setEncoding("utf8");
12569
- const timer = setTimeout(() => {
12570
- reject(new Error("codebase-index server handshake timed out"));
12571
- socket.destroy();
12572
- }, CONNECT_ATTEMPT_TIMEOUT_MS);
12573
- timer.unref?.();
12574
- const finishResolve = () => {
12575
- clearTimeout(timer);
12576
- this.connectResolve = null;
12577
- this.connectReject = null;
12578
- resolve16();
12579
- };
12580
- const finishReject = (error) => {
12581
- clearTimeout(timer);
12582
- this.connectResolve = null;
12583
- this.connectReject = null;
12584
- reject(error);
12585
- };
12586
- this.connectResolve = finishResolve;
12587
- this.connectReject = finishReject;
12588
- socket.on("data", (chunk) => this.onData(socket, chunk));
12589
- socket.on("error", (error) => {
12590
- if (!this.info) finishReject(error);
12591
- });
12592
- socket.on("close", () => this.onClose(socket));
12616
+ if (langs && langs.length > 0) {
12617
+ const langSet = new Set(langs);
12618
+ files = files.filter((f) => {
12619
+ const lang = detectLang(f);
12620
+ return lang ? langSet.has(lang) : false;
12593
12621
  });
12594
12622
  }
12595
- onData(socket, chunk) {
12596
- if (socket !== this.socket) return;
12597
- this.buffer += chunk;
12598
- while (true) {
12599
- const newline = this.buffer.indexOf("\n");
12600
- if (newline < 0) {
12601
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12602
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12623
+ if (force) store.clearAll();
12624
+ const existingMeta = /* @__PURE__ */ new Map();
12625
+ if (!force) {
12626
+ for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
12627
+ }
12628
+ const totalFilesForProgress = files.length;
12629
+ let filesPreSkipped = 0;
12630
+ if (!force && trustedUnchanged) {
12631
+ files = files.filter((file) => {
12632
+ const meta = existingMeta.get(file);
12633
+ if (!meta || !trustedUnchanged.has(file)) return true;
12634
+ langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
12635
+ symbolsIndexed += meta.symbolCount;
12636
+ filesIndexed++;
12637
+ filesPreSkipped++;
12638
+ return false;
12639
+ });
12640
+ if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
12641
+ }
12642
+ const parallelBatch = resolveParallelBatch();
12643
+ let filesSinceLastYield = 0;
12644
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
12645
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
12646
+ const batchFiles = files.slice(batchStart, batchEnd);
12647
+ opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
12648
+ filesSinceLastYield += batchFiles.length;
12649
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
12650
+ filesSinceLastYield = 0;
12651
+ await yieldEventLoop();
12652
+ if (isFrugalPerf()) {
12653
+ await new Promise((r) => setTimeout(r, 8));
12654
+ }
12655
+ throwIfAborted(signal);
12656
+ }
12657
+ const statOpts = signal ? { signal } : {};
12658
+ const statReadParse = await Promise.allSettled(
12659
+ batchFiles.map(
12660
+ async (file) => {
12661
+ let stat17;
12662
+ try {
12663
+ stat17 = await fs17.stat(file, statOpts);
12664
+ } catch (e) {
12665
+ if (isAbortError(e)) throw e;
12666
+ return {
12667
+ file,
12668
+ stat: null,
12669
+ lang: "",
12670
+ parsed: null,
12671
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
12672
+ missing: isMissingPathError(e)
12673
+ };
12674
+ }
12675
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
12676
+ const lang = detectLang(file);
12677
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
12678
+ if (stat17.size > MAX_INDEX_FILE_BYTES) {
12679
+ return {
12680
+ file,
12681
+ stat: stat17,
12682
+ lang,
12683
+ parsed: null,
12684
+ error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12685
+ };
12686
+ }
12687
+ const meta = existingMeta.get(file);
12688
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
12689
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
12690
+ }
12691
+ let content;
12692
+ try {
12693
+ content = await fs17.readFile(file, { encoding: "utf8", signal });
12694
+ } catch (e) {
12695
+ if (isAbortError(e)) throw e;
12696
+ return {
12697
+ file,
12698
+ stat: stat17,
12699
+ lang,
12700
+ parsed: null,
12701
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
12702
+ };
12703
+ }
12704
+ let parsed;
12705
+ try {
12706
+ parsed = await parseFileContent(file, content, lang);
12707
+ } catch (e) {
12708
+ return {
12709
+ file,
12710
+ stat: stat17,
12711
+ lang,
12712
+ parsed: null,
12713
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
12714
+ };
12715
+ }
12716
+ return { file, stat: stat17, lang, parsed, content };
12603
12717
  }
12604
- return;
12718
+ )
12719
+ );
12720
+ const batchEntries = [];
12721
+ const deleteForFiles = [];
12722
+ for (let fi = 0; fi < statReadParse.length; fi++) {
12723
+ const settled = statReadParse[fi];
12724
+ const file = expectDefined6(batchFiles[fi]);
12725
+ if (settled.status === "rejected") {
12726
+ const err = settled.reason;
12727
+ if (err instanceof Error && isAbortError(err)) throw err;
12728
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
12729
+ continue;
12605
12730
  }
12606
- if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12607
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12608
- return;
12731
+ const result = settled.value;
12732
+ if (result.error) {
12733
+ if (result.missing) store.deleteFile(file);
12734
+ errors.push(`${file}: ${result.error}`);
12735
+ continue;
12609
12736
  }
12610
- const line = this.buffer.slice(0, newline);
12611
- this.buffer = this.buffer.slice(newline + 1);
12612
- if (!line) continue;
12613
- let message;
12614
- try {
12615
- message = JSON.parse(line);
12616
- } catch {
12617
- socket.destroy(new Error("invalid codebase-index server response"));
12618
- return;
12737
+ const { stat: stat17, lang, parsed } = result;
12738
+ if (result.skippedMeta) {
12739
+ langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
12740
+ symbolsIndexed += result.skippedMeta.symbolCount;
12741
+ filesIndexed++;
12742
+ continue;
12619
12743
  }
12620
- this.onMessage(message);
12621
- }
12622
- }
12623
- onMessage(message) {
12624
- if (message.type === "hello") {
12625
- if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12626
- this.rejectStaleServer(
12627
- message,
12628
- `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12629
- );
12630
- return;
12744
+ if (!lang || !parsed) {
12745
+ if (lang) {
12746
+ store.upsertFile({
12747
+ file,
12748
+ lang,
12749
+ mtimeMs: Math.floor(stat17.mtimeMs),
12750
+ symbolCount: 0,
12751
+ lastIndexed: Date.now()
12752
+ });
12753
+ filesIndexed++;
12754
+ }
12755
+ continue;
12631
12756
  }
12632
- const expectedBuildId = projectIndexServerExpectedBuildId();
12633
- if (expectedBuildId && message.buildId !== expectedBuildId) {
12634
- this.rejectStaleServer(
12635
- message,
12636
- `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12637
- );
12638
- return;
12757
+ if (parsed.symbols.length === 0) {
12758
+ store.replaceEmptyFile({
12759
+ file,
12760
+ lang,
12761
+ mtimeMs: Math.floor(stat17.mtimeMs),
12762
+ symbolCount: 0,
12763
+ lastIndexed: Date.now()
12764
+ });
12765
+ filesIndexed++;
12766
+ continue;
12639
12767
  }
12640
- this.info = message;
12641
- this.markResponsive();
12642
- this.transition("connected", { pid: message.pid });
12643
- ensureHeartbeatLoop();
12644
- this.connectResolve?.();
12645
- return;
12646
- }
12647
- if (message.type === "index-state") {
12648
- this.activity = message.state;
12649
- this.markResponsive();
12650
- this.transition("connected", { pid: this.info?.pid });
12651
- return;
12652
- }
12653
- const entry = this.pending.get(message.id);
12654
- if (!entry) return;
12655
- this.markResponsive();
12656
- const status = connectionStates.get(this.endpoint)?.status;
12657
- if (status === "degraded" || status === "unresponsive") {
12658
- this.transition("connected", { pid: this.info?.pid });
12659
- }
12660
- if (message.type === "progress") {
12661
- entry.onProgress?.(message.current, message.total);
12662
- return;
12663
- }
12664
- this.pending.delete(message.id);
12665
- this.cleanupPending(entry);
12666
- if (message.ok) entry.resolve(message.result);
12667
- else entry.reject(remoteError(message.error, message.errorName));
12668
- }
12669
- onClose(socket) {
12670
- if (socket !== this.socket) return;
12671
- const wasConnected = this.info !== null;
12672
- this.socket = null;
12673
- this.info = null;
12674
- this.activity = null;
12675
- this.health = null;
12676
- const error = new Error("codebase-index server connection closed");
12677
- this.connectReject?.(error);
12678
- this.connectResolve = null;
12679
- this.connectReject = null;
12680
- this.rejectPending(error);
12681
- if (wasConnected) this.transition("error", { error });
12682
- maybeStopHeartbeatLoop();
12683
- }
12684
- cleanupPending(entry) {
12685
- clearTimeout(entry.timer);
12686
- if (entry.signal && entry.onAbort) {
12687
- entry.signal.removeEventListener("abort", entry.onAbort);
12688
- }
12689
- }
12690
- rejectPending(error) {
12691
- const entries = [...this.pending.values()];
12692
- this.pending.clear();
12693
- for (const entry of entries) {
12694
- this.cleanupPending(entry);
12695
- entry.reject(error);
12768
+ batchEntries.push({
12769
+ file,
12770
+ lang,
12771
+ symbols: parsed.symbols,
12772
+ refs: parsed.refs ?? [],
12773
+ mtimeMs: Math.floor(stat17.mtimeMs),
12774
+ symbolCount: parsed.symbols.length
12775
+ });
12776
+ deleteForFiles.push(file);
12696
12777
  }
12697
- }
12698
- write(message) {
12699
- const socket = this.socket;
12700
- if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
12701
- }
12702
- rejectStaleServer(message, reason) {
12703
- const socket = this.socket;
12704
- if (socket && !socket.destroyed) {
12705
- socket.write(
12706
- encodeProjectServerMessage({
12707
- type: "shutdown",
12708
- id: 0,
12709
- reason: "stale-build-replacement"
12710
- })
12711
- );
12712
- const timer = setTimeout(() => socket.destroy(), 25);
12713
- timer.unref?.();
12778
+ if (batchEntries.length > 0) {
12779
+ try {
12780
+ store.commitBatch(batchEntries, { deleteForFiles });
12781
+ for (const entry of batchEntries) {
12782
+ const count = entry.symbols.length;
12783
+ symbolsIndexed += count;
12784
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
12785
+ filesIndexed++;
12786
+ }
12787
+ } catch (err) {
12788
+ const message = err instanceof Error ? err.message : String(err);
12789
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
12790
+ for (const entry of batchEntries) {
12791
+ try {
12792
+ store.deleteRefsForFile(entry.file);
12793
+ store.deleteSymbolsForFile(entry.file);
12794
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
12795
+ symbolsIndexed += symbolsWithIds.length;
12796
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
12797
+ filesIndexed++;
12798
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
12799
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
12800
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
12801
+ }
12802
+ store.resolveRefsForNames([
12803
+ ...entry.symbols.map((symbol) => symbol.name),
12804
+ ...entry.refs.map((ref) => ref.toName)
12805
+ ]);
12806
+ store.upsertFile({
12807
+ file: entry.file,
12808
+ lang: entry.lang,
12809
+ mtimeMs: entry.mtimeMs,
12810
+ symbolCount: entry.symbolCount,
12811
+ lastIndexed: Date.now()
12812
+ });
12813
+ } catch (innerErr) {
12814
+ errors.push(
12815
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
12816
+ );
12817
+ }
12818
+ }
12819
+ }
12714
12820
  }
12715
- this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
12716
12821
  }
12717
- spawnDetachedServer() {
12718
- const url = resolveProjectServerUrl();
12719
- if (!url) throw new Error("built codebase-index project server is unavailable");
12720
- if (process.platform !== "win32") {
12721
- try {
12722
- fs17.rmSync(this.endpoint, { force: true });
12723
- } catch {
12822
+ if (discoveredFiles && discoveryComplete) {
12823
+ for (const [file_] of existingMeta) {
12824
+ if (!discoveredFiles.has(file_)) {
12825
+ store.deleteFile(file_);
12724
12826
  }
12725
12827
  }
12726
- const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
12727
- if (this.indexDir) args.push("--index-dir", this.indexDir);
12728
- const child = spawn7(process.execPath, args, {
12729
- detached: true,
12730
- stdio: "ignore",
12731
- windowsHide: true,
12732
- env: process.env
12828
+ }
12829
+ if (needsFullRefResolution) store.resolveRefs();
12830
+ store.setMetadata("ref_resolution_version", refResolutionVersion);
12831
+ store.setMetadata("relation_graph_version", relationGraphVersion);
12832
+ if (!opts.files || filesIndexed >= 50) store.optimize();
12833
+ store.setLastIndexed(Date.now());
12834
+ if (!opts.files) store.compactIfNeeded();
12835
+ const durationMs = Date.now() - startMs;
12836
+ return {
12837
+ filesIndexed,
12838
+ symbolsIndexed,
12839
+ langStats,
12840
+ durationMs,
12841
+ errors
12842
+ };
12843
+ }
12844
+
12845
+ // src/codebase-index/index-service.ts
12846
+ async function indexService(args, hooks = {}) {
12847
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12848
+ try {
12849
+ return await runIndexerWithStore(store, {
12850
+ projectRoot: args.projectRoot,
12851
+ indexDir: args.indexDir,
12852
+ files: args.files,
12853
+ force: args.force,
12854
+ langs: args.langs,
12855
+ ignore: args.ignore,
12856
+ signal: hooks.signal,
12857
+ onProgress: hooks.onProgress
12733
12858
  });
12734
- child.unref();
12859
+ } finally {
12860
+ indexStorePool.release(store);
12735
12861
  }
12736
- forceKillKnownServer() {
12737
- const pid = this.info?.pid;
12738
- return pid ? this.forceKillServer(pid) : false;
12862
+ }
12863
+ function searchService(args) {
12864
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12865
+ try {
12866
+ return store.searchRanked(
12867
+ args.query,
12868
+ {
12869
+ kind: args.kind,
12870
+ lang: args.lang,
12871
+ file: args.file,
12872
+ lspKind: args.lspKind
12873
+ },
12874
+ args.limit
12875
+ );
12876
+ } finally {
12877
+ indexStorePool.release(store);
12739
12878
  }
12740
- forceKillServer(pid) {
12741
- if (pid === process.pid) return false;
12742
- try {
12743
- process.kill(pid);
12744
- const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12745
- try {
12746
- const metadata = JSON.parse(fs17.readFileSync(metadataPath, "utf8"));
12747
- if (metadata.pid === pid) fs17.rmSync(metadataPath, { force: true });
12748
- } catch {
12749
- }
12750
- return true;
12751
- } catch {
12752
- return false;
12753
- }
12879
+ }
12880
+ function statsService(args) {
12881
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12882
+ try {
12883
+ return store.getStats();
12884
+ } finally {
12885
+ indexStorePool.release(store);
12754
12886
  }
12755
- };
12756
- var connections = /* @__PURE__ */ new Map();
12757
- var heartbeatTimer;
12758
- function ensureHeartbeatLoop() {
12759
- if (heartbeatTimer) return;
12760
- heartbeatTimer = setInterval(() => {
12761
- for (const connection of connections.values()) {
12762
- if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
12763
- });
12764
- }
12765
- }, SERVER_HEARTBEAT_INTERVAL_MS);
12766
- heartbeatTimer.unref?.();
12767
12887
  }
12768
- function maybeStopHeartbeatLoop() {
12769
- if (!heartbeatTimer) return;
12770
- if ([...connections.values()].some((connection) => connection.isConnected())) return;
12771
- clearInterval(heartbeatTimer);
12772
- heartbeatTimer = void 0;
12888
+ function packageGraphService(args) {
12889
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12890
+ try {
12891
+ return store.getPackageGraph();
12892
+ } finally {
12893
+ indexStorePool.release(store);
12894
+ }
12773
12895
  }
12774
- function connectionFor(projectRoot, indexDir) {
12775
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12776
- let connection = connections.get(endpoint);
12777
- if (!connection) {
12778
- connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
12779
- connections.set(endpoint, connection);
12896
+ function fileGraphService(args) {
12897
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12898
+ try {
12899
+ return store.getFileGraph(args.packageFilter);
12900
+ } finally {
12901
+ indexStorePool.release(store);
12780
12902
  }
12781
- return connection;
12782
12903
  }
12783
- function callProjectIndexServer(op, args, options) {
12784
- return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
12904
+ function symbolGraphService(args) {
12905
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12906
+ try {
12907
+ return store.getSymbolGraph(args.fileFilter);
12908
+ } finally {
12909
+ indexStorePool.release(store);
12910
+ }
12785
12911
  }
12786
12912
 
12787
12913
  // src/codebase-index/background-indexer.ts
@@ -12892,8 +13018,17 @@ function terminateWorker(reason) {
12892
13018
  if (w) void w.terminate().catch(() => {
12893
13019
  });
12894
13020
  }
13021
+ var warnedInvalidEndpoints = /* @__PURE__ */ new Set();
13022
+ function warnEndpointInvalidOnce(availability) {
13023
+ if (warnedInvalidEndpoints.has(availability.endpoint)) return;
13024
+ warnedInvalidEndpoints.add(availability.endpoint);
13025
+ process.stderr.write(
13026
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Subsequent calls will reject until TMPDIR is shortened to restore the shared daemon.
13027
+ `
13028
+ );
13029
+ }
12895
13030
  function callIndexOp(op, args, opts) {
12896
- const availability = resolveProjectIndexDaemonAvailability();
13031
+ const availability = resolveProjectIndexDaemonAvailability(args.projectRoot, args.indexDir);
12897
13032
  if (availability.kind === "available") {
12898
13033
  return callProjectIndexServer(op, args, opts);
12899
13034
  }
@@ -12904,6 +13039,14 @@ function callIndexOp(op, args, opts) {
12904
13039
  )
12905
13040
  );
12906
13041
  }
13042
+ if (availability.kind === "endpoint-invalid") {
13043
+ warnEndpointInvalidOnce(availability);
13044
+ return Promise.reject(
13045
+ new Error(
13046
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Set a shorter TMPDIR to relocate the shared daemon, or set WRONGSTACK_INDEX_INLINE=1 to explicitly opt into a process-local index.`
13047
+ )
13048
+ );
13049
+ }
12907
13050
  const w = ensureWorker();
12908
13051
  if (!w) return callInline(op, args, opts);
12909
13052
  if (opts.signal?.aborted) {
@@ -13349,6 +13492,7 @@ var codebaseStatsTool = {
13349
13492
  };
13350
13493
 
13351
13494
  // src/codebase-index/dead-code-scan.ts
13495
+ init_languages2();
13352
13496
  import * as fs19 from "node:fs";
13353
13497
  import * as path24 from "node:path";
13354
13498
  var deadCodeScanTool = {
@@ -13417,7 +13561,15 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
13417
13561
  if (rootPkg) {
13418
13562
  addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
13419
13563
  }
13420
- const workspaces = rootPkg ? extractWorkspaceGlobs(rootPkg, projectRoot) : [];
13564
+ let workspaces;
13565
+ if (rootPkg) {
13566
+ workspaces = extractWorkspaceGlobs(rootPkg, projectRoot);
13567
+ if (workspaces.length === 0) {
13568
+ workspaces = extractPnpmWorkspaceDirs(projectRoot);
13569
+ }
13570
+ } else {
13571
+ workspaces = [];
13572
+ }
13421
13573
  for (const wsDir of workspaces) {
13422
13574
  const pkgJsonPath = path24.join(wsDir, "package.json");
13423
13575
  const pkg = tryReadJson(pkgJsonPath);
@@ -13439,77 +13591,189 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
13439
13591
  }
13440
13592
  return [...entries];
13441
13593
  }
13594
+ var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
13595
+ var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path24.sep}${d}${path24.sep}`);
13596
+ function trySourceEquivalent(resolved) {
13597
+ resolved = resolved.replace(/[/\\]/g, path24.sep);
13598
+ for (const marker of BUILD_OUTPUT_DIR_NAMES) {
13599
+ const idx = resolved.indexOf(marker);
13600
+ if (idx === -1) continue;
13601
+ const base = resolved.replace(marker, `${path24.sep}src${path24.sep}`);
13602
+ const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
13603
+ if (candidate !== base && fs19.existsSync(candidate)) {
13604
+ return candidate;
13605
+ }
13606
+ const dtsStripped = base.replace(/\.d\.ts$/, "");
13607
+ const candidateDts = dtsStripped + ".ts";
13608
+ if (candidateDts !== base && candidateDts !== candidate && fs19.existsSync(candidateDts)) {
13609
+ return candidateDts;
13610
+ }
13611
+ const candidateNoExt = base + ".ts";
13612
+ if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs19.existsSync(candidateNoExt)) {
13613
+ return candidateNoExt;
13614
+ }
13615
+ }
13616
+ return null;
13617
+ }
13618
+ function tryAddEntryPath(pkgDir, rawPath, entries) {
13619
+ const resolved = resolveAgainst(pkgDir, rawPath);
13620
+ if (fs19.existsSync(resolved)) entries.add(resolved);
13621
+ const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
13622
+ if (tsResolved !== resolved && fs19.existsSync(tsResolved)) {
13623
+ entries.add(tsResolved);
13624
+ }
13625
+ const srcAlt = trySourceEquivalent(resolved);
13626
+ if (srcAlt) entries.add(srcAlt);
13627
+ }
13442
13628
  function addPkgJsonEntryPoints(pkgDir, pkg, entries) {
13443
13629
  if (typeof pkg.main === "string") {
13444
- const resolved = resolveAgainst(pkgDir, pkg.main);
13445
- if (fs19.existsSync(resolved)) entries.add(resolved);
13446
- const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
13447
- if (tsResolved !== resolved && fs19.existsSync(tsResolved)) {
13448
- entries.add(tsResolved);
13449
- }
13630
+ tryAddEntryPath(pkgDir, pkg.main, entries);
13450
13631
  }
13451
13632
  const bin = pkg.bin;
13452
13633
  if (typeof bin === "string") {
13453
- const resolved = resolveAgainst(pkgDir, bin);
13454
- if (fs19.existsSync(resolved)) entries.add(resolved);
13634
+ tryAddEntryPath(pkgDir, bin, entries);
13455
13635
  } else if (bin && typeof bin === "object") {
13456
13636
  for (const value of Object.values(bin)) {
13457
13637
  if (typeof value === "string") {
13458
- const resolved = resolveAgainst(pkgDir, value);
13459
- if (fs19.existsSync(resolved)) entries.add(resolved);
13638
+ tryAddEntryPath(pkgDir, value, entries);
13460
13639
  }
13461
13640
  }
13462
13641
  }
13463
13642
  for (const key of ["types", "typings"]) {
13464
13643
  if (typeof pkg[key] === "string") {
13465
- const resolved = resolveAgainst(pkgDir, pkg[key]);
13466
- if (fs19.existsSync(resolved)) entries.add(resolved);
13644
+ tryAddEntryPath(pkgDir, pkg[key], entries);
13467
13645
  }
13468
13646
  }
13469
13647
  const exports_ = pkg.exports;
13470
13648
  if (exports_ && typeof exports_ === "object") {
13471
13649
  for (const value of Object.values(exports_)) {
13472
13650
  if (typeof value === "string") {
13473
- const resolved = resolveAgainst(pkgDir, value);
13474
- if (fs19.existsSync(resolved)) entries.add(resolved);
13651
+ tryAddEntryPath(pkgDir, value, entries);
13475
13652
  } else if (value && typeof value === "object") {
13476
- for (const nested of Object.values(
13477
- value
13478
- )) {
13653
+ for (const nested of Object.values(value)) {
13479
13654
  if (typeof nested === "string") {
13480
- const resolved = resolveAgainst(pkgDir, nested);
13481
- if (fs19.existsSync(resolved)) entries.add(resolved);
13655
+ tryAddEntryPath(pkgDir, nested, entries);
13482
13656
  }
13483
13657
  }
13484
13658
  }
13485
13659
  }
13486
13660
  }
13487
13661
  }
13662
+ function expandGlobPattern(entry, projectRoot) {
13663
+ const dirs = [];
13664
+ if (entry.includes("*")) {
13665
+ const base = entry.replace(/\/\*+$/, "");
13666
+ const baseDir = path24.resolve(projectRoot, base);
13667
+ try {
13668
+ const children = fs19.readdirSync(baseDir, { withFileTypes: true });
13669
+ for (const child of children) {
13670
+ if (child.isDirectory()) {
13671
+ dirs.push(path24.join(baseDir, child.name));
13672
+ }
13673
+ }
13674
+ } catch {
13675
+ }
13676
+ } else {
13677
+ dirs.push(path24.resolve(projectRoot, entry));
13678
+ }
13679
+ return dirs;
13680
+ }
13488
13681
  function extractWorkspaceGlobs(pkg, projectRoot) {
13489
13682
  const dirs = [];
13490
13683
  const workspaces = pkg.workspaces;
13491
13684
  if (Array.isArray(workspaces)) {
13492
13685
  for (const entry of workspaces) {
13493
13686
  if (typeof entry === "string") {
13494
- if (entry.includes("*")) {
13495
- const base = entry.replace(/\/\*+$/, "");
13496
- const baseDir = path24.resolve(projectRoot, base);
13497
- try {
13498
- const children = fs19.readdirSync(baseDir, { withFileTypes: true });
13499
- for (const child of children) {
13500
- if (child.isDirectory()) {
13501
- dirs.push(path24.join(baseDir, child.name));
13502
- }
13503
- }
13504
- } catch {
13687
+ dirs.push(...expandGlobPattern(entry, projectRoot));
13688
+ }
13689
+ }
13690
+ }
13691
+ return dirs;
13692
+ }
13693
+ function extractPnpmWorkspaceDirs(projectRoot) {
13694
+ const yamlPath = path24.join(projectRoot, "pnpm-workspace.yaml");
13695
+ if (!fs19.existsSync(yamlPath)) return [];
13696
+ try {
13697
+ const content = fs19.readFileSync(yamlPath, "utf8");
13698
+ const dirs = [];
13699
+ let inPackages = false;
13700
+ const lines = content.split("\n");
13701
+ const itemRe = /^\s+-\s+"([^"]+)"|^\s+-\s+'([^']+)'|^\s+-\s+(\S+)/;
13702
+ for (const line of lines) {
13703
+ const trimmed = line.trim();
13704
+ if (/^packages\s*:\s*$/.test(trimmed)) {
13705
+ inPackages = true;
13706
+ continue;
13707
+ }
13708
+ if (inPackages && trimmed.length > 0 && !line.startsWith(" ") && !line.startsWith(" ")) {
13709
+ if (!trimmed.startsWith("-")) {
13710
+ inPackages = false;
13711
+ continue;
13712
+ }
13713
+ }
13714
+ if (inPackages) {
13715
+ const m = itemRe.exec(line);
13716
+ if (m) {
13717
+ const entry = m[1] ?? m[2] ?? m[3];
13718
+ if (entry) {
13719
+ dirs.push(...expandGlobPattern(entry, projectRoot));
13505
13720
  }
13506
- } else {
13507
- dirs.push(path24.resolve(projectRoot, entry));
13508
13721
  }
13509
13722
  }
13510
13723
  }
13724
+ return dirs;
13725
+ } catch {
13726
+ return [];
13511
13727
  }
13512
- return dirs;
13728
+ }
13729
+ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
13730
+ if (!moduleSpecifier.startsWith(".")) return [];
13731
+ const dir = path24.dirname(importerPath);
13732
+ const base = path24.resolve(dir, moduleSpecifier);
13733
+ const results = [];
13734
+ const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
13735
+ const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
13736
+ const candidates = skipBase ? [stripped] : [base];
13737
+ for (const candidate of candidates) {
13738
+ if (indexedFiles.has(candidate + ".ts")) results.push(candidate + ".ts");
13739
+ if (indexedFiles.has(candidate + ".tsx")) results.push(candidate + ".tsx");
13740
+ if (indexedFiles.has(candidate + ".js")) results.push(candidate + ".js");
13741
+ if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
13742
+ if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
13743
+ if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
13744
+ if (indexedFiles.has(path24.join(candidate, "index.ts")))
13745
+ results.push(path24.join(candidate, "index.ts"));
13746
+ if (indexedFiles.has(path24.join(candidate, "index.tsx")))
13747
+ results.push(path24.join(candidate, "index.tsx"));
13748
+ if (indexedFiles.has(path24.join(candidate, "index.js")))
13749
+ results.push(path24.join(candidate, "index.js"));
13750
+ if (indexedFiles.has(path24.join(candidate, "index.jsx")))
13751
+ results.push(path24.join(candidate, "index.jsx"));
13752
+ if (indexedFiles.has(path24.join(candidate, "index.mjs")))
13753
+ results.push(path24.join(candidate, "index.mjs"));
13754
+ if (indexedFiles.has(path24.join(candidate, "index.cjs")))
13755
+ results.push(path24.join(candidate, "index.cjs"));
13756
+ }
13757
+ return [...new Set(results)];
13758
+ }
13759
+ function parseNamedExportSymbols(matchText) {
13760
+ const braceStart = matchText.indexOf("{");
13761
+ if (braceStart === -1) return null;
13762
+ const braceEnd = matchText.indexOf("}", braceStart);
13763
+ if (braceEnd === -1) return null;
13764
+ const inner = matchText.slice(braceStart + 1, braceEnd);
13765
+ const symbols = [];
13766
+ for (const part of inner.split(",")) {
13767
+ let s = part.trim();
13768
+ if (!s) continue;
13769
+ s = s.replace(/^type\s+/, "");
13770
+ const asIdx = s.search(/\s+as\s+/);
13771
+ if (asIdx !== -1) {
13772
+ s = s.slice(0, asIdx).trim();
13773
+ }
13774
+ if (s) symbols.push(s);
13775
+ }
13776
+ return symbols;
13513
13777
  }
13514
13778
  function runDeadCodeScan(projectRoot, opts = {}) {
13515
13779
  const store = opts.store ?? indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
@@ -13531,12 +13795,71 @@ function runDeadCodeScan(projectRoot, opts = {}) {
13531
13795
  }
13532
13796
  const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
13533
13797
  const entryFileSet = new Set(discoveredFiles.map((f) => path24.resolve(f)));
13798
+ const indexedFiles = /* @__PURE__ */ new Set();
13799
+ for (const s of allSymbols) indexedFiles.add(s.file);
13800
+ for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
13534
13801
  const seedIds = /* @__PURE__ */ new Set();
13535
13802
  for (const s of allSymbols) {
13536
13803
  if (entryFileSet.has(s.file)) {
13537
13804
  seedIds.add(s.id);
13538
13805
  }
13539
13806
  }
13807
+ const fileToSymbolIds = /* @__PURE__ */ new Map();
13808
+ for (const s of allSymbols) {
13809
+ let byFile = fileToSymbolIds.get(s.file);
13810
+ if (!byFile) {
13811
+ byFile = [];
13812
+ fileToSymbolIds.set(s.file, byFile);
13813
+ }
13814
+ byFile.push(s.id);
13815
+ }
13816
+ const scannedBarrels = /* @__PURE__ */ new Set();
13817
+ const barrelWorkList = [...entryFileSet];
13818
+ while (barrelWorkList.length > 0) {
13819
+ const epFile = barrelWorkList.pop();
13820
+ if (scannedBarrels.has(epFile)) continue;
13821
+ scannedBarrels.add(epFile);
13822
+ try {
13823
+ const content = fs19.readFileSync(epFile, "utf8");
13824
+ const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
13825
+ const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
13826
+ let match;
13827
+ while ((match = reExportRe.exec(strippedContent)) !== null) {
13828
+ const moduleSpec = match[1];
13829
+ const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
13830
+ for (const rf of resolvedFiles) {
13831
+ const fileSyms = fileToSymbolIds.get(rf);
13832
+ if (fileSyms) {
13833
+ const namedSymbols = parseNamedExportSymbols(match[0]);
13834
+ if (namedSymbols) {
13835
+ const nameSet = new Set(namedSymbols);
13836
+ for (const sid of fileSyms) {
13837
+ const sym = symbolById.get(sid);
13838
+ if (sym && nameSet.has(sym.name)) seedIds.add(sid);
13839
+ }
13840
+ } else {
13841
+ for (const sid of fileSyms) seedIds.add(sid);
13842
+ }
13843
+ }
13844
+ if (!scannedBarrels.has(rf)) {
13845
+ barrelWorkList.push(rf);
13846
+ }
13847
+ }
13848
+ }
13849
+ } catch (err) {
13850
+ if (err instanceof Error && err.code !== "ENOENT") {
13851
+ console.warn(
13852
+ JSON.stringify({
13853
+ level: "warn",
13854
+ event: "dead_code_scan_barrel_read_failed",
13855
+ message: err.message,
13856
+ file: epFile,
13857
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13858
+ })
13859
+ );
13860
+ }
13861
+ }
13862
+ }
13540
13863
  const alive = new Set(seedIds);
13541
13864
  const frontier = [...seedIds];
13542
13865
  const visitedEdges = /* @__PURE__ */ new Set();
@@ -13570,8 +13893,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
13570
13893
  dead.push({
13571
13894
  name: s.name,
13572
13895
  kind: s.kind,
13573
- lang: "ts",
13574
- // populated from symbol file metadata
13896
+ lang: detectLang(s.file) ?? "ts",
13575
13897
  file: s.file,
13576
13898
  line: s.line,
13577
13899
  reason
@@ -13596,16 +13918,14 @@ function runDeadCodeScan(projectRoot, opts = {}) {
13596
13918
  deadFiles.push({
13597
13919
  file,
13598
13920
  symbolCount: syms.length,
13599
- lang: syms[0]?.kind ?? "ts"
13921
+ lang: detectLang(file) ?? "ts"
13600
13922
  });
13601
13923
  }
13602
13924
  }
13603
13925
  const deadPackages = [];
13604
13926
  const pkgEntries = findPackageEntries(projectRoot);
13605
13927
  for (const [pkgName, pkgDir] of pkgEntries) {
13606
- const pkgFiles = allSymbols.filter(
13607
- (s) => s.file.startsWith(pkgDir + path24.sep)
13608
- );
13928
+ const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path24.sep));
13609
13929
  if (pkgFiles.length === 0) continue;
13610
13930
  const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
13611
13931
  if (pkgUsed.length === 0) {
@@ -13646,7 +13966,10 @@ function findPackageEntries(projectRoot) {
13646
13966
  pkgMap.set(rootPkg.name, projectRoot);
13647
13967
  }
13648
13968
  if (rootPkg) {
13649
- const wsDirs = extractWorkspaceGlobs(rootPkg, projectRoot);
13969
+ let wsDirs = extractWorkspaceGlobs(rootPkg, projectRoot);
13970
+ if (wsDirs.length === 0) {
13971
+ wsDirs = extractPnpmWorkspaceDirs(projectRoot);
13972
+ }
13650
13973
  for (const wsDir of wsDirs) {
13651
13974
  const wsPkg = tryReadJson(path24.join(wsDir, "package.json"));
13652
13975
  if (wsPkg && typeof wsPkg.name === "string") {
@@ -14263,7 +14586,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
14263
14586
 
14264
14587
  // src/e2e.ts
14265
14588
  init_util();
14266
- import { open, readdir as readdir5 } from "node:fs/promises";
14589
+ import { open, readdir as readdir6 } from "node:fs/promises";
14267
14590
  import * as path27 from "node:path";
14268
14591
  async function readBoundedText(filePath, maxBytes) {
14269
14592
  let handle;
@@ -14381,7 +14704,7 @@ async function scanWorkspace(root, maxDepth, signal) {
14381
14704
  }
14382
14705
  let entries;
14383
14706
  try {
14384
- entries = await readdir5(current.directory, { withFileTypes: true });
14707
+ entries = await readdir6(current.directory, { withFileTypes: true });
14385
14708
  } catch {
14386
14709
  continue;
14387
14710
  }
@@ -14440,7 +14763,7 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
14440
14763
  while (true) {
14441
14764
  const names = /* @__PURE__ */ new Set();
14442
14765
  try {
14443
- for (const entry of await readdir5(directory)) names.add(entry);
14766
+ for (const entry of await readdir6(directory)) names.add(entry);
14444
14767
  } catch {
14445
14768
  }
14446
14769
  if (names.has("pnpm-lock.yaml")) return "pnpm";
@@ -14508,7 +14831,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
14508
14831
  if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
14509
14832
  let entries;
14510
14833
  try {
14511
- entries = await readdir5(directory, { withFileTypes: true });
14834
+ entries = await readdir6(directory, { withFileTypes: true });
14512
14835
  } catch {
14513
14836
  continue;
14514
14837
  }
@@ -17217,7 +17540,7 @@ async function detectFixer(cwd) {
17217
17540
  init_util();
17218
17541
  import { spawn as spawn10 } from "node:child_process";
17219
17542
  import { statSync as statSync4 } from "node:fs";
17220
- import { dirname as dirname13, resolve as resolve13, sep as sep6 } from "node:path";
17543
+ import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
17221
17544
  import { assessCommitSafety } from "@wrongstack/core/coordination";
17222
17545
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
17223
17546
  var TIMEOUT_MS2 = 3e4;
@@ -17379,7 +17702,7 @@ function findGitDir2(cwd, projectRoot) {
17379
17702
  } catch {
17380
17703
  }
17381
17704
  if (dir === root) break;
17382
- const parent = dirname13(dir);
17705
+ const parent = dirname14(dir);
17383
17706
  if (parent === dir) break;
17384
17707
  dir = parent;
17385
17708
  }
@@ -21477,12 +21800,13 @@ init_util();
21477
21800
  import * as fs28 from "node:fs/promises";
21478
21801
  import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
21479
21802
  import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
21803
+ var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
21480
21804
  var MAX_BYTES2 = 5 * 1024 * 1024;
21481
21805
  var readTool = {
21482
21806
  name: "read",
21483
21807
  category: "Filesystem",
21484
- description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
21485
- usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
21808
+ description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
21809
+ usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
21486
21810
  selection: {
21487
21811
  doNotUseWhen: "you need to search many files for matching content.",
21488
21812
  useInstead: ["grep"]
@@ -21512,11 +21836,15 @@ var readTool = {
21512
21836
  type: "string",
21513
21837
  enum: ["content", "summary"],
21514
21838
  description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
21839
+ },
21840
+ includeSymbols: {
21841
+ type: "boolean",
21842
+ description: "When true, include the codebase-index symbol list for this file as a structured `symbols` field in the result. Overrides the advanced-mode meta flag per-call."
21515
21843
  }
21516
21844
  },
21517
21845
  required: ["path"]
21518
21846
  },
21519
- async execute(input, ctx) {
21847
+ async execute(input, ctx, execOpts) {
21520
21848
  if (!input?.path) {
21521
21849
  throw new ToolValidationError5({
21522
21850
  message: "read: path is required",
@@ -21524,6 +21852,7 @@ var readTool = {
21524
21852
  });
21525
21853
  }
21526
21854
  const absPath = await safeResolveReal(input.path, ctx);
21855
+ const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
21527
21856
  let stat17;
21528
21857
  try {
21529
21858
  stat17 = await fs28.stat(absPath);
@@ -21567,13 +21896,15 @@ var readTool = {
21567
21896
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
21568
21897
  if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
21569
21898
  ctx.recordRead(absPath, stat17.mtimeMs);
21899
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21570
21900
  return {
21571
21901
  text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat17.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
21572
21902
  total_lines: prior.totalLines,
21573
21903
  encoding: "utf8",
21574
21904
  truncated: requestedEnd < prior.totalLines,
21575
21905
  cached: true,
21576
- note: "Repeated read suppressed to save tokens."
21906
+ note: mergeSymbolNote("Repeated read suppressed to save tokens.", symResult2?.note),
21907
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
21577
21908
  };
21578
21909
  }
21579
21910
  const buf = await fs28.readFile(absPath);
@@ -21587,27 +21918,43 @@ var readTool = {
21587
21918
  if (input.mode === "summary") {
21588
21919
  ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21589
21920
  rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
21921
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21590
21922
  return {
21591
21923
  text: summarizeFile(input.path, stat17.size, allLines),
21592
21924
  total_lines: total,
21593
21925
  encoding: "utf8",
21594
21926
  truncated: total > 200,
21595
- note: "Summary mode returned compact structure instead of full file content."
21927
+ note: mergeSymbolNote(
21928
+ "Summary mode returned compact structure instead of full file content.",
21929
+ symResult2?.note
21930
+ ),
21931
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
21596
21932
  };
21597
21933
  }
21598
21934
  if (limit === 0) {
21599
21935
  ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21600
21936
  rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
21601
- return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
21937
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21938
+ return {
21939
+ text: "",
21940
+ total_lines: total,
21941
+ encoding: "utf8",
21942
+ truncated: total > 0,
21943
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {},
21944
+ ...symResult2?.note ? { note: symResult2.note } : {}
21945
+ };
21602
21946
  }
21603
21947
  if (offset > total) {
21604
21948
  ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21605
21949
  rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
21950
+ const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21606
21951
  return {
21607
21952
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
21608
21953
  total_lines: total,
21609
21954
  encoding: "utf8",
21610
- truncated: false
21955
+ truncated: false,
21956
+ ...symResult2?.symbols ? { symbols: symResult2.symbols } : {},
21957
+ ...symResult2?.note ? { note: symResult2.note } : {}
21611
21958
  };
21612
21959
  }
21613
21960
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
@@ -21616,14 +21963,53 @@ var readTool = {
21616
21963
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
21617
21964
  ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21618
21965
  rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
21966
+ const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21619
21967
  return {
21620
21968
  text: numbered,
21621
21969
  total_lines: total,
21622
21970
  encoding: "utf8",
21623
- truncated
21971
+ truncated,
21972
+ ...symResult?.symbols ? { symbols: symResult.symbols } : {},
21973
+ ...symResult?.note ? { note: symResult.note } : {}
21624
21974
  };
21625
21975
  }
21626
21976
  };
21977
+ async function fetchSymbolsForFile(absPath, ctx, signal) {
21978
+ try {
21979
+ const state = getIndexState();
21980
+ if (!state.ready) return {};
21981
+ const { results, total } = await searchCodebaseIndex(
21982
+ {
21983
+ projectRoot: ctx.projectRoot,
21984
+ indexDir: codebaseIndexDirOverride(ctx),
21985
+ query: "",
21986
+ file: absPath,
21987
+ limit: 500
21988
+ },
21989
+ { signal }
21990
+ );
21991
+ if (results.length === 0) return {};
21992
+ const sorted = results.map((r) => ({
21993
+ name: r.name,
21994
+ kind: r.kind,
21995
+ line: r.line,
21996
+ col: r.col,
21997
+ signature: r.signature
21998
+ })).sort((a, b) => a.line - b.line || a.col - b.col);
21999
+ const result = { symbols: sorted };
22000
+ if (total > results.length) {
22001
+ result.note = `Symbol listing truncated to ${results.length} of ${total} entries.`;
22002
+ }
22003
+ return result;
22004
+ } catch {
22005
+ return {};
22006
+ }
22007
+ }
22008
+ function mergeSymbolNote(note, symNote) {
22009
+ if (!symNote) return note;
22010
+ if (!note) return symNote;
22011
+ return `${note} ${symNote}`;
22012
+ }
21627
22013
  var READ_RANGES_META_KEY = "tools.read.ranges.v1";
21628
22014
  function getReadRanges(ctx) {
21629
22015
  const existing = ctx.meta[READ_RANGES_META_KEY];