@wrongstack/tools 0.309.1 → 0.310.1

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 (58) hide show
  1. package/dist/_regex.d.ts +6 -34
  2. package/dist/bash.js +3 -3
  3. package/dist/builtin.d.ts +17 -14
  4. package/dist/builtin.js +3028 -1675
  5. package/dist/codebase-index/binary-frame.d.ts +57 -8
  6. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +6 -0
  7. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +6 -0
  8. package/dist/codebase-index/codebase-search-tool.d.ts +15 -5
  9. package/dist/codebase-index/index-service.d.ts +3 -19
  10. package/dist/codebase-index/index.js +2735 -1415
  11. package/dist/codebase-index/indexer.d.ts +3 -0
  12. package/dist/codebase-index/parser-batch.d.ts +53 -0
  13. package/dist/codebase-index/parser-dispatch.d.ts +32 -0
  14. package/dist/codebase-index/parser-output.d.ts +14 -0
  15. package/dist/codebase-index/parser-worker-pool.d.ts +57 -4
  16. package/dist/codebase-index/parser-worker-script.d.ts +5 -2
  17. package/dist/codebase-index/parser-worker-script.js +4042 -0
  18. package/dist/codebase-index/project-server-cache.d.ts +16 -0
  19. package/dist/codebase-index/project-server-client.d.ts +2 -2
  20. package/dist/codebase-index/project-server-query-cache.d.ts +88 -0
  21. package/dist/codebase-index/project-server.js +2846 -1308
  22. package/dist/codebase-index/py-parser.d.ts +5 -0
  23. package/dist/codebase-index/schema.d.ts +14 -1
  24. package/dist/codebase-index/sqlite-runtime.d.ts +2 -2
  25. package/dist/codebase-index/tree-sitter/queries.d.ts +30 -3
  26. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  27. package/dist/codebase-index/vector-search.d.ts +12 -0
  28. package/dist/codebase-index/wal-maintenance.d.ts +58 -0
  29. package/dist/codebase-index/worker-protocol/contracts.d.ts +44 -0
  30. package/dist/codebase-index/worker-protocol.d.ts +17 -1
  31. package/dist/codebase-index/worker.js +2300 -1020
  32. package/dist/codebase-index/writer-admin.d.ts +11 -0
  33. package/dist/codebase-index/writer-helpers.d.ts +31 -1
  34. package/dist/codebase-index/writer-mutations.d.ts +0 -6
  35. package/dist/codebase-index/writer-schema.d.ts +2 -2
  36. package/dist/codebase-index/writer.d.ts +15 -0
  37. package/dist/edit.js +2511 -1203
  38. package/dist/exec.js +5 -3
  39. package/dist/grep.js +5 -124
  40. package/dist/index.js +3028 -1738
  41. package/dist/json.js +5 -124
  42. package/dist/kanban.js +130 -0
  43. package/dist/logs.js +5 -121
  44. package/dist/pack.js +3028 -1675
  45. package/dist/patch.js +2524 -1216
  46. package/dist/plan.js +106 -0
  47. package/dist/read.js +2506 -1198
  48. package/dist/replace.js +2487 -1295
  49. package/dist/search.js +6 -2
  50. package/dist/session-kanban.js +24 -16
  51. package/dist/task.js +106 -0
  52. package/dist/todo.js +106 -0
  53. package/dist/tool-tier.d.ts +11 -0
  54. package/dist/tool-tier.js +3040 -1679
  55. package/dist/tree.js +14 -3
  56. package/dist/win32.js +3 -3
  57. package/dist/write.js +2513 -1205
  58. package/package.json +5 -4
package/dist/replace.js CHANGED
@@ -173,238 +173,133 @@ var init_languages = __esm({
173
173
  }
174
174
  });
175
175
 
176
- // src/codebase-index/ts-parser.ts
177
- var ts_parser_exports = {};
178
- __export(ts_parser_exports, {
179
- detectLang: () => detectLang,
180
- parseSymbols: () => parseSymbols
181
- });
182
- function loadTypescript() {
183
- tsLoad ??= import("@typescript/typescript6").then((m) => {
184
- ts = m.default ?? m;
185
- return ts;
186
- });
187
- return tsLoad;
188
- }
189
- function kindMap() {
190
- kindMapCache ??= {
191
- [ts.SyntaxKind.ClassDeclaration]: "class",
192
- [ts.SyntaxKind.InterfaceDeclaration]: "interface",
193
- [ts.SyntaxKind.EnumDeclaration]: "enum",
194
- [ts.SyntaxKind.TypeAliasDeclaration]: "type",
195
- [ts.SyntaxKind.FunctionDeclaration]: "function",
196
- [ts.SyntaxKind.MethodDeclaration]: "method",
197
- [ts.SyntaxKind.GetAccessor]: "property",
198
- [ts.SyntaxKind.SetAccessor]: "property",
199
- [ts.SyntaxKind.PropertyDeclaration]: "property",
200
- [ts.SyntaxKind.Parameter]: "parameter",
201
- [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
202
- };
203
- return kindMapCache;
204
- }
205
- function kindOf(node) {
206
- if (ts.isVariableDeclaration(node)) {
207
- const parent = node.parent;
208
- if (ts.isVariableDeclarationList(parent)) {
209
- const flags = parent.flags;
210
- if (flags & ts.NodeFlags.Let) return "let";
211
- if (flags & ts.NodeFlags.Const) return "const";
212
- return "var";
213
- }
214
- }
215
- if (ts.isModuleDeclaration(node)) return "namespace";
216
- return kindMap()[node.kind] ?? null;
217
- }
218
- function getSignature(printer, node, sourceFile) {
219
- const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
220
- return raw.replace(/\s+/g, " ").slice(0, 500);
221
- }
222
- function getJsDoc(node, sourceFile) {
223
- const fullText = sourceFile.getFullText();
224
- const nodePos = node.getFullStart();
225
- const comments = ts.getLeadingCommentRanges(fullText, nodePos);
226
- if (!comments) return "";
227
- for (const range of comments) {
228
- const commentText = fullText.slice(range.pos, range.end);
229
- const trimmed = commentText.trim();
230
- if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
231
- const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
232
- return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
233
- }
176
+ // src/codebase-index/import-extractor.ts
177
+ function lastSegment(specifier) {
178
+ const pathLike = /[/\\]|::/.test(specifier);
179
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
180
+ let last = segments[segments.length - 1] ?? specifier;
181
+ if (last === "*" || last === "_") {
182
+ last = segments[segments.length - 2] ?? specifier;
234
183
  }
235
- return "";
184
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
185
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
186
+ return dotted[dotted.length - 1] ?? last;
236
187
  }
237
- function pushScopeName(node, parts) {
238
- if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
239
- parts.push(node.name?.text ?? "Anon");
240
- } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
241
- if (node.name && ts.isIdentifier(node.name)) {
242
- parts.push(node.name.text);
243
- }
188
+ function newlineOffsets(content) {
189
+ const offsets = [];
190
+ for (let i = 0; i < content.length; i++) {
191
+ if (content.charCodeAt(i) === 10) offsets.push(i);
244
192
  }
193
+ return offsets;
245
194
  }
246
- async function parseSymbols(opts) {
247
- const { file, content, lang } = opts;
248
- await loadTypescript();
249
- let sourceFile;
250
- try {
251
- sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
252
- } catch {
253
- return { file, lang, symbols: [], mtimeMs: Date.now() };
254
- }
255
- const symbols = [];
256
- const refs = [];
257
- const printer = ts.createPrinter({});
258
- function visit(node, funcDepth, scopeParts) {
259
- const kind = kindOf(node);
260
- if (kind) {
261
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
262
- } else {
263
- const nameNode = node.name;
264
- if (!nameNode || !ts.isIdentifier(nameNode)) {
265
- return;
266
- }
267
- const name = nameNode.text;
268
- const pos2 = nameNode.getStart(sourceFile);
269
- const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
270
- const scope = scopeParts.join(".");
271
- const signature = getSignature(printer, node, sourceFile);
272
- const docComment = getJsDoc(node, sourceFile);
273
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
274
- symbols.push({
275
- id: 0,
276
- lang,
277
- kind,
278
- name,
279
- file,
280
- line: line2 + 1,
281
- col: character,
282
- signature,
283
- docComment,
284
- scope,
285
- text
286
- });
287
- }
288
- }
289
- const pos = node.getStart(sourceFile);
290
- const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
291
- const lineNum = line + 1;
292
- if (ts.isCallExpression(node)) {
293
- const expr = node.expression;
294
- if (ts.isIdentifier(expr)) {
295
- refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
296
- }
297
- } else if (ts.isPropertyAccessExpression(node)) {
298
- if (ts.isIdentifier(node.expression)) {
299
- refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
300
- }
301
- } else if (ts.isTypeReferenceNode(node)) {
302
- const name = getTypeName(node.typeName);
303
- if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
304
- } else if (ts.isHeritageClause(node)) {
305
- for (const t of node.types) {
306
- const name = getTypeName(t.expression);
307
- if (name)
308
- refs.push({
309
- fromId: 0,
310
- toName: name,
311
- callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
312
- line: lineNum
313
- });
314
- }
315
- } else if (ts.isImportDeclaration(node)) {
316
- emitImportSpecifierRefs(node, refs, lineNum);
317
- } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
318
- emitExportSpecifierRefs(node, refs, lineNum);
319
- }
320
- const scopeIdx = scopeParts.length;
321
- pushScopeName(node, scopeParts);
322
- const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
323
- ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
324
- scopeParts.length = scopeIdx;
195
+ function lineAt(offsets, index) {
196
+ let low = 0;
197
+ let high = offsets.length;
198
+ while (low < high) {
199
+ const mid = low + high >>> 1;
200
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
201
+ else high = mid;
325
202
  }
326
- visit(sourceFile, 0, []);
327
- return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
203
+ return low + 1;
328
204
  }
329
- function getTypeName(name) {
330
- if (ts.isIdentifier(name)) return name.text;
331
- if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
332
- return "";
205
+ function hasImportPatterns(lang) {
206
+ return LANG_IMPORTS[lang] !== void 0;
333
207
  }
334
- function deduplicateRefs(refs) {
208
+ function extractImports(opts) {
209
+ const patterns = LANG_IMPORTS[opts.lang];
210
+ if (!patterns || !opts.content) return [];
211
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
212
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
213
+ const refs = [];
335
214
  const seen = /* @__PURE__ */ new Set();
336
- return refs.filter((r) => {
337
- const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
338
- if (seen.has(key)) return false;
339
- seen.add(key);
340
- return true;
341
- });
342
- }
343
- function getImportSpecifierName(spec) {
344
- return spec.propertyName?.text ?? spec.name.text;
345
- }
346
- function emitImportSpecifierRefs(node, refs, lineNum) {
347
- const module = moduleSpecifierOf(node.moduleSpecifier);
348
- const clause = node.importClause;
349
- if (!clause) {
350
- if (module) {
351
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
352
- }
353
- return;
354
- }
355
- if (clause.name) {
356
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
357
- }
358
- const bindings = clause.namedBindings;
359
- if (!bindings) return;
360
- if (ts.isNamedImports(bindings)) {
361
- for (const element of bindings.elements) {
215
+ const offsets = newlineOffsets(content);
216
+ for (const pattern of patterns) {
217
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
218
+ for (const match of content.matchAll(re)) {
219
+ if (refs.length >= limit) return refs;
220
+ const specifier = match[1]?.trim();
221
+ if (!specifier) continue;
222
+ const module = specifier;
223
+ const toName = pattern.name === "full" ? module : lastSegment(module);
224
+ if (!toName) continue;
225
+ const key = `${module}\0${toName}`;
226
+ if (seen.has(key)) continue;
227
+ seen.add(key);
362
228
  refs.push({
363
229
  fromId: 0,
364
- toName: getImportSpecifierName(element),
230
+ toName,
365
231
  callType: "import",
366
- line: lineNum,
232
+ line: lineAt(offsets, match.index ?? 0),
233
+ lang: opts.lang,
367
234
  module
368
235
  });
369
236
  }
370
- } else if (ts.isNamespaceImport(bindings)) {
371
- refs.push({
372
- fromId: 0,
373
- toName: bindings.name.text,
374
- callType: "import",
375
- line: lineNum,
376
- module
377
- });
378
237
  }
238
+ return refs;
379
239
  }
380
- function moduleSpecifierOf(node) {
381
- return node && ts.isStringLiteral(node) ? node.text : void 0;
382
- }
383
- function emitExportSpecifierRefs(node, refs, lineNum) {
384
- const module = moduleSpecifierOf(node.moduleSpecifier);
385
- const clause = node.exportClause;
386
- if (clause && ts.isNamespaceExport(clause)) {
387
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
388
- return;
389
- }
390
- if (clause && ts.isNamedExports(clause)) {
391
- for (const element of clause.elements) {
392
- const originalName = element.propertyName?.text ?? element.name.text;
393
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
394
- }
395
- return;
396
- }
397
- if (module) {
398
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
399
- }
400
- }
401
- var ts, tsLoad, kindMapCache;
402
- var init_ts_parser = __esm({
403
- "src/codebase-index/ts-parser.ts"() {
240
+ var IMPORT_MAX_FILE_CHARS, IMPORT_MAX_PER_FILE, DOTTED_IMPORT, LANG_IMPORTS;
241
+ var init_import_extractor = __esm({
242
+ "src/codebase-index/import-extractor.ts"() {
404
243
  "use strict";
405
- init_languages();
406
- tsLoad = null;
407
- kindMapCache = null;
244
+ IMPORT_MAX_FILE_CHARS = 512 * 1024;
245
+ IMPORT_MAX_PER_FILE = 400;
246
+ DOTTED_IMPORT = [
247
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
248
+ ];
249
+ LANG_IMPORTS = {
250
+ // Go and Python have real AST extractors; these patterns are the fallback for
251
+ // machines with no Go toolchain or Python interpreter installed, where the
252
+ // parser degrades to regex symbols and would otherwise contribute no edges.
253
+ go: [
254
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
255
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
256
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
257
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
258
+ ],
259
+ py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
260
+ rs: [
261
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
262
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
263
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
264
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
265
+ ],
266
+ java: DOTTED_IMPORT,
267
+ kotlin: DOTTED_IMPORT,
268
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
269
+ csharp: [
270
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
271
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
272
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
273
+ ],
274
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
275
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
276
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
277
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
278
+ php: [
279
+ // `use A\B\C` imports the class C, which is what the index has a symbol
280
+ // for — the namespace symbol only covers the `A\B` prefix.
281
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
282
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
283
+ ],
284
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
285
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
286
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
287
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
288
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
289
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
290
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
291
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
292
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
293
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
294
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
295
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
296
+ html: [
297
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
298
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
299
+ ],
300
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
301
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
302
+ };
408
303
  }
409
304
  });
410
305
 
@@ -490,6 +385,32 @@ function parseParserOutput(stdout, lang) {
490
385
  refs: dedupeRefs(coerceRefs(record.refs, lang))
491
386
  };
492
387
  }
388
+ function parseParserBatchOutput(stdout, lang) {
389
+ const trimmed = stdout.trim();
390
+ if (!trimmed) return [];
391
+ let parsed;
392
+ try {
393
+ parsed = JSON.parse(trimmed);
394
+ } catch {
395
+ return [];
396
+ }
397
+ if (!parsed || typeof parsed !== "object") return [];
398
+ const results = parsed.results;
399
+ if (!Array.isArray(results)) return [];
400
+ return results.flatMap((entry) => {
401
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
402
+ const candidate = entry;
403
+ if (typeof candidate.file !== "string" || !candidate.file) return [];
404
+ return [
405
+ {
406
+ file: candidate.file,
407
+ error: typeof candidate.error === "string" && candidate.error ? candidate.error : void 0,
408
+ symbols: coerceSymbols(candidate.symbols),
409
+ refs: dedupeRefs(coerceRefs(candidate.refs, lang))
410
+ }
411
+ ];
412
+ });
413
+ }
493
414
  function dedupeRefs(refs) {
494
415
  const seen = /* @__PURE__ */ new Set();
495
416
  return refs.filter((ref) => {
@@ -530,187 +451,172 @@ var init_spawn_gate = __esm({
530
451
  }
531
452
  });
532
453
 
533
- // src/codebase-index/go-parser.ts
534
- var go_parser_exports = {};
535
- __export(go_parser_exports, {
536
- detectLang: () => detectLang,
537
- parseSymbols: () => parseSymbols2
538
- });
454
+ // src/codebase-index/parser-batch.ts
539
455
  import { spawn } from "node:child_process";
456
+ import * as fsSync from "node:fs";
457
+ import * as fs4 from "node:fs/promises";
540
458
  import * as os from "node:os";
541
459
  import * as path7 from "node:path";
542
- import * as fs4 from "node:fs/promises";
543
- async function parseSymbols2(opts) {
544
- const { file, content, lang } = opts;
545
- try {
546
- const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
547
- if (parsed.symbols.length > 0) {
548
- return parsed;
460
+ function chunkBatchFiles(files) {
461
+ const chunks = [];
462
+ let current = [];
463
+ let bytes = 0;
464
+ for (const file of files) {
465
+ const size = Buffer.byteLength(file.content, "utf8");
466
+ if (current.length > 0 && (current.length >= MAX_BATCH_FILES || bytes + size > MAX_BATCH_BYTES)) {
467
+ chunks.push(current);
468
+ current = [];
469
+ bytes = 0;
470
+ }
471
+ current.push(file);
472
+ bytes += size;
473
+ }
474
+ if (current.length > 0) chunks.push(current);
475
+ return chunks;
476
+ }
477
+ function batchTimeoutMs(fileCount) {
478
+ return Math.min(12e4, 15e3 + fileCount * 1500);
479
+ }
480
+ async function ensureScriptPath(cached, prefix, fileName, script) {
481
+ if (cached) return { path: cached, wrote: false };
482
+ const dir = await fs4.mkdtemp(path7.join(os.tmpdir(), prefix));
483
+ const scriptPath = path7.join(dir, fileName);
484
+ await fs4.writeFile(scriptPath, script, { encoding: "utf8", flag: "wx" });
485
+ process.once("exit", () => {
486
+ try {
487
+ fsSync.rmSync(dir, { recursive: true, force: true });
488
+ } catch {
549
489
  }
550
- const fallback = fallbackParse(file, content, lang);
551
- return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
552
- } catch {
553
- return fallbackParse(file, content, lang);
554
- }
490
+ });
491
+ return { path: scriptPath, wrote: true };
555
492
  }
556
- function fallbackParse(filePath, content, lang) {
557
- if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
558
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
559
- }
560
- const symbols = [];
561
- const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
562
- const lines = content.split(/\r?\n/);
563
- for (const [idx, line] of lines.entries()) {
564
- const trimmed = line.trimStart();
565
- const col = line.length - trimmed.length + 1;
566
- const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
567
- if (fn?.[1]) {
568
- addFallbackSymbol(symbols, {
569
- filePath,
570
- lang,
571
- kind: trimmed.startsWith("func (") ? "method" : "function",
572
- name: fn[1],
573
- line: idx + 1,
574
- col,
575
- signature: trimmed,
576
- scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
577
- });
578
- continue;
579
- }
580
- const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
581
- if (typeDecl?.[1]) {
582
- addFallbackSymbol(symbols, {
583
- filePath,
584
- lang,
585
- kind: "type",
586
- name: typeDecl[1],
587
- line: idx + 1,
588
- col,
589
- signature: trimmed,
590
- scope: packageName
591
- });
592
- continue;
593
- }
594
- const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
595
- if (valueDecl?.[1] && valueDecl[2]) {
596
- addFallbackSymbol(symbols, {
597
- filePath,
598
- lang,
599
- kind: valueDecl[1],
600
- name: valueDecl[2],
601
- line: idx + 1,
602
- col,
603
- signature: trimmed,
604
- scope: packageName
605
- });
493
+ function runToolchainChild(binary, args, stdinPayload, timeoutMs) {
494
+ return new Promise((resolve4) => {
495
+ let settled = false;
496
+ let stdout = "";
497
+ let proc;
498
+ try {
499
+ proc = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
500
+ } catch {
501
+ resolve4(null);
502
+ return;
606
503
  }
607
- }
608
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
609
- }
610
- function addFallbackSymbol(symbols, opts) {
611
- symbols.push({
612
- id: 0,
613
- lang: opts.lang,
614
- kind: opts.kind,
615
- name: opts.name,
616
- file: opts.filePath,
617
- line: opts.line,
618
- col: opts.col,
619
- signature: opts.signature,
620
- docComment: "",
621
- scope: opts.scope,
622
- text: `${opts.name} ${opts.signature}`.trim()
504
+ const finish = (value) => {
505
+ if (settled) return;
506
+ settled = true;
507
+ clearTimeout(timer);
508
+ resolve4(value);
509
+ };
510
+ const timer = setTimeout(() => {
511
+ proc.kill("SIGKILL");
512
+ finish(null);
513
+ }, timeoutMs);
514
+ timer.unref?.();
515
+ proc.on("error", () => finish(null));
516
+ proc.stdout?.on("data", (chunk) => {
517
+ stdout += chunk.toString();
518
+ });
519
+ proc.stderr?.resume();
520
+ proc.stdin?.on("error", () => {
521
+ });
522
+ proc.stdin?.write(stdinPayload);
523
+ proc.stdin?.end();
524
+ proc.on("close", (code) => finish({ code, stdout }));
623
525
  });
624
526
  }
625
- function hasUnbalancedDelimiters(content) {
626
- const pairs = { "(": ")", "[": "]", "{": "}" };
627
- const closers = new Set(Object.values(pairs));
628
- const stack = [];
629
- for (const ch of content) {
630
- if (pairs[ch]) {
631
- stack.push(pairs[ch]);
632
- } else if (closers.has(ch) && stack.pop() !== ch) {
633
- return true;
634
- }
527
+ async function runGoBatch(files, goBinary) {
528
+ const out = /* @__PURE__ */ new Map();
529
+ if (files.length === 0) return out;
530
+ const { path: scriptPath } = await ensureScriptPath(
531
+ _goBatchScriptPath,
532
+ "ws-go-parse",
533
+ "batch.go",
534
+ GO_BATCH_SCRIPT
535
+ );
536
+ _goBatchScriptPath = scriptPath;
537
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
538
+ const result = await withSpawnGate(
539
+ () => runToolchainChild(
540
+ goBinary ?? resolveWin32Command("go"),
541
+ ["run", scriptPath],
542
+ payload,
543
+ batchTimeoutMs(files.length)
544
+ )
545
+ );
546
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
547
+ for (const entry of parseParserBatchOutput(result.stdout, "go")) {
548
+ if (entry.error !== void 0) continue;
549
+ out.set(entry.file, {
550
+ file: entry.file,
551
+ lang: "go",
552
+ symbols: entry.symbols.map((s) => ({
553
+ id: 0,
554
+ lang: "go",
555
+ kind: s.kind,
556
+ name: s.name,
557
+ file: entry.file,
558
+ line: s.line,
559
+ col: s.col,
560
+ signature: s.signature ?? "",
561
+ docComment: "",
562
+ scope: s.scope ?? "",
563
+ text: `${s.name} ${s.signature ?? ""}`.trim()
564
+ })),
565
+ refs: entry.refs,
566
+ mtimeMs: Date.now()
567
+ });
635
568
  }
636
- return stack.length > 0;
569
+ return out;
637
570
  }
638
- async function syncGoParse(filePath, content, lang) {
639
- try {
640
- let scriptPath = _cachedGoScriptPath;
641
- if (!scriptPath) {
642
- const tmpDir = await fs4.mkdtemp(path7.join(os.tmpdir(), "ws-go-parse-"));
643
- scriptPath = path7.join(tmpDir, "parse.go");
644
- await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
645
- _cachedGoScriptPath = scriptPath;
646
- }
647
- const goBinary = resolveWin32Command("go");
648
- const goResult = await new Promise(
649
- (resolve4, reject) => {
650
- let settled = false;
651
- const proc = spawn(goBinary, ["run", scriptPath], {
652
- stdio: ["pipe", "pipe", "pipe"],
653
- windowsHide: true
654
- });
655
- proc.on("error", (err) => {
656
- if (settled) return;
657
- settled = true;
658
- reject(err);
659
- });
660
- let stdout2 = "";
661
- proc.stdout?.on("data", (chunk) => {
662
- stdout2 += chunk.toString();
663
- });
664
- proc.stderr?.resume();
665
- proc.stdin?.write(content);
666
- proc.stdin?.end();
667
- const timer = setTimeout(() => {
668
- if (settled) return;
669
- settled = true;
670
- proc.kill("SIGKILL");
671
- reject(new Error("timeout"));
672
- }, 15e3);
673
- timer.unref?.();
674
- proc.on("close", (code2) => {
675
- if (settled) return;
676
- settled = true;
677
- clearTimeout(timer);
678
- resolve4({ code: code2, stdout: stdout2 });
679
- });
680
- }
681
- );
682
- const { code, stdout } = goResult;
683
- if (code !== 0 || !stdout.trim()) {
684
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
685
- }
686
- const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
687
- const symbols = rawSymbols.map((s) => ({
688
- id: 0,
689
- lang,
690
- kind: s.kind,
691
- name: s.name,
692
- file: filePath,
693
- line: s.line,
694
- col: s.col,
695
- signature: s.signature ?? "",
696
- docComment: "",
697
- scope: s.scope ?? "",
698
- text: `${s.name} ${s.signature ?? ""}`.trim()
699
- }));
700
- return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
701
- } catch {
702
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
571
+ async function runPyBatch(files, pythonBinary) {
572
+ const out = /* @__PURE__ */ new Map();
573
+ if (files.length === 0) return out;
574
+ const { path: scriptPath } = await ensureScriptPath(
575
+ _pyBatchScriptPath,
576
+ "ws-py-parse",
577
+ "batch.py",
578
+ PY_BATCH_SCRIPT
579
+ );
580
+ _pyBatchScriptPath = scriptPath;
581
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
582
+ const result = await withSpawnGate(
583
+ () => runToolchainChild(pythonBinary, [scriptPath], payload, batchTimeoutMs(files.length))
584
+ );
585
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
586
+ for (const entry of parseParserBatchOutput(result.stdout, "py")) {
587
+ if (entry.error !== void 0) continue;
588
+ out.set(entry.file, {
589
+ file: entry.file,
590
+ lang: "py",
591
+ symbols: entry.symbols.map((s) => ({
592
+ id: 0,
593
+ lang: "py",
594
+ kind: s.kind,
595
+ name: s.name,
596
+ file: entry.file,
597
+ line: s.line,
598
+ col: s.col,
599
+ signature: s.signature ?? "",
600
+ docComment: "",
601
+ scope: s.scope ?? "",
602
+ text: `${s.name} ${s.signature ?? ""}`.trim()
603
+ })),
604
+ refs: entry.refs,
605
+ mtimeMs: Date.now()
606
+ });
703
607
  }
608
+ return out;
704
609
  }
705
- var GO_PARSE_SCRIPT, _cachedGoScriptPath;
706
- var init_go_parser = __esm({
707
- "src/codebase-index/go-parser.ts"() {
610
+ var MAX_BATCH_FILES, MAX_BATCH_BYTES, GO_BATCH_SCRIPT, PY_BATCH_SCRIPT, _goBatchScriptPath, _pyBatchScriptPath;
611
+ var init_parser_batch = __esm({
612
+ "src/codebase-index/parser-batch.ts"() {
708
613
  "use strict";
709
614
  init_win32_resolve();
710
615
  init_parser_output();
711
616
  init_spawn_gate();
712
- init_languages();
713
- GO_PARSE_SCRIPT = `
617
+ MAX_BATCH_FILES = 100;
618
+ MAX_BATCH_BYTES = 8 * 1024 * 1024;
619
+ GO_BATCH_SCRIPT = `
714
620
  package main
715
621
 
716
622
  import (
@@ -734,8 +640,6 @@ type Sym struct {
734
640
  Scope string \`json:"scope"\`
735
641
  }
736
642
 
737
- // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
738
- // yields both. Module is the import path for CallType "import", else empty.
739
643
  type Ref struct {
740
644
  ToName string \`json:"toName"\`
741
645
  CallType string \`json:"callType"\`
@@ -743,57 +647,53 @@ type Ref struct {
743
647
  Module string \`json:"module"\`
744
648
  }
745
649
 
746
- type Result struct {
747
- Symbols []Sym \`json:"symbols"\`
748
- Refs []Ref \`json:"refs"\`
650
+ type FileResult struct {
651
+ File string \`json:"file"\`
652
+ Error string \`json:"error,omitempty"\`
653
+ Symbols []Sym \`json:"symbols"\`
654
+ Refs []Ref \`json:"refs"\`
749
655
  }
750
656
 
751
- func emptyResult() string {
752
- return "{\\"symbols\\":[],\\"refs\\":[]}"
657
+ type BatchResult struct {
658
+ Version int \`json:"version"\`
659
+ Results []FileResult \`json:"results"\`
753
660
  }
754
661
 
755
- func main() {
756
- src, err := io.ReadAll(os.Stdin)
757
- if err != nil {
758
- fmt.Print(emptyResult())
759
- return
760
- }
662
+ type inputFile struct {
663
+ File string \`json:"file"\`
664
+ Content string \`json:"content"\`
665
+ }
666
+
667
+ func parseOne(name string, src []byte) FileResult {
668
+ res := FileResult{File: name, Symbols: []Sym{}, Refs: []Ref{}}
761
669
  fset := token.NewFileSet()
762
670
  node, err := parser.ParseFile(fset, "src.go", src, 0)
763
671
  if err != nil {
764
- fmt.Print(emptyResult())
765
- return
672
+ res.Error = err.Error()
673
+ return res
766
674
  }
767
-
768
- var syms []Sym
769
-
770
- // Package-level scope
771
675
  pkgScope := node.Name.Name
772
-
773
- // Collect all top-level declarations
774
676
  for _, decl := range node.Decls {
775
677
  switch d := decl.(type) {
776
678
  case *ast.FuncDecl:
777
- name := d.Name.Name
679
+ symName := d.Name.Name
778
680
  kind := "function"
779
681
  scope := pkgScope
780
682
  if d.Recv != nil && len(d.Recv.List) > 0 {
781
- scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
683
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + symName
782
684
  kind = "method"
783
685
  } else {
784
- scope = pkgScope + "." + name
686
+ scope = pkgScope + "." + symName
785
687
  }
786
688
  pos := fset.Position(d.Pos())
787
- sig := formatFuncSig(d)
788
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
789
-
689
+ res.Symbols = append(res.Symbols, Sym{Name: symName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: formatFuncSig(d), Scope: scope})
790
690
  case *ast.GenDecl:
791
691
  for _, spec := range d.Specs {
792
692
  switch s := spec.(type) {
793
693
  case *ast.TypeSpec:
794
- name := s.Name.Name
694
+ typeName := s.Name.Name
795
695
  pos := fset.Position(s.Pos())
796
- sig := "type " + name
696
+ sig := "type " + typeName
797
697
  if s.TypeParams != nil {
798
698
  sig += formatTypeParams(s.TypeParams)
799
699
  }
@@ -804,64 +704,72 @@ func main() {
804
704
  } else {
805
705
  sig += " = " + formatType(s.Type)
806
706
  }
807
- syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
808
-
707
+ res.Symbols = append(res.Symbols, Sym{Name: typeName, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
809
708
  case *ast.ValueSpec:
810
709
  for _, n := range s.Names {
811
- name := n.Name
710
+ valueName := n.Name
812
711
  pos := fset.Position(n.Pos())
813
712
  kind := "var"
814
713
  if d.Tok == token.CONST {
815
714
  kind = "const"
816
715
  }
817
- sig := kind + " " + name
716
+ sig := kind + " " + valueName
818
717
  if s.Type != nil {
819
718
  sig += " " + formatType(s.Type)
820
719
  }
821
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
720
+ res.Symbols = append(res.Symbols, Sym{Name: valueName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
822
721
  }
823
722
  }
824
723
  }
825
724
  }
826
725
  }
827
-
828
- refs := []Ref{}
829
726
  ast.Inspect(node, func(n ast.Node) bool {
830
727
  switch expr := n.(type) {
831
728
  case *ast.CallExpr:
832
729
  line := fset.Position(expr.Pos()).Line
833
730
  switch fun := expr.Fun.(type) {
834
731
  case *ast.Ident:
835
- refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
732
+ res.Refs = append(res.Refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
836
733
  case *ast.SelectorExpr:
837
- // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
838
- // declared symbol name, so it resolves the same way the TypeScript
839
- // and Python extractors' call refs do.
840
- refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
734
+ res.Refs = append(res.Refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
841
735
  }
842
736
  case *ast.ImportSpec:
843
737
  if expr.Path != nil {
844
738
  if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
845
739
  line := fset.Position(expr.Pos()).Line
846
- // A Go import names a package, not a symbol; the package's
847
- // last path segment is the name it is referenced by.
848
740
  name := importPath
849
741
  if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
850
742
  name = importPath[idx+1:]
851
743
  }
852
- refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
744
+ res.Refs = append(res.Refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
853
745
  }
854
746
  }
855
747
  }
856
748
  return true
857
749
  })
750
+ return res
751
+ }
858
752
 
859
- if syms == nil {
860
- syms = []Sym{}
753
+ func main() {
754
+ raw, err := io.ReadAll(os.Stdin)
755
+ if err != nil {
756
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
757
+ fmt.Print(string(out))
758
+ return
861
759
  }
862
- data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
760
+ var inputs []inputFile
761
+ if err := json.Unmarshal(raw, &inputs); err != nil {
762
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
763
+ fmt.Print(string(out))
764
+ return
765
+ }
766
+ results := make([]FileResult, 0, len(inputs))
767
+ for _, in := range inputs {
768
+ results = append(results, parseOne(in.File, []byte(in.Content)))
769
+ }
770
+ data, err := json.Marshal(BatchResult{Version: 1, Results: results})
863
771
  if err != nil {
864
- fmt.Print(emptyResult())
772
+ fmt.Print("{\\"version\\":1,\\"results\\":[]}")
865
773
  return
866
774
  }
867
775
  fmt.Print(string(data))
@@ -981,10 +889,8 @@ func formatType(t ast.Expr) string {
981
889
  case *ast.BasicLit:
982
890
  return v.Value
983
891
  case *ast.IndexExpr:
984
- // Generic instantiation with one type arg, e.g. Logger[int].
985
892
  return formatType(v.X) + "[" + formatType(v.Index) + "]"
986
893
  case *ast.IndexListExpr:
987
- // Generic instantiation with multiple type args, e.g. Map[K, V].
988
894
  args := make([]string, len(v.Indices))
989
895
  for i, idx := range v.Indices {
990
896
  args[i] = formatType(idx)
@@ -995,7 +901,187 @@ func formatType(t ast.Expr) string {
995
901
  }
996
902
  }
997
903
  `;
998
- _cachedGoScriptPath = null;
904
+ PY_BATCH_SCRIPT = `import ast, json, sys
905
+
906
+ def get_name(node):
907
+ if isinstance(node, ast.Name):
908
+ return node.id
909
+ elif isinstance(node, ast.Attribute):
910
+ return get_name(node.value) + "." + node.attr
911
+ elif isinstance(node, ast.Subscript):
912
+ return get_name(node.value)
913
+ elif isinstance(node, ast.Call):
914
+ return get_name(node.func)
915
+ elif isinstance(node, ast.Constant):
916
+ return str(node.value)
917
+ return ""
918
+
919
+ def leaf_name(node):
920
+ if isinstance(node, ast.Attribute):
921
+ return node.attr
922
+ if isinstance(node, ast.Name):
923
+ return node.id
924
+ return get_name(node).split(".")[-1]
925
+
926
+ def is_private(name):
927
+ return name.startswith("__") and not name.endswith("__")
928
+
929
+ def parse_one(name, source, module_name):
930
+ result = {"file": name, "symbols": [], "refs": []}
931
+ try:
932
+ tree = ast.parse(source, filename=name)
933
+ except Exception as e:
934
+ result["error"] = str(e)
935
+ return result
936
+ syms = []
937
+ refs = []
938
+ scope_stack = [module_name]
939
+
940
+ def sym(d):
941
+ return {
942
+ "name": d["name"], "kind": d["kind"], "line": d["line"], "col": d["col"],
943
+ "signature": d["signature"], "scope": d["scope"],
944
+ }
945
+
946
+ class Visitor(ast.NodeVisitor):
947
+ def visit_ClassDef(self, node):
948
+ bases = [get_name(b) for b in node.bases]
949
+ sig = "class " + node.name
950
+ if bases:
951
+ sig += "(" + ", ".join(bases) + ")"
952
+ sig += ": ..."
953
+ syms.append(sym({
954
+ "name": node.name, "kind": "class", "line": node.lineno,
955
+ "col": node.col_offset, "signature": sig,
956
+ "scope": ".".join(scope_stack) + "." + node.name,
957
+ }))
958
+ scope_stack.append(node.name)
959
+ self.generic_visit(node)
960
+ scope_stack.pop()
961
+
962
+ def visit_FunctionDef(self, node):
963
+ args = ", ".join(a.arg for a in node.args.args)
964
+ returns = get_name(node.returns) if node.returns is not None else ""
965
+ is_async = isinstance(node, ast.AsyncFunctionDef)
966
+ kind = "function"
967
+ prefix = "def "
968
+ for dec in node.decorator_list:
969
+ d = get_name(dec)
970
+ if d.endswith(".staticmethod"):
971
+ kind = "staticmethod"
972
+ elif d.endswith(".classmethod"):
973
+ kind = "classmethod"
974
+ elif d == "property":
975
+ kind = "property"
976
+ if is_async:
977
+ kind = "async_" + kind
978
+ sig = f"{prefix}{node.name}({args})"
979
+ if returns:
980
+ sig += f" -> {returns}"
981
+ syms.append(sym({
982
+ "name": node.name, "kind": kind, "line": node.lineno,
983
+ "col": node.col_offset, "signature": sig,
984
+ "scope": ".".join(scope_stack) + "." + node.name,
985
+ }))
986
+
987
+ def visit_AsyncFunctionDef(self, node):
988
+ self.visit_FunctionDef(node)
989
+
990
+ def visit_Assign(self, node):
991
+ for target in node.targets:
992
+ if isinstance(target, ast.Name):
993
+ tname = target.id
994
+ if is_private(tname):
995
+ continue
996
+ kind = "const" if tname.isupper() else "var"
997
+ col = target.col_offset if hasattr(target, "col_offset") else 0
998
+ syms.append(sym({
999
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
1000
+ "signature": f"{tname} = ...", "scope": ".".join(scope_stack),
1001
+ }))
1002
+
1003
+ def visit_AnnAssign(self, node):
1004
+ if isinstance(node.target, ast.Name):
1005
+ tname = node.target.id
1006
+ if is_private(tname):
1007
+ return
1008
+ kind = "const" if tname.isupper() else "var"
1009
+ col = node.target.col_offset if hasattr(node.target, "col_offset") else 0
1010
+ sig = f"{tname}: {get_name(node.annotation)}"
1011
+ if node.value:
1012
+ sig += " = ..."
1013
+ syms.append(sym({
1014
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
1015
+ "signature": sig, "scope": ".".join(scope_stack),
1016
+ }))
1017
+
1018
+ def visit_Import(self, node):
1019
+ # Parity with the single-file parser: imports are symbols too.
1020
+ for alias in node.names:
1021
+ name = alias.asname or alias.name
1022
+ syms.append(sym({
1023
+ "name": name, "kind": "import", "line": node.lineno,
1024
+ "col": node.col_offset, "signature": f"import {alias.name}",
1025
+ "scope": ".".join(scope_stack),
1026
+ }))
1027
+
1028
+ def visit_ImportFrom(self, node):
1029
+ module = node.module or ""
1030
+ for alias in node.names:
1031
+ name = alias.asname or alias.name
1032
+ syms.append(sym({
1033
+ "name": name, "kind": "import", "line": node.lineno,
1034
+ "col": node.col_offset, "signature": f"from {module} import {alias.name}",
1035
+ "scope": ".".join(scope_stack),
1036
+ }))
1037
+
1038
+ Visitor().visit(tree)
1039
+
1040
+ for node in ast.walk(tree):
1041
+ if isinstance(node, ast.Call):
1042
+ cname = leaf_name(node.func)
1043
+ if cname:
1044
+ refs.append({"toName": cname, "callType": "call", "line": node.lineno})
1045
+ elif isinstance(node, ast.Import):
1046
+ for alias in node.names:
1047
+ refs.append({
1048
+ "toName": alias.name.split(".")[-1], "callType": "import",
1049
+ "line": node.lineno, "module": alias.name,
1050
+ })
1051
+ elif isinstance(node, ast.ImportFrom):
1052
+ module = ("." * (node.level or 0)) + (node.module or "")
1053
+ for alias in node.names:
1054
+ refs.append({
1055
+ "toName": alias.name, "callType": "import",
1056
+ "line": node.lineno, "module": module,
1057
+ })
1058
+ elif isinstance(node, ast.ClassDef):
1059
+ for base in node.bases:
1060
+ bname = leaf_name(base)
1061
+ if bname:
1062
+ refs.append({"toName": bname, "callType": "inherit", "line": node.lineno})
1063
+
1064
+ result["symbols"] = syms
1065
+ result["refs"] = refs
1066
+ return result
1067
+
1068
+ def main():
1069
+ try:
1070
+ inputs = json.loads(sys.stdin.read())
1071
+ except Exception:
1072
+ print(json.dumps({"version": 1, "results": []}))
1073
+ return
1074
+ results = []
1075
+ for entry in inputs:
1076
+ name = entry.get("file", "")
1077
+ module_name = name.rsplit("/", 1)[-1].rsplit("\\\\", 1)[-1][:-3]
1078
+ results.append(parse_one(name, entry.get("content", ""), module_name))
1079
+ print(json.dumps({"version": 1, "results": results}))
1080
+
1081
+ main()
1082
+ `;
1083
+ _goBatchScriptPath = null;
1084
+ _pyBatchScriptPath = null;
999
1085
  }
1000
1086
  });
1001
1087
 
@@ -1005,7 +1091,7 @@ __export(generic_parser_exports, {
1005
1091
  GENERIC_MAX_FILE_CHARS: () => GENERIC_MAX_FILE_CHARS,
1006
1092
  GENERIC_MAX_SYMBOLS_DEFAULT: () => GENERIC_MAX_SYMBOLS_DEFAULT,
1007
1093
  parseGeneric: () => parseGeneric,
1008
- parseSymbols: () => parseSymbols3
1094
+ parseSymbols: () => parseSymbols
1009
1095
  });
1010
1096
  function patternsFor(lang) {
1011
1097
  return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
@@ -1093,7 +1179,7 @@ function parseGeneric(opts) {
1093
1179
  }
1094
1180
  return { file, lang, symbols, mtimeMs };
1095
1181
  }
1096
- async function parseSymbols3(opts) {
1182
+ async function parseSymbols(opts) {
1097
1183
  return parseGeneric(opts);
1098
1184
  }
1099
1185
  var C_LIKE, LANG_PATTERNS, KEYWORDS, GENERIC_MAX_SYMBOLS_DEFAULT, GENERIC_MAX_FILE_CHARS;
@@ -1326,13 +1412,14 @@ var init_generic_parser = __esm({
1326
1412
  var py_parser_exports = {};
1327
1413
  __export(py_parser_exports, {
1328
1414
  detectLang: () => detectLang,
1329
- parseSymbols: () => parseSymbols4
1415
+ parseSymbols: () => parseSymbols2,
1416
+ resolvePythonBinary: () => resolvePythonBinary
1330
1417
  });
1331
1418
  import { spawn as spawn2 } from "node:child_process";
1332
1419
  import * as fs5 from "node:fs/promises";
1333
1420
  import * as os2 from "node:os";
1334
1421
  import * as path8 from "node:path";
1335
- async function parseSymbols4(opts) {
1422
+ async function parseSymbols2(opts) {
1336
1423
  const { file, content, lang } = opts;
1337
1424
  try {
1338
1425
  const native = await withSpawnGate(() => syncPyParse(file, content, lang));
@@ -1406,6 +1493,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1406
1493
  });
1407
1494
  });
1408
1495
  }
1496
+ function resolvePythonBinary() {
1497
+ cachedPyBinary ??= resolvePython();
1498
+ return cachedPyBinary;
1499
+ }
1409
1500
  async function syncPyParse(filePath, content, lang) {
1410
1501
  try {
1411
1502
  if (!_cachedScriptPath) {
@@ -1696,9 +1787,713 @@ for node in ast.walk(tree):
1696
1787
  if name:
1697
1788
  refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1698
1789
 
1699
- print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1790
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1791
+ `;
1792
+ _cachedScriptPath = null;
1793
+ }
1794
+ });
1795
+
1796
+ // src/codebase-index/ts-parser.ts
1797
+ var ts_parser_exports = {};
1798
+ __export(ts_parser_exports, {
1799
+ detectLang: () => detectLang,
1800
+ parseSymbols: () => parseSymbols3
1801
+ });
1802
+ function loadTypescript() {
1803
+ tsLoad ??= import("@typescript/typescript6").then((m) => {
1804
+ ts = m.default ?? m;
1805
+ return ts;
1806
+ });
1807
+ return tsLoad;
1808
+ }
1809
+ function kindMap() {
1810
+ kindMapCache ??= {
1811
+ [ts.SyntaxKind.ClassDeclaration]: "class",
1812
+ [ts.SyntaxKind.InterfaceDeclaration]: "interface",
1813
+ [ts.SyntaxKind.EnumDeclaration]: "enum",
1814
+ [ts.SyntaxKind.TypeAliasDeclaration]: "type",
1815
+ [ts.SyntaxKind.FunctionDeclaration]: "function",
1816
+ [ts.SyntaxKind.MethodDeclaration]: "method",
1817
+ [ts.SyntaxKind.GetAccessor]: "property",
1818
+ [ts.SyntaxKind.SetAccessor]: "property",
1819
+ [ts.SyntaxKind.PropertyDeclaration]: "property",
1820
+ [ts.SyntaxKind.Parameter]: "parameter",
1821
+ [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
1822
+ };
1823
+ return kindMapCache;
1824
+ }
1825
+ function kindOf(node) {
1826
+ if (ts.isVariableDeclaration(node)) {
1827
+ const parent = node.parent;
1828
+ if (ts.isVariableDeclarationList(parent)) {
1829
+ const flags = parent.flags;
1830
+ if (flags & ts.NodeFlags.Let) return "let";
1831
+ if (flags & ts.NodeFlags.Const) return "const";
1832
+ return "var";
1833
+ }
1834
+ }
1835
+ if (ts.isModuleDeclaration(node)) return "namespace";
1836
+ return kindMap()[node.kind] ?? null;
1837
+ }
1838
+ function getSignature(printer, node, sourceFile) {
1839
+ const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1840
+ return raw.replace(/\s+/g, " ").slice(0, 500);
1841
+ }
1842
+ function getJsDoc(node, sourceFile) {
1843
+ const fullText = sourceFile.getFullText();
1844
+ const nodePos = node.getFullStart();
1845
+ const comments = ts.getLeadingCommentRanges(fullText, nodePos);
1846
+ if (!comments) return "";
1847
+ for (const range of comments) {
1848
+ const commentText = fullText.slice(range.pos, range.end);
1849
+ const trimmed = commentText.trim();
1850
+ if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
1851
+ const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
1852
+ return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
1853
+ }
1854
+ }
1855
+ return "";
1856
+ }
1857
+ function pushScopeName(node, parts) {
1858
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
1859
+ parts.push(node.name?.text ?? "Anon");
1860
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
1861
+ if (node.name && ts.isIdentifier(node.name)) {
1862
+ parts.push(node.name.text);
1863
+ }
1864
+ }
1865
+ }
1866
+ async function parseSymbols3(opts) {
1867
+ const { file, content, lang } = opts;
1868
+ await loadTypescript();
1869
+ let sourceFile;
1870
+ try {
1871
+ sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
1872
+ } catch {
1873
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1874
+ }
1875
+ const symbols = [];
1876
+ const refs = [];
1877
+ const printer = ts.createPrinter({});
1878
+ function visit(node, funcDepth, scopeParts) {
1879
+ const kind = kindOf(node);
1880
+ if (kind) {
1881
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
1882
+ } else {
1883
+ const nameNode = node.name;
1884
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
1885
+ return;
1886
+ }
1887
+ const name = nameNode.text;
1888
+ const pos2 = nameNode.getStart(sourceFile);
1889
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
1890
+ const scope = scopeParts.join(".");
1891
+ const signature = getSignature(printer, node, sourceFile);
1892
+ const docComment = getJsDoc(node, sourceFile);
1893
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
1894
+ symbols.push({
1895
+ id: 0,
1896
+ lang,
1897
+ kind,
1898
+ name,
1899
+ file,
1900
+ line: line2 + 1,
1901
+ col: character,
1902
+ signature,
1903
+ docComment,
1904
+ scope,
1905
+ text
1906
+ });
1907
+ }
1908
+ }
1909
+ const pos = node.getStart(sourceFile);
1910
+ const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1911
+ const lineNum = line + 1;
1912
+ if (ts.isCallExpression(node)) {
1913
+ const expr = node.expression;
1914
+ if (ts.isIdentifier(expr)) {
1915
+ refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
1916
+ }
1917
+ } else if (ts.isPropertyAccessExpression(node)) {
1918
+ if (ts.isIdentifier(node.expression)) {
1919
+ refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
1920
+ }
1921
+ } else if (ts.isTypeReferenceNode(node)) {
1922
+ const name = getTypeName(node.typeName);
1923
+ if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
1924
+ } else if (ts.isHeritageClause(node)) {
1925
+ for (const t of node.types) {
1926
+ const name = getTypeName(t.expression);
1927
+ if (name)
1928
+ refs.push({
1929
+ fromId: 0,
1930
+ toName: name,
1931
+ callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
1932
+ line: lineNum
1933
+ });
1934
+ }
1935
+ } else if (ts.isImportDeclaration(node)) {
1936
+ emitImportSpecifierRefs(node, refs, lineNum);
1937
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
1938
+ emitExportSpecifierRefs(node, refs, lineNum);
1939
+ }
1940
+ const scopeIdx = scopeParts.length;
1941
+ pushScopeName(node, scopeParts);
1942
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
1943
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
1944
+ scopeParts.length = scopeIdx;
1945
+ }
1946
+ visit(sourceFile, 0, []);
1947
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1948
+ }
1949
+ function getTypeName(name) {
1950
+ if (ts.isIdentifier(name)) return name.text;
1951
+ if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
1952
+ return "";
1953
+ }
1954
+ function deduplicateRefs(refs) {
1955
+ const seen = /* @__PURE__ */ new Set();
1956
+ return refs.filter((r) => {
1957
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
1958
+ if (seen.has(key)) return false;
1959
+ seen.add(key);
1960
+ return true;
1961
+ });
1962
+ }
1963
+ function getImportSpecifierName(spec) {
1964
+ return spec.propertyName?.text ?? spec.name.text;
1965
+ }
1966
+ function emitImportSpecifierRefs(node, refs, lineNum) {
1967
+ const module = moduleSpecifierOf(node.moduleSpecifier);
1968
+ const clause = node.importClause;
1969
+ if (!clause) {
1970
+ if (module) {
1971
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
1972
+ }
1973
+ return;
1974
+ }
1975
+ if (clause.name) {
1976
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
1977
+ }
1978
+ const bindings = clause.namedBindings;
1979
+ if (!bindings) return;
1980
+ if (ts.isNamedImports(bindings)) {
1981
+ for (const element of bindings.elements) {
1982
+ refs.push({
1983
+ fromId: 0,
1984
+ toName: getImportSpecifierName(element),
1985
+ callType: "import",
1986
+ line: lineNum,
1987
+ module
1988
+ });
1989
+ }
1990
+ } else if (ts.isNamespaceImport(bindings)) {
1991
+ refs.push({
1992
+ fromId: 0,
1993
+ toName: bindings.name.text,
1994
+ callType: "import",
1995
+ line: lineNum,
1996
+ module
1997
+ });
1998
+ }
1999
+ }
2000
+ function moduleSpecifierOf(node) {
2001
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
2002
+ }
2003
+ function emitExportSpecifierRefs(node, refs, lineNum) {
2004
+ const module = moduleSpecifierOf(node.moduleSpecifier);
2005
+ const clause = node.exportClause;
2006
+ if (clause && ts.isNamespaceExport(clause)) {
2007
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
2008
+ return;
2009
+ }
2010
+ if (clause && ts.isNamedExports(clause)) {
2011
+ for (const element of clause.elements) {
2012
+ const originalName = element.propertyName?.text ?? element.name.text;
2013
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
2014
+ }
2015
+ return;
2016
+ }
2017
+ if (module) {
2018
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
2019
+ }
2020
+ }
2021
+ var ts, tsLoad, kindMapCache;
2022
+ var init_ts_parser = __esm({
2023
+ "src/codebase-index/ts-parser.ts"() {
2024
+ "use strict";
2025
+ init_languages();
2026
+ tsLoad = null;
2027
+ kindMapCache = null;
2028
+ }
2029
+ });
2030
+
2031
+ // src/codebase-index/go-parser.ts
2032
+ var go_parser_exports = {};
2033
+ __export(go_parser_exports, {
2034
+ detectLang: () => detectLang,
2035
+ parseSymbols: () => parseSymbols4
2036
+ });
2037
+ import { spawn as spawn3 } from "node:child_process";
2038
+ import * as os3 from "node:os";
2039
+ import * as path9 from "node:path";
2040
+ import * as fs6 from "node:fs/promises";
2041
+ async function parseSymbols4(opts) {
2042
+ const { file, content, lang } = opts;
2043
+ try {
2044
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
2045
+ if (parsed.symbols.length > 0) {
2046
+ return parsed;
2047
+ }
2048
+ const fallback = fallbackParse(file, content, lang);
2049
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
2050
+ } catch {
2051
+ return fallbackParse(file, content, lang);
2052
+ }
2053
+ }
2054
+ function fallbackParse(filePath, content, lang) {
2055
+ if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
2056
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2057
+ }
2058
+ const symbols = [];
2059
+ const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
2060
+ const lines = content.split(/\r?\n/);
2061
+ for (const [idx, line] of lines.entries()) {
2062
+ const trimmed = line.trimStart();
2063
+ const col = line.length - trimmed.length + 1;
2064
+ const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
2065
+ if (fn?.[1]) {
2066
+ addFallbackSymbol(symbols, {
2067
+ filePath,
2068
+ lang,
2069
+ kind: trimmed.startsWith("func (") ? "method" : "function",
2070
+ name: fn[1],
2071
+ line: idx + 1,
2072
+ col,
2073
+ signature: trimmed,
2074
+ scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
2075
+ });
2076
+ continue;
2077
+ }
2078
+ const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2079
+ if (typeDecl?.[1]) {
2080
+ addFallbackSymbol(symbols, {
2081
+ filePath,
2082
+ lang,
2083
+ kind: "type",
2084
+ name: typeDecl[1],
2085
+ line: idx + 1,
2086
+ col,
2087
+ signature: trimmed,
2088
+ scope: packageName
2089
+ });
2090
+ continue;
2091
+ }
2092
+ const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2093
+ if (valueDecl?.[1] && valueDecl[2]) {
2094
+ addFallbackSymbol(symbols, {
2095
+ filePath,
2096
+ lang,
2097
+ kind: valueDecl[1],
2098
+ name: valueDecl[2],
2099
+ line: idx + 1,
2100
+ col,
2101
+ signature: trimmed,
2102
+ scope: packageName
2103
+ });
2104
+ }
2105
+ }
2106
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2107
+ }
2108
+ function addFallbackSymbol(symbols, opts) {
2109
+ symbols.push({
2110
+ id: 0,
2111
+ lang: opts.lang,
2112
+ kind: opts.kind,
2113
+ name: opts.name,
2114
+ file: opts.filePath,
2115
+ line: opts.line,
2116
+ col: opts.col,
2117
+ signature: opts.signature,
2118
+ docComment: "",
2119
+ scope: opts.scope,
2120
+ text: `${opts.name} ${opts.signature}`.trim()
2121
+ });
2122
+ }
2123
+ function hasUnbalancedDelimiters(content) {
2124
+ const pairs = { "(": ")", "[": "]", "{": "}" };
2125
+ const closers = new Set(Object.values(pairs));
2126
+ const stack = [];
2127
+ for (const ch of content) {
2128
+ if (pairs[ch]) {
2129
+ stack.push(pairs[ch]);
2130
+ } else if (closers.has(ch) && stack.pop() !== ch) {
2131
+ return true;
2132
+ }
2133
+ }
2134
+ return stack.length > 0;
2135
+ }
2136
+ async function syncGoParse(filePath, content, lang) {
2137
+ try {
2138
+ let scriptPath = _cachedGoScriptPath;
2139
+ if (!scriptPath) {
2140
+ const tmpDir = await fs6.mkdtemp(path9.join(os3.tmpdir(), "ws-go-parse-"));
2141
+ scriptPath = path9.join(tmpDir, "parse.go");
2142
+ await fs6.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2143
+ _cachedGoScriptPath = scriptPath;
2144
+ }
2145
+ const goBinary = resolveWin32Command("go");
2146
+ const goResult = await new Promise(
2147
+ (resolve4, reject) => {
2148
+ let settled = false;
2149
+ const proc = spawn3(goBinary, ["run", scriptPath], {
2150
+ stdio: ["pipe", "pipe", "pipe"],
2151
+ windowsHide: true
2152
+ });
2153
+ proc.on("error", (err) => {
2154
+ if (settled) return;
2155
+ settled = true;
2156
+ reject(err);
2157
+ });
2158
+ let stdout2 = "";
2159
+ proc.stdout?.on("data", (chunk) => {
2160
+ stdout2 += chunk.toString();
2161
+ });
2162
+ proc.stderr?.resume();
2163
+ proc.stdin?.write(content);
2164
+ proc.stdin?.end();
2165
+ const timer = setTimeout(() => {
2166
+ if (settled) return;
2167
+ settled = true;
2168
+ proc.kill("SIGKILL");
2169
+ reject(new Error("timeout"));
2170
+ }, 15e3);
2171
+ timer.unref?.();
2172
+ proc.on("close", (code2) => {
2173
+ if (settled) return;
2174
+ settled = true;
2175
+ clearTimeout(timer);
2176
+ resolve4({ code: code2, stdout: stdout2 });
2177
+ });
2178
+ }
2179
+ );
2180
+ const { code, stdout } = goResult;
2181
+ if (code !== 0 || !stdout.trim()) {
2182
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2183
+ }
2184
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
2185
+ const symbols = rawSymbols.map((s) => ({
2186
+ id: 0,
2187
+ lang,
2188
+ kind: s.kind,
2189
+ name: s.name,
2190
+ file: filePath,
2191
+ line: s.line,
2192
+ col: s.col,
2193
+ signature: s.signature ?? "",
2194
+ docComment: "",
2195
+ scope: s.scope ?? "",
2196
+ text: `${s.name} ${s.signature ?? ""}`.trim()
2197
+ }));
2198
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
2199
+ } catch {
2200
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2201
+ }
2202
+ }
2203
+ var GO_PARSE_SCRIPT, _cachedGoScriptPath;
2204
+ var init_go_parser = __esm({
2205
+ "src/codebase-index/go-parser.ts"() {
2206
+ "use strict";
2207
+ init_win32_resolve();
2208
+ init_parser_output();
2209
+ init_spawn_gate();
2210
+ init_languages();
2211
+ GO_PARSE_SCRIPT = `
2212
+ package main
2213
+
2214
+ import (
2215
+ "encoding/json"
2216
+ "fmt"
2217
+ "go/ast"
2218
+ "go/parser"
2219
+ "go/token"
2220
+ "io"
2221
+ "os"
2222
+ "strconv"
2223
+ "strings"
2224
+ )
2225
+
2226
+ type Sym struct {
2227
+ Name string \`json:"name"\`
2228
+ Kind string \`json:"kind"\`
2229
+ Line int \`json:"line"\`
2230
+ Col int \`json:"col"\`
2231
+ Signature string \`json:"signature"\`
2232
+ Scope string \`json:"scope"\`
2233
+ }
2234
+
2235
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
2236
+ // yields both. Module is the import path for CallType "import", else empty.
2237
+ type Ref struct {
2238
+ ToName string \`json:"toName"\`
2239
+ CallType string \`json:"callType"\`
2240
+ Line int \`json:"line"\`
2241
+ Module string \`json:"module"\`
2242
+ }
2243
+
2244
+ type Result struct {
2245
+ Symbols []Sym \`json:"symbols"\`
2246
+ Refs []Ref \`json:"refs"\`
2247
+ }
2248
+
2249
+ func emptyResult() string {
2250
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
2251
+ }
2252
+
2253
+ func main() {
2254
+ src, err := io.ReadAll(os.Stdin)
2255
+ if err != nil {
2256
+ fmt.Print(emptyResult())
2257
+ return
2258
+ }
2259
+ fset := token.NewFileSet()
2260
+ node, err := parser.ParseFile(fset, "src.go", src, 0)
2261
+ if err != nil {
2262
+ fmt.Print(emptyResult())
2263
+ return
2264
+ }
2265
+
2266
+ var syms []Sym
2267
+
2268
+ // Package-level scope
2269
+ pkgScope := node.Name.Name
2270
+
2271
+ // Collect all top-level declarations
2272
+ for _, decl := range node.Decls {
2273
+ switch d := decl.(type) {
2274
+ case *ast.FuncDecl:
2275
+ name := d.Name.Name
2276
+ kind := "function"
2277
+ scope := pkgScope
2278
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2279
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
2280
+ kind = "method"
2281
+ } else {
2282
+ scope = pkgScope + "." + name
2283
+ }
2284
+ pos := fset.Position(d.Pos())
2285
+ sig := formatFuncSig(d)
2286
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
2287
+
2288
+ case *ast.GenDecl:
2289
+ for _, spec := range d.Specs {
2290
+ switch s := spec.(type) {
2291
+ case *ast.TypeSpec:
2292
+ name := s.Name.Name
2293
+ pos := fset.Position(s.Pos())
2294
+ sig := "type " + name
2295
+ if s.TypeParams != nil {
2296
+ sig += formatTypeParams(s.TypeParams)
2297
+ }
2298
+ if st, ok := s.Type.(*ast.StructType); ok {
2299
+ sig += " = struct { " + formatFields(st.Fields.List) + " }"
2300
+ } else if it, ok := s.Type.(*ast.InterfaceType); ok {
2301
+ sig += " = interface { " + formatMethods(it.Methods.List) + " }"
2302
+ } else {
2303
+ sig += " = " + formatType(s.Type)
2304
+ }
2305
+ syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2306
+
2307
+ case *ast.ValueSpec:
2308
+ for _, n := range s.Names {
2309
+ name := n.Name
2310
+ pos := fset.Position(n.Pos())
2311
+ kind := "var"
2312
+ if d.Tok == token.CONST {
2313
+ kind = "const"
2314
+ }
2315
+ sig := kind + " " + name
2316
+ if s.Type != nil {
2317
+ sig += " " + formatType(s.Type)
2318
+ }
2319
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2320
+ }
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2325
+
2326
+ refs := []Ref{}
2327
+ ast.Inspect(node, func(n ast.Node) bool {
2328
+ switch expr := n.(type) {
2329
+ case *ast.CallExpr:
2330
+ line := fset.Position(expr.Pos()).Line
2331
+ switch fun := expr.Fun.(type) {
2332
+ case *ast.Ident:
2333
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
2334
+ case *ast.SelectorExpr:
2335
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
2336
+ // declared symbol name, so it resolves the same way the TypeScript
2337
+ // and Python extractors' call refs do.
2338
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
2339
+ }
2340
+ case *ast.ImportSpec:
2341
+ if expr.Path != nil {
2342
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
2343
+ line := fset.Position(expr.Pos()).Line
2344
+ // A Go import names a package, not a symbol; the package's
2345
+ // last path segment is the name it is referenced by.
2346
+ name := importPath
2347
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
2348
+ name = importPath[idx+1:]
2349
+ }
2350
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
2351
+ }
2352
+ }
2353
+ }
2354
+ return true
2355
+ })
2356
+
2357
+ if syms == nil {
2358
+ syms = []Sym{}
2359
+ }
2360
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
2361
+ if err != nil {
2362
+ fmt.Print(emptyResult())
2363
+ return
2364
+ }
2365
+ fmt.Print(string(data))
2366
+ }
2367
+
2368
+ func recvTypeName(t ast.Expr) string {
2369
+ switch v := t.(type) {
2370
+ case *ast.Ident:
2371
+ return v.Name
2372
+ case *ast.StarExpr:
2373
+ return recvTypeName(v.X)
2374
+ default:
2375
+ return "?"
2376
+ }
2377
+ }
2378
+
2379
+ func formatFuncSig(d *ast.FuncDecl) string {
2380
+ scope := ""
2381
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2382
+ scope = "(" + formatFieldList(d.Recv.List) + ") "
2383
+ }
2384
+ scope += formatFuncType(d.Type)
2385
+ return "func " + scope
2386
+ }
2387
+
2388
+ func formatFuncType(f *ast.FuncType) string {
2389
+ params := formatFieldList(f.Params.List)
2390
+ results := ""
2391
+ if f.Results != nil {
2392
+ results = " -> " + formatFieldList(f.Results.List)
2393
+ }
2394
+ return params + results
2395
+ }
2396
+
2397
+ func formatFieldList(fields []*ast.Field) string {
2398
+ if len(fields) == 0 {
2399
+ return "()"
2400
+ }
2401
+ names := make([]string, 0, len(fields))
2402
+ for _, f := range fields {
2403
+ name := ""
2404
+ if len(f.Names) > 0 {
2405
+ name = f.Names[0].Name
2406
+ }
2407
+ t := formatType(f.Type)
2408
+ if name != "" {
2409
+ names = append(names, name+" "+t)
2410
+ } else {
2411
+ names = append(names, t)
2412
+ }
2413
+ }
2414
+ return "(" + strings.Join(names, ", ") + ")"
2415
+ }
2416
+
2417
+ func formatFields(fields []*ast.Field) string {
2418
+ lines := make([]string, 0)
2419
+ for _, f := range fields {
2420
+ name := ""
2421
+ if len(f.Names) > 0 {
2422
+ name = f.Names[0].Name
2423
+ }
2424
+ t := formatType(f.Type)
2425
+ if name != "" {
2426
+ lines = append(lines, name+" "+t)
2427
+ } else {
2428
+ lines = append(lines, t)
2429
+ }
2430
+ }
2431
+ return strings.Join(lines, "; ")
2432
+ }
2433
+
2434
+ func formatMethods(fields []*ast.Field) string {
2435
+ return formatFields(fields)
2436
+ }
2437
+
2438
+ func formatTypeParams(tp *ast.FieldList) string {
2439
+ if tp == nil || len(tp.List) == 0 {
2440
+ return ""
2441
+ }
2442
+ params := make([]string, len(tp.List))
2443
+ for i, p := range tp.List {
2444
+ if len(p.Names) > 0 {
2445
+ params[i] = p.Names[0].Name
2446
+ } else {
2447
+ params[i] = "T"
2448
+ }
2449
+ }
2450
+ return "[" + strings.Join(params, ", ") + "]"
2451
+ }
2452
+
2453
+ func formatType(t ast.Expr) string {
2454
+ if t == nil {
2455
+ return "?"
2456
+ }
2457
+ switch v := t.(type) {
2458
+ case *ast.Ident:
2459
+ return v.Name
2460
+ case *ast.SelectorExpr:
2461
+ return formatType(v.X) + "." + v.Sel.Name
2462
+ case *ast.StarExpr:
2463
+ return "*" + formatType(v.X)
2464
+ case *ast.ArrayType:
2465
+ if v.Len == nil {
2466
+ return "[]" + formatType(v.Elt)
2467
+ }
2468
+ return "[...]" + formatType(v.Elt)
2469
+ case *ast.MapType:
2470
+ return "map[" + formatType(v.Key) + "]" + formatType(v.Value)
2471
+ case *ast.InterfaceType:
2472
+ return "interface{}"
2473
+ case *ast.StructType:
2474
+ return "struct{}"
2475
+ case *ast.FuncType:
2476
+ return formatFuncType(v)
2477
+ case *ast.ChanType:
2478
+ return "chan " + formatType(v.Value)
2479
+ case *ast.BasicLit:
2480
+ return v.Value
2481
+ case *ast.IndexExpr:
2482
+ // Generic instantiation with one type arg, e.g. Logger[int].
2483
+ return formatType(v.X) + "[" + formatType(v.Index) + "]"
2484
+ case *ast.IndexListExpr:
2485
+ // Generic instantiation with multiple type args, e.g. Map[K, V].
2486
+ args := make([]string, len(v.Indices))
2487
+ for i, idx := range v.Indices {
2488
+ args[i] = formatType(idx)
2489
+ }
2490
+ return formatType(v.X) + "[" + strings.Join(args, ", ") + "]"
2491
+ default:
2492
+ return "?"
2493
+ }
2494
+ }
1700
2495
  `;
1701
- _cachedScriptPath = null;
2496
+ _cachedGoScriptPath = null;
1702
2497
  }
1703
2498
  });
1704
2499
 
@@ -1794,7 +2589,7 @@ __export(json_parser_exports, {
1794
2589
  parseSymbols: () => parseSymbols6
1795
2590
  });
1796
2591
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1797
- import * as path9 from "node:path";
2592
+ import * as path10 from "node:path";
1798
2593
  function parseSymbols6(opts) {
1799
2594
  const { file, content, lang } = opts;
1800
2595
  try {
@@ -1806,7 +2601,7 @@ function parseSymbols6(opts) {
1806
2601
  function regexParse2(opts) {
1807
2602
  const { file, content, lang } = opts;
1808
2603
  const symbols = [];
1809
- const basename7 = path9.basename(file).toLowerCase();
2604
+ const basename7 = path10.basename(file).toLowerCase();
1810
2605
  const isPackageJson = basename7 === "package.json";
1811
2606
  const isTsconfig = basename7 === "tsconfig.json" || basename7 === "tsconfig.build.json";
1812
2607
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1832,11 +2627,11 @@ function regexParse2(opts) {
1832
2627
  const line = lineFromOffset(offset);
1833
2628
  symbols.push(
1834
2629
  makeSymbol({
1835
- name: path9.basename(file),
2630
+ name: path10.basename(file),
1836
2631
  kind: "object",
1837
2632
  line,
1838
2633
  col: 0,
1839
- signature: `"${path9.basename(file)}" = { ... }`,
2634
+ signature: `"${path10.basename(file)}" = { ... }`,
1840
2635
  file,
1841
2636
  lang
1842
2637
  })
@@ -2164,6 +2959,106 @@ var init_yaml_parser = __esm({
2164
2959
  });
2165
2960
 
2166
2961
  // src/codebase-index/tree-sitter/queries.ts
2962
+ function parseGroupedUse(text) {
2963
+ const open = text.indexOf("{");
2964
+ const close = text.lastIndexOf("}");
2965
+ if (open < 0 || close <= open) return null;
2966
+ const prefix = text.slice(0, open).replace(/[\\/]+$/, "");
2967
+ const out = [];
2968
+ for (const rawMember of text.slice(open + 1, close).split(",")) {
2969
+ let member = rawMember.trim();
2970
+ if (!member) continue;
2971
+ member = member.replace(
2972
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2973
+ ""
2974
+ ).trim();
2975
+ if (!member) continue;
2976
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(member);
2977
+ if (aliasMatch) member = member.slice(0, aliasMatch.index).trim();
2978
+ if (!member) continue;
2979
+ const module = prefix ? `${prefix}\\${member}` : member;
2980
+ const toName = member.split(/[\\/]/).filter(Boolean).pop();
2981
+ if (toName) out.push({ toName, callType: "import", module });
2982
+ }
2983
+ return out.length ? out : null;
2984
+ }
2985
+ function importFromText(prefixes) {
2986
+ return (node) => {
2987
+ let text = node.text.replace(/\s+/g, " ").trim();
2988
+ for (const prefix of prefixes) {
2989
+ if (text.startsWith(prefix)) text = text.slice(prefix.length).trim();
2990
+ }
2991
+ text = text.replace(
2992
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2993
+ ""
2994
+ ).trim();
2995
+ if (text.includes("{")) return parseGroupedUse(text);
2996
+ if (text.includes(",") && !text.includes("<") && !text.includes("=")) {
2997
+ const out = [];
2998
+ for (const clause of text.split(",")) {
2999
+ const one = oneImportClause(clause.trim());
3000
+ if (one) out.push(one);
3001
+ }
3002
+ return out.length ? out : null;
3003
+ }
3004
+ const single = oneImportClause(text);
3005
+ return single ? [single] : null;
3006
+ };
3007
+ }
3008
+ function oneImportClause(rawClause) {
3009
+ let text = rawClause;
3010
+ text = text.replace(
3011
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
3012
+ ""
3013
+ ).trim();
3014
+ text = text.replace(/[;}]+$/g, "").trim();
3015
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(text);
3016
+ if (aliasMatch) text = text.slice(0, aliasMatch.index).trim();
3017
+ const eqMatch = /^([A-Za-z_]\w*)\s*=\s*(.+)$/.exec(text);
3018
+ if (eqMatch) text = eqMatch[2].trim();
3019
+ if (!text) return null;
3020
+ if (text.endsWith("*")) text = text.slice(0, -1).replace(/[.]$/, "");
3021
+ if (!text) return null;
3022
+ const module = text;
3023
+ const toName = module.split(/[.\\/]/).filter(Boolean).pop()?.replace(/<.*>$/s, "");
3024
+ if (!toName) return null;
3025
+ return { toName, callType: "import", module };
3026
+ }
3027
+ function heritageLeaf(node, depth) {
3028
+ if (depth > 6) return null;
3029
+ const named = node.childForFieldName("name");
3030
+ if (named) {
3031
+ if (named.type === "scoped_type_identifier" || named.type === "qualified_name" || named.type === "scope_resolution" || named.type === "user_type") {
3032
+ return heritageLeaf(named, depth + 1);
3033
+ }
3034
+ return named.text;
3035
+ }
3036
+ const children = [];
3037
+ for (let i = 0; i < node.namedChildCount; i++) {
3038
+ const c = node.namedChild(i);
3039
+ if (c) children.push(c);
3040
+ }
3041
+ for (let i = children.length - 1; i >= 0; i--) {
3042
+ const c = children[i];
3043
+ if (c.type === "type_arguments" || c.type === "type_argument_list" || // cpp: (template_type arguments: (template_argument_list …)) — the
3044
+ // descriptor's type_identifier inside it is never the declared base.
3045
+ c.type === "template_argument_list" || c.type === "type_parameter_list" || c.type === "type_projection" || c.type === "value_arguments") {
3046
+ continue;
3047
+ }
3048
+ if (c.type === "type_identifier" || c.type === "identifier" || c.type === "constant" || c.type === "name") {
3049
+ return c.text;
3050
+ }
3051
+ if (c.type === "scoped_type_identifier" || c.type === "qualified_name" || c.type === "scope_resolution" || c.type === "user_type") {
3052
+ return heritageLeaf(c, depth + 1);
3053
+ }
3054
+ }
3055
+ return leafSegment(
3056
+ node.text.replace(/\\/g, ".").replace(/::/g, ".").replace(/<[^<>]*>$/, "")
3057
+ );
3058
+ }
3059
+ function leafSegment(text) {
3060
+ return text.split(".").filter(Boolean).pop() ?? text;
3061
+ }
2167
3062
  function getQueries(lang) {
2168
3063
  return LANG_QUERIES[lang] ?? DEFAULT_QUERIES;
2169
3064
  }
@@ -2175,10 +3070,127 @@ function readFirstString(node) {
2175
3070
  const child = node.namedChild(0);
2176
3071
  return child ? readFirstString(child) : null;
2177
3072
  }
2178
- var DEFAULT_QUERIES, LANG_QUERIES;
3073
+ var heritageExtractor, cCallExtractor, rubyCallExtractor, firstIdentifierCallExtractor, cIncludeExtractor, phpConstructorExtractor, DEFAULT_QUERIES, LANG_QUERIES;
2179
3074
  var init_queries = __esm({
2180
3075
  "src/codebase-index/tree-sitter/queries.ts"() {
2181
3076
  "use strict";
3077
+ heritageExtractor = (node) => {
3078
+ const out = [];
3079
+ const SKIP_SUBTREES = /* @__PURE__ */ new Set([
3080
+ "type_arguments",
3081
+ "type_argument_list",
3082
+ // tree-sitter-cpp names its argument subtree template_argument_list —
3083
+ // verified AST: (base_class_clause (template_type name:
3084
+ // (type_identifier) arguments: (template_argument_list
3085
+ // (type_descriptor type: (type_identifier))))). Without this entry
3086
+ // `class D : Base<Foo>` recurses into the descriptor and emits Foo as a
3087
+ // phantom inherit ref.
3088
+ "template_argument_list",
3089
+ "type_parameter_list",
3090
+ "type_projection",
3091
+ "value_arguments"
3092
+ ]);
3093
+ const collect = (current, depth) => {
3094
+ if (depth > 4) return;
3095
+ for (let i = 0; i < current.namedChildCount; i++) {
3096
+ const child = current.namedChild(i);
3097
+ if (!child) continue;
3098
+ if (SKIP_SUBTREES.has(child.type)) continue;
3099
+ if (child.type === "type_identifier" || child.type === "identifier" || child.type === "named_type" || child.type === "type" || // PHP heritage carries `name`; Ruby a `constant`.
3100
+ child.type === "constant" || child.type === "name") {
3101
+ const name = child.type === "named_type" ? leafSegment(child.text) : child.text;
3102
+ if (name) out.push({ toName: name });
3103
+ continue;
3104
+ }
3105
+ if (child.type === "generic_type" || child.type === "generic_name") {
3106
+ for (let j = 0; j < child.namedChildCount; j++) {
3107
+ const inner = child.namedChild(j);
3108
+ if (inner && !SKIP_SUBTREES.has(inner.type) && (inner.type === "type_identifier" || inner.type === "identifier" || inner.type === "name")) {
3109
+ out.push({ toName: inner.text });
3110
+ break;
3111
+ }
3112
+ }
3113
+ continue;
3114
+ }
3115
+ if (child.type === "qualified_name" || child.type === "scoped_type_identifier" || child.type === "user_type" || child.type === "scope_resolution") {
3116
+ const leaf = heritageLeaf(child, 0);
3117
+ if (leaf) out.push({ toName: leaf });
3118
+ continue;
3119
+ }
3120
+ collect(child, depth + 1);
3121
+ }
3122
+ };
3123
+ collect(node, 0);
3124
+ return out;
3125
+ };
3126
+ cCallExtractor = (node) => {
3127
+ const fn = node.childForFieldName("function");
3128
+ if (!fn) return null;
3129
+ if (fn.type === "field_expression") {
3130
+ const field = fn.childForFieldName("field");
3131
+ if (field) return [{ toName: field.text, callType: "call" }];
3132
+ const seg = fn.text.split("->").filter(Boolean).pop();
3133
+ if (seg) return [{ toName: leafSegment(seg.split(".")[0] ?? seg), callType: "call" }];
3134
+ return null;
3135
+ }
3136
+ if (fn.type === "qualified_identifier") {
3137
+ const name = fn.childForFieldName("name");
3138
+ if (name) return [{ toName: name.text, callType: "call" }];
3139
+ const seg = fn.text.split("::").filter(Boolean).pop();
3140
+ if (seg) return [{ toName: seg.split(/[<(]/)[0].trim(), callType: "call" }];
3141
+ return null;
3142
+ }
3143
+ return [{ toName: fn.text.split(/[<(]/)[0].trim(), callType: "call" }];
3144
+ };
3145
+ rubyCallExtractor = (node) => {
3146
+ const emissions = [];
3147
+ const method = node.childForFieldName("method");
3148
+ if (method) {
3149
+ const name = method.text;
3150
+ if (name && !name.includes(" ")) emissions.push({ toName: name, callType: "call" });
3151
+ if (name === "require" || name === "require_relative") {
3152
+ const args = node.childForFieldName("arguments");
3153
+ const first = args?.namedChild(0);
3154
+ if (first) {
3155
+ const raw = first.text.replace(/^['"]|['"]$/g, "");
3156
+ const toName = raw.split("/").filter(Boolean).pop();
3157
+ if (toName) emissions.push({ toName, callType: "import", module: raw });
3158
+ }
3159
+ }
3160
+ }
3161
+ return emissions;
3162
+ };
3163
+ firstIdentifierCallExtractor = (node) => {
3164
+ for (let i = 0; i < node.namedChildCount; i++) {
3165
+ const child = node.namedChild(i);
3166
+ if (child && (child.type === "simple_identifier" || child.type === "identifier")) {
3167
+ return [{ toName: child.text, callType: "call" }];
3168
+ }
3169
+ }
3170
+ const first = node.namedChild(0);
3171
+ if (!first) return null;
3172
+ const leaf = leafSegment(first.text.split(/[<(]/)[0] ?? first.text);
3173
+ if (!leaf) return null;
3174
+ return [{ toName: leaf, callType: "call" }];
3175
+ };
3176
+ cIncludeExtractor = (node) => {
3177
+ const raw = node.text.replace(/^#\s*include\s*/i, "").trim();
3178
+ const module = raw.replace(/^["'<]|["'>]$/g, "");
3179
+ if (!module) return null;
3180
+ const toName = module.split("/").pop()?.replace(/\.h$/, "");
3181
+ if (!toName) return null;
3182
+ return [{ toName, callType: "import", module }];
3183
+ };
3184
+ phpConstructorExtractor = (node) => {
3185
+ for (let i = 0; i < node.namedChildCount; i++) {
3186
+ const child = node.namedChild(i);
3187
+ if (child && (child.type === "qualified_name" || child.type === "name")) {
3188
+ const leaf = child.text.split(/[\\]/).filter(Boolean).pop();
3189
+ if (leaf) return [{ toName: leaf, callType: "call" }];
3190
+ }
3191
+ }
3192
+ return null;
3193
+ };
2182
3194
  DEFAULT_QUERIES = {
2183
3195
  declKinds: {}
2184
3196
  };
@@ -2211,7 +3223,13 @@ var init_queries = __esm({
2211
3223
  "struct_specifier",
2212
3224
  "union_specifier",
2213
3225
  "enum_specifier"
2214
- ])
3226
+ ]),
3227
+ refRules: {
3228
+ // `obj->run()` and `Cls::stat()` carry structured function fields —
3229
+ // cCallExtractor handles all three AST shapes.
3230
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3231
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor }
3232
+ }
2215
3233
  },
2216
3234
  cpp: {
2217
3235
  declKinds: {
@@ -2242,7 +3260,13 @@ var init_queries = __esm({
2242
3260
  "union_specifier",
2243
3261
  "enum_specifier",
2244
3262
  "namespace_definition"
2245
- ])
3263
+ ]),
3264
+ refRules: {
3265
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3266
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor },
3267
+ // `class Foo : public Bar, private Baz` — the base-class clause.
3268
+ base_class_clause: { callType: "inherit", nameExtractor: heritageExtractor }
3269
+ }
2246
3270
  },
2247
3271
  java: {
2248
3272
  declKinds: {
@@ -2277,7 +3301,22 @@ var init_queries = __esm({
2277
3301
  "interface_declaration",
2278
3302
  "enum_declaration",
2279
3303
  "record_declaration"
2280
- ])
3304
+ ]),
3305
+ refRules: {
3306
+ method_invocation: { callType: "call", field: "name" },
3307
+ object_creation_expression: { callType: "call", field: "type" },
3308
+ // Verified AST: `superclass: (superclass (type_identifier))` and
3309
+ // `interfaces: (super_interfaces (type_list ...))` — no underscores.
3310
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor },
3311
+ super_interfaces: {
3312
+ callType: "implement",
3313
+ nameExtractor: heritageExtractor
3314
+ },
3315
+ import_declaration: {
3316
+ callType: "import",
3317
+ nameExtractor: importFromText(["import "])
3318
+ }
3319
+ }
2281
3320
  },
2282
3321
  csharp: {
2283
3322
  // C# 10+ `namespace Foo.Bar;` produces this node type. The legacy block
@@ -2315,7 +3354,18 @@ var init_queries = __esm({
2315
3354
  "struct_declaration",
2316
3355
  "enum_declaration",
2317
3356
  "record_declaration"
2318
- ])
3357
+ ]),
3358
+ refRules: {
3359
+ // Verified AST: `invocation_expression function: (identifier)` — the
3360
+ // callee field is `function` (C-style), not `name`.
3361
+ invocation_expression: { callType: "call", field: "function" },
3362
+ object_creation_expression: { callType: "call", field: "type" },
3363
+ base_list: { callType: "inherit", nameExtractor: heritageExtractor },
3364
+ using_directive: {
3365
+ callType: "import",
3366
+ nameExtractor: importFromText(["using "])
3367
+ }
3368
+ }
2319
3369
  },
2320
3370
  php: {
2321
3371
  declKinds: {
@@ -2334,7 +3384,7 @@ var init_queries = __esm({
2334
3384
  interface_declaration: "name",
2335
3385
  trait_declaration: "name",
2336
3386
  enum_declaration: "name",
2337
- namespace_declaration: "name"
3387
+ namespace_definition: "name"
2338
3388
  },
2339
3389
  scopeNodes: /* @__PURE__ */ new Set([
2340
3390
  "program",
@@ -2343,7 +3393,24 @@ var init_queries = __esm({
2343
3393
  "interface_declaration",
2344
3394
  "trait_declaration",
2345
3395
  "enum_declaration"
2346
- ])
3396
+ ]),
3397
+ refRules: {
3398
+ function_call_expression: { callType: "call", field: "function" },
3399
+ // Verified AST: `new App\Model\User()` carries a BARE qualified_name
3400
+ // child (no `name:` field), so the field default never fires.
3401
+ object_creation_expression: { callType: "call", nameExtractor: phpConstructorExtractor },
3402
+ base_clause: { callType: "inherit", nameExtractor: heritageExtractor },
3403
+ class_interface_clause: {
3404
+ callType: "implement",
3405
+ nameExtractor: heritageExtractor
3406
+ },
3407
+ // Verified AST: `namespace_use_declaration (namespace_use_clause
3408
+ // (qualified_name ...))` — not `use_declaration`.
3409
+ namespace_use_declaration: {
3410
+ callType: "import",
3411
+ nameExtractor: importFromText(["use "])
3412
+ }
3413
+ }
2347
3414
  },
2348
3415
  // ─── Scripting / mobile ────────────────────────────────────────────────────
2349
3416
  ruby: {
@@ -2361,7 +3428,13 @@ var init_queries = __esm({
2361
3428
  module: "name",
2362
3429
  constant: "name"
2363
3430
  },
2364
- scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"])
3431
+ scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"]),
3432
+ refRules: {
3433
+ // `call` covers both `foo(...)` and `obj.foo(...)` — the extractor
3434
+ // records the method leaf, plus `require`/`require_relative` imports.
3435
+ call: { callType: "call", nameExtractor: rubyCallExtractor },
3436
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor }
3437
+ }
2365
3438
  },
2366
3439
  swift: {
2367
3440
  declKinds: {
@@ -2394,7 +3467,18 @@ var init_queries = __esm({
2394
3467
  "protocol_declaration",
2395
3468
  "actor_declaration",
2396
3469
  "extension_declaration"
2397
- ])
3470
+ ]),
3471
+ refRules: {
3472
+ // Verified AST: `call_expression (simple_identifier) (call_suffix …)` —
3473
+ // the callee is a bare first child, no field name.
3474
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3475
+ // Verified AST: `inheritance_specifier inherits_from: (user_type …)`.
3476
+ inheritance_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3477
+ import_declaration: {
3478
+ callType: "import",
3479
+ nameExtractor: importFromText(["import ", "import type ", "@testable import "])
3480
+ }
3481
+ }
2398
3482
  },
2399
3483
  kotlin: {
2400
3484
  declKinds: {
@@ -2419,7 +3503,17 @@ var init_queries = __esm({
2419
3503
  "object_declaration",
2420
3504
  "interface_declaration",
2421
3505
  "function_declaration"
2422
- ])
3506
+ ]),
3507
+ refRules: {
3508
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3509
+ // Verified AST: `delegation_specifier (user_type (type_identifier))` —
3510
+ // the `: Handler` / `: Base()` clause.
3511
+ delegation_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3512
+ import_header: {
3513
+ callType: "import",
3514
+ nameExtractor: importFromText(["import "])
3515
+ }
3516
+ }
2423
3517
  },
2424
3518
  elixir: {
2425
3519
  declKinds: {
@@ -2491,7 +3585,38 @@ function visitTree(tree, content, file, lang, queries) {
2491
3585
  const boundedContent = content.length > TREE_SITTER_MAX_FILE_CHARS ? content.slice(0, TREE_SITTER_MAX_FILE_CHARS) : content;
2492
3586
  const nlOffsets = newlineOffsets3(boundedContent);
2493
3587
  const symbols = [];
3588
+ const refs = [];
3589
+ const seenRefs = /* @__PURE__ */ new Set();
2494
3590
  const scopeStack = [];
3591
+ function emitRefsForNode(node, rule) {
3592
+ const emissions = rule.nameExtractor?.(node) ?? defaultRefTarget(node, rule);
3593
+ if (!emissions) return;
3594
+ const { line } = lineColAt2(nlOffsets, node.startIndex);
3595
+ for (const emission of emissions) {
3596
+ if (!emission.toName) continue;
3597
+ const callType = emission.callType ?? rule.callType;
3598
+ const key = `${emission.toName}:${callType}:${line}:${emission.module ?? ""}:${node.startIndex}`;
3599
+ if (seenRefs.has(key)) continue;
3600
+ seenRefs.add(key);
3601
+ refs.push({
3602
+ fromId: 0,
3603
+ // assignRefsToSymbols attaches owners after insertion
3604
+ toName: emission.toName.slice(0, 200),
3605
+ callType,
3606
+ line,
3607
+ lang,
3608
+ module: emission.module
3609
+ });
3610
+ }
3611
+ }
3612
+ function defaultRefTarget(node, rule) {
3613
+ if (!rule.field) return null;
3614
+ const field = node.childForFieldName(rule.field);
3615
+ if (!field) return null;
3616
+ const leaf = field.text.split(/[.:\\]/).filter(Boolean).pop()?.split(/[<(]/)[0];
3617
+ if (!leaf) return null;
3618
+ return [{ toName: leaf.trim() }];
3619
+ }
2495
3620
  function visit(node, depth) {
2496
3621
  if (symbols.length >= TREE_SITTER_MAX_SYMBOLS) return;
2497
3622
  if (node.isMissing || node.isError) {
@@ -2510,6 +3635,8 @@ function visitTree(tree, content, file, lang, queries) {
2510
3635
  );
2511
3636
  if (emitted) symbols.push(emitted);
2512
3637
  }
3638
+ const refRule = queries.refRules?.[node.type];
3639
+ if (refRule) emitRefsForNode(node, refRule);
2513
3640
  }
2514
3641
  const pushesScope = queries.scopeNodes?.has(node.type) ?? false;
2515
3642
  const pushIdx = pushesScope ? pushScope(scopeStack, node, queries) : -1;
@@ -2527,7 +3654,7 @@ function visitTree(tree, content, file, lang, queries) {
2527
3654
  if (pushIdx !== -1) scopeStack.pop();
2528
3655
  }
2529
3656
  visit(tree.rootNode, 0);
2530
- return { symbols };
3657
+ return { symbols, refs };
2531
3658
  }
2532
3659
  function pushScope(scopeStack, node, queries) {
2533
3660
  const name = extractName(node, queries);
@@ -2614,7 +3741,7 @@ __export(tree_sitter_parser_exports, {
2614
3741
  parseSymbols: () => parseSymbols8,
2615
3742
  parseTreeSitterAst: () => parseTreeSitterAst
2616
3743
  });
2617
- import * as path10 from "node:path";
3744
+ import * as path11 from "node:path";
2618
3745
  import { fileURLToPath } from "node:url";
2619
3746
  function optInEnabled(env) {
2620
3747
  return process.env[env] === "1" || process.env[env] === "true";
@@ -2637,7 +3764,7 @@ async function loadLanguage(lang) {
2637
3764
  if (!grammarName) {
2638
3765
  throw new Error(`tree-sitter: no grammar registered for lang "${lang}"`);
2639
3766
  }
2640
- const wasmPath = path10.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
3767
+ const wasmPath = path11.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
2641
3768
  const { Language, init } = await getRuntime();
2642
3769
  await init();
2643
3770
  const languageObj = await Language.load(wasmPath);
@@ -2658,7 +3785,7 @@ function isTreeSitterSupported(lang) {
2658
3785
  function getGrammarWasmPath(lang) {
2659
3786
  const name = resolveGrammarName(lang);
2660
3787
  if (!name) return void 0;
2661
- return path10.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
3788
+ return path11.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
2662
3789
  }
2663
3790
  async function parseSymbols8(opts) {
2664
3791
  const { file, content, lang } = opts;
@@ -2674,10 +3801,10 @@ async function parseSymbols8(opts) {
2674
3801
  if (!tree) {
2675
3802
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2676
3803
  }
2677
- const { symbols } = visitTree(tree, content, file, lang, getQueries(lang));
3804
+ const { symbols, refs } = visitTree(tree, content, file, lang, getQueries(lang));
2678
3805
  parser.delete();
2679
3806
  tree.delete();
2680
- return { file, lang, symbols, refs: [], mtimeMs: Date.now() };
3807
+ return { file, lang, symbols, refs, mtimeMs: Date.now() };
2681
3808
  } catch {
2682
3809
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2683
3810
  }
@@ -2710,7 +3837,7 @@ async function parseTreeSitterAst(opts) {
2710
3837
  try {
2711
3838
  const { Parser, Language, init } = await getRuntime();
2712
3839
  await init();
2713
- const wasmPath = path10.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
3840
+ const wasmPath = path11.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
2714
3841
  const languageObj = await Language.load(wasmPath);
2715
3842
  const parser = new Parser();
2716
3843
  parser.setLanguage(languageObj);
@@ -2719,46 +3846,186 @@ async function parseTreeSitterAst(opts) {
2719
3846
  parser.delete();
2720
3847
  return null;
2721
3848
  }
2722
- return { tree, parser };
2723
- } catch {
2724
- return null;
3849
+ return { tree, parser };
3850
+ } catch {
3851
+ return null;
3852
+ }
3853
+ }
3854
+ var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
3855
+ var init_tree_sitter_parser = __esm({
3856
+ "src/codebase-index/tree-sitter-parser.ts"() {
3857
+ "use strict";
3858
+ init_queries();
3859
+ init_visitor();
3860
+ WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
3861
+ RUNTIME_WASM = path11.join(WASM_DIR, "tree-sitter-runtime.wasm");
3862
+ LANG_TO_GRAMMAR = {
3863
+ c: "c",
3864
+ cpp: "cpp",
3865
+ java: "java",
3866
+ csharp: "c_sharp",
3867
+ // tree-sitter directory uses underscore
3868
+ php: "php",
3869
+ ruby: "ruby",
3870
+ swift: "swift",
3871
+ kotlin: "kotlin",
3872
+ shell: "bash",
3873
+ // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
3874
+ // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
3875
+ elixir: "elixir"
3876
+ };
3877
+ GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
3878
+ PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
3879
+ RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
3880
+ runtimePromise = null;
3881
+ languageCache = /* @__PURE__ */ new Map();
3882
+ }
3883
+ });
3884
+
3885
+ // src/codebase-index/parser-dispatch.ts
3886
+ var parser_dispatch_exports = {};
3887
+ __export(parser_dispatch_exports, {
3888
+ parseFileContent: () => parseFileContent,
3889
+ parseFilesContent: () => parseFilesContent
3890
+ });
3891
+ async function parseFileContent(file, content, lang) {
3892
+ const parsed = await dispatch(file, content, lang);
3893
+ return withRelations(parsed, content, lang);
3894
+ }
3895
+ async function parseFilesContent(files) {
3896
+ if (files.length === 0) return [];
3897
+ const slots = files.map(() => ({ result: null }));
3898
+ const batchingEnabled = process.env["WRONGSTACK_TOOLCHAIN_BATCH"] !== "0";
3899
+ if (batchingEnabled) {
3900
+ const goFiles = [];
3901
+ const pyFiles = [];
3902
+ files.forEach((f, index) => {
3903
+ if (f.lang === "go") goFiles.push({ ...f, index });
3904
+ else if (f.lang === "py") pyFiles.push({ ...f, index });
3905
+ });
3906
+ if (goFiles.length > 0) {
3907
+ await applyBatchResults(slots, goFiles, (chunks) => runGoBatch(chunks), "go");
3908
+ }
3909
+ if (pyFiles.length > 0) {
3910
+ const pyBinary = await resolvePythonBinary();
3911
+ if (pyBinary) {
3912
+ await applyBatchResults(slots, pyFiles, (chunks) => runPyBatch(chunks, pyBinary), "py");
3913
+ }
3914
+ }
3915
+ }
3916
+ const jobs = [];
3917
+ for (let i = 0; i < files.length; i++) {
3918
+ if (slots[i].result !== null) continue;
3919
+ const { file, content, lang } = files[i];
3920
+ const slot = slots[i];
3921
+ jobs.push(
3922
+ (async () => {
3923
+ try {
3924
+ slot.result = await parseFileContent(file, content, lang);
3925
+ } catch (err) {
3926
+ slot.error = err instanceof Error ? err.message : String(err);
3927
+ }
3928
+ })()
3929
+ );
3930
+ }
3931
+ await Promise.all(jobs);
3932
+ return slots;
3933
+ }
3934
+ async function applyBatchResults(slots, batchFiles, runBatch, lang) {
3935
+ for (const chunk of chunkBatchFiles(batchFiles)) {
3936
+ let byFile = null;
3937
+ try {
3938
+ byFile = await runBatch(chunk);
3939
+ } catch {
3940
+ byFile = null;
3941
+ }
3942
+ if (!byFile) continue;
3943
+ for (const item of chunk) {
3944
+ const parsed = byFile.get(item.file);
3945
+ if (!parsed) continue;
3946
+ slots[item.index] = { result: withRelations(parsed, item.content, lang) };
3947
+ }
3948
+ }
3949
+ }
3950
+ async function dispatch(file, content, lang) {
3951
+ switch (lang) {
3952
+ case "ts":
3953
+ case "tsx":
3954
+ case "js":
3955
+ case "jsx": {
3956
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3957
+ return parseSymbols9({ file, content, lang });
3958
+ }
3959
+ case "go": {
3960
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3961
+ return parseSymbols9({ file, content, lang: "go" });
3962
+ }
3963
+ case "py": {
3964
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3965
+ return parseSymbols9({ file, content, lang: "py" });
3966
+ }
3967
+ case "rs": {
3968
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3969
+ return parseSymbols9({ file, content, lang: "rs" });
3970
+ }
3971
+ case "json": {
3972
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3973
+ return parseSymbols9({ file, content, lang: "json" });
3974
+ }
3975
+ case "yaml": {
3976
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3977
+ return parseSymbols9({ file, content, lang: "yaml" });
3978
+ }
3979
+ // Phase 1: ten languages now route through the Tree-Sitter WASM
3980
+ // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3981
+ // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3982
+ // or the parser returns zero symbols — preserving the indexable-file
3983
+ // contract that "missing a parser must never mean skipping the file".
3984
+ case "c":
3985
+ case "cpp":
3986
+ case "java":
3987
+ case "csharp":
3988
+ case "php":
3989
+ case "ruby":
3990
+ case "swift":
3991
+ case "kotlin":
3992
+ case "shell":
3993
+ case "elixir": {
3994
+ try {
3995
+ const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
3996
+ const parsed = await parseSymbols10({ file, content, lang });
3997
+ if (parsed.symbols.length > 0) return parsed;
3998
+ } catch {
3999
+ }
4000
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4001
+ return parseSymbols9({ file, content, lang });
4002
+ }
4003
+ default: {
4004
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4005
+ return parseSymbols9({ file, content, lang });
4006
+ }
2725
4007
  }
2726
4008
  }
2727
- var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
2728
- var init_tree_sitter_parser = __esm({
2729
- "src/codebase-index/tree-sitter-parser.ts"() {
4009
+ function withRelations(parsed, content, lang) {
4010
+ let refs = parsed.refs ?? [];
4011
+ if (refs.length === 0 && hasImportPatterns(lang)) {
4012
+ refs = extractImports({ content, lang });
4013
+ }
4014
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
4015
+ }
4016
+ var init_parser_dispatch = __esm({
4017
+ "src/codebase-index/parser-dispatch.ts"() {
2730
4018
  "use strict";
2731
- init_queries();
2732
- init_visitor();
2733
- WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
2734
- RUNTIME_WASM = path10.join(WASM_DIR, "tree-sitter-runtime.wasm");
2735
- LANG_TO_GRAMMAR = {
2736
- c: "c",
2737
- cpp: "cpp",
2738
- java: "java",
2739
- csharp: "c_sharp",
2740
- // tree-sitter directory uses underscore
2741
- php: "php",
2742
- ruby: "ruby",
2743
- swift: "swift",
2744
- kotlin: "kotlin",
2745
- shell: "bash",
2746
- // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
2747
- // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
2748
- elixir: "elixir"
2749
- };
2750
- GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
2751
- PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
2752
- RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
2753
- runtimePromise = null;
2754
- languageCache = /* @__PURE__ */ new Map();
4019
+ init_import_extractor();
4020
+ init_parser_batch();
4021
+ init_py_parser();
2755
4022
  }
2756
4023
  });
2757
4024
 
2758
4025
  // src/replace.ts
2759
- import { spawn as spawn4 } from "node:child_process";
2760
- import * as fs14 from "node:fs/promises";
2761
- import * as path15 from "node:path";
4026
+ import { spawn as spawn5 } from "node:child_process";
4027
+ import * as fs15 from "node:fs/promises";
4028
+ import * as path16 from "node:path";
2762
4029
  import { ToolValidationError } from "@wrongstack/core/types";
2763
4030
  import {
2764
4031
  atomicWrite,
@@ -2772,127 +4039,11 @@ import {
2772
4039
  } from "@wrongstack/core/utils";
2773
4040
 
2774
4041
  // src/_regex.ts
2775
- var MAX_PATTERN_LEN = 256;
2776
- var DANGEROUS_PATTERNS = [
2777
- // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
2778
- /(\([^)]*[+*][^)]*\))[+*]/,
2779
- /(\(\?:[^)]*[+*][^)]*\))[+*]/,
2780
- // Adjacent quantifiers: a++ a*+
2781
- /[+*]{2,}/,
2782
- // Quantifier on alternation with length 2+
2783
- /\([^|)]+\|[^)]+\)[+*][+*]/,
2784
- // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
2785
- /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
2786
- ];
2787
- function hasAmbiguousQuantifiedAlternation(pattern) {
2788
- for (let i = 0; i < pattern.length; i++) {
2789
- if (pattern[i] !== "(") continue;
2790
- if (i > 0 && pattern[i - 1] === "\\") continue;
2791
- let depth = 0;
2792
- let inClass = false;
2793
- let j = i;
2794
- for (; j < pattern.length; j++) {
2795
- const ch = pattern[j];
2796
- if (ch === "\\") {
2797
- j++;
2798
- continue;
2799
- }
2800
- if (inClass) {
2801
- if (ch === "]") inClass = false;
2802
- continue;
2803
- }
2804
- if (ch === "[") {
2805
- inClass = true;
2806
- continue;
2807
- }
2808
- if (ch === "(") depth++;
2809
- else if (ch === ")") {
2810
- depth--;
2811
- if (depth === 0) break;
2812
- }
2813
- }
2814
- if (j >= pattern.length) return false;
2815
- const next = pattern[j + 1];
2816
- if (next !== "+" && next !== "*" && next !== "{") continue;
2817
- let inner = pattern.slice(i + 1, j);
2818
- inner = inner.replace(/^\?(?::|<?[=!])/u, "");
2819
- const branches = [];
2820
- let current = "";
2821
- let d = 0;
2822
- let cls = false;
2823
- for (let k = 0; k < inner.length; k++) {
2824
- const ch = inner[k];
2825
- if (ch === "\\") {
2826
- current += ch + (inner[k + 1] ?? "");
2827
- k++;
2828
- continue;
2829
- }
2830
- if (cls) {
2831
- if (ch === "]") cls = false;
2832
- current += ch;
2833
- continue;
2834
- }
2835
- if (ch === "[") {
2836
- cls = true;
2837
- current += ch;
2838
- continue;
2839
- }
2840
- if (ch === "(") d++;
2841
- if (ch === ")") d--;
2842
- if (ch === "|" && d === 0) {
2843
- branches.push(current);
2844
- current = "";
2845
- continue;
2846
- }
2847
- current += ch;
2848
- }
2849
- branches.push(current);
2850
- if (branches.length < 2) continue;
2851
- for (let a = 0; a < branches.length; a++) {
2852
- for (let b = a + 1; b < branches.length; b++) {
2853
- const x = branches[a];
2854
- const y = branches[b];
2855
- if (x === "" || y === "") return true;
2856
- if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
2857
- }
2858
- }
2859
- }
2860
- return false;
2861
- }
2862
- function compileUserRegex(pattern, flags) {
2863
- if (typeof pattern !== "string") {
2864
- return { ok: false, reason: "pattern must be a string" };
2865
- }
2866
- if (pattern.length === 0) {
2867
- return { ok: false, reason: "pattern is empty" };
2868
- }
2869
- if (pattern.length > MAX_PATTERN_LEN) {
2870
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
2871
- }
2872
- for (const rx of DANGEROUS_PATTERNS) {
2873
- if (rx.test(pattern)) {
2874
- return {
2875
- ok: false,
2876
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
2877
- };
2878
- }
2879
- }
2880
- if (hasAmbiguousQuantifiedAlternation(pattern)) {
2881
- return {
2882
- ok: false,
2883
- reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
2884
- };
2885
- }
2886
- try {
2887
- return { ok: true, regex: new RegExp(pattern, flags) };
2888
- } catch (err) {
2889
- return {
2890
- ok: false,
2891
- reason: err instanceof Error ? err.message : "invalid regex"
2892
- };
2893
- }
2894
- }
2895
- var MAX_SUBJECT_LEN = 64 * 1024;
4042
+ import {
4043
+ capSubject,
4044
+ compileUserRegex,
4045
+ MAX_SUBJECT_LEN
4046
+ } from "@wrongstack/primitives";
2896
4047
 
2897
4048
  // src/_util.ts
2898
4049
  import { createHash } from "node:crypto";
@@ -2992,7 +4143,7 @@ function takeHeadBytes(s, maxBytes) {
2992
4143
  }
2993
4144
 
2994
4145
  // src/codebase-index/background-indexer.ts
2995
- import * as fs13 from "node:fs";
4146
+ import * as fs14 from "node:fs";
2996
4147
  import { fileURLToPath as fileURLToPath6 } from "node:url";
2997
4148
  import { Worker as Worker2 } from "node:worker_threads";
2998
4149
 
@@ -3080,9 +4231,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
3080
4231
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3081
4232
  import { execFile } from "node:child_process";
3082
4233
  import { createHash as createHash2 } from "node:crypto";
3083
- import * as fs9 from "node:fs/promises";
4234
+ import * as fs10 from "node:fs/promises";
3084
4235
  import { availableParallelism } from "node:os";
3085
- import * as path13 from "node:path";
4236
+ import * as path14 from "node:path";
3086
4237
  import {
3087
4238
  DEFAULT_WALK_IGNORE_DIRS,
3088
4239
  indexParallelBatchSize,
@@ -3752,281 +4903,94 @@ var ModuleResolver = class {
3752
4903
  const head = segments[0];
3753
4904
  if (head === "self" || head === "super") {
3754
4905
  let base = path5.posix.dirname(fromFile);
3755
- for (const segment of segments) {
3756
- if (segment === "super") base = path5.posix.dirname(base);
3757
- else if (segment !== "self") break;
3758
- }
3759
- const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
3760
- return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
3761
- }
3762
- const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
3763
- const crate = head === "crate" ? owningCrate : this.structure.roots.find(
3764
- (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
3765
- );
3766
- if (!crate) {
3767
- return this.lookupWithExtensions(
3768
- path5.posix.join(path5.posix.dirname(fromFile), ...segments),
3769
- "rs"
3770
- );
3771
- }
3772
- const rest = segments.slice(1);
3773
- for (const base of crate.sourceRoots) {
3774
- const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
3775
- const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
3776
- const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
3777
- if (hit) return hit;
3778
- }
3779
- return void 0;
3780
- }
3781
- /** `com.example.Thing` and `com.example.*` against JVM source roots. */
3782
- resolveJvm(spec) {
3783
- const segments = spec.split(".").filter(Boolean);
3784
- if (segments.length === 0) return void 0;
3785
- const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
3786
- const wildcard = segments[segments.length - 1] === "*";
3787
- const parts = wildcard ? segments.slice(0, -1) : segments;
3788
- for (const base of [...sourceRoots, this.structure.projectRoot]) {
3789
- const target = path5.posix.join(base, ...parts);
3790
- const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
3791
- if (hit) return hit;
3792
- }
3793
- return void 0;
3794
- }
3795
- /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
3796
- resolveInclude(fromFile, spec) {
3797
- const relative4 = this.lookupWithExtensions(
3798
- path5.posix.join(path5.posix.dirname(fromFile), spec),
3799
- "c"
3800
- );
3801
- if (relative4) return relative4;
3802
- for (const base of [
3803
- path5.posix.join(this.structure.projectRoot, "include"),
3804
- this.structure.projectRoot
3805
- ]) {
3806
- const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
3807
- if (hit) return hit;
3808
- }
3809
- return void 0;
3810
- }
3811
- /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
3812
- resolveRuby(fromFile, spec) {
3813
- const relative4 = this.lookupWithExtensions(
3814
- path5.posix.join(path5.posix.dirname(fromFile), spec),
3815
- "ruby"
3816
- );
3817
- if (relative4) return relative4;
3818
- for (const base of [
3819
- path5.posix.join(this.structure.projectRoot, "lib"),
3820
- this.structure.projectRoot
3821
- ]) {
3822
- const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
3823
- if (hit) return hit;
3824
- }
3825
- return void 0;
3826
- }
3827
- };
3828
-
3829
- // src/codebase-index/import-extractor.ts
3830
- var IMPORT_MAX_FILE_CHARS = 512 * 1024;
3831
- var IMPORT_MAX_PER_FILE = 400;
3832
- var DOTTED_IMPORT = [
3833
- { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
3834
- ];
3835
- var LANG_IMPORTS = {
3836
- // Go and Python have real AST extractors; these patterns are the fallback for
3837
- // machines with no Go toolchain or Python interpreter installed, where the
3838
- // parser degrades to regex symbols and would otherwise contribute no edges.
3839
- go: [
3840
- { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
3841
- // Grouped form: inside `import ( … )` each line is an optional alias plus a
3842
- // quoted path. A stray match elsewhere resolves to no file and is dropped.
3843
- { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
3844
- ],
3845
- py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
3846
- rs: [
3847
- // use a::b::C; | use a::b::{C, D}; → the path before any brace
3848
- { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
3849
- // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
3850
- { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
3851
- ],
3852
- java: DOTTED_IMPORT,
3853
- kotlin: DOTTED_IMPORT,
3854
- scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
3855
- csharp: [
3856
- // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
3857
- { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
3858
- { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
3859
- ],
3860
- // Quoted includes only: <stdio.h> is a system header with no indexed file.
3861
- c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3862
- cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3863
- ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
3864
- php: [
3865
- // `use A\B\C` imports the class C, which is what the index has a symbol
3866
- // for — the namespace symbol only covers the `A\B` prefix.
3867
- { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
3868
- { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
3869
- ],
3870
- swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
3871
- dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
3872
- lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
3873
- elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
3874
- haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
3875
- zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
3876
- proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
3877
- // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
3878
- css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
3879
- // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
3880
- vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3881
- svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3882
- html: [
3883
- { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
3884
- { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
3885
- ],
3886
- shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
3887
- r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
3888
- };
3889
- function lastSegment(specifier) {
3890
- const pathLike = /[/\\]|::/.test(specifier);
3891
- const segments = specifier.split(/[/\\]|::/).filter(Boolean);
3892
- let last = segments[segments.length - 1] ?? specifier;
3893
- if (last === "*" || last === "_") {
3894
- last = segments[segments.length - 2] ?? specifier;
3895
- }
3896
- if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
3897
- const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
3898
- return dotted[dotted.length - 1] ?? last;
3899
- }
3900
- function newlineOffsets(content) {
3901
- const offsets = [];
3902
- for (let i = 0; i < content.length; i++) {
3903
- if (content.charCodeAt(i) === 10) offsets.push(i);
3904
- }
3905
- return offsets;
3906
- }
3907
- function lineAt(offsets, index) {
3908
- let low = 0;
3909
- let high = offsets.length;
3910
- while (low < high) {
3911
- const mid = low + high >>> 1;
3912
- if ((offsets[mid] ?? 0) < index) low = mid + 1;
3913
- else high = mid;
3914
- }
3915
- return low + 1;
3916
- }
3917
- function hasImportPatterns(lang) {
3918
- return LANG_IMPORTS[lang] !== void 0;
3919
- }
3920
- function extractImports(opts) {
3921
- const patterns = LANG_IMPORTS[opts.lang];
3922
- if (!patterns || !opts.content) return [];
3923
- const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
3924
- const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
3925
- const refs = [];
3926
- const seen = /* @__PURE__ */ new Set();
3927
- const offsets = newlineOffsets(content);
3928
- for (const pattern of patterns) {
3929
- const re = new RegExp(pattern.re.source, pattern.re.flags);
3930
- for (const match of content.matchAll(re)) {
3931
- if (refs.length >= limit) return refs;
3932
- const specifier = match[1]?.trim();
3933
- if (!specifier) continue;
3934
- const module = specifier;
3935
- const toName = pattern.name === "full" ? module : lastSegment(module);
3936
- if (!toName) continue;
3937
- const key = `${module}\0${toName}`;
3938
- if (seen.has(key)) continue;
3939
- seen.add(key);
3940
- refs.push({
3941
- fromId: 0,
3942
- toName,
3943
- callType: "import",
3944
- line: lineAt(offsets, match.index ?? 0),
3945
- lang: opts.lang,
3946
- module
3947
- });
3948
- }
3949
- }
3950
- return refs;
3951
- }
3952
-
3953
- // src/codebase-index/parser-dispatch.ts
3954
- async function parseFileContent(file, content, lang) {
3955
- const parsed = await dispatch(file, content, lang);
3956
- return withRelations(parsed, content, lang);
3957
- }
3958
- async function dispatch(file, content, lang) {
3959
- switch (lang) {
3960
- case "ts":
3961
- case "tsx":
3962
- case "js":
3963
- case "jsx": {
3964
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3965
- return parseSymbols9({ file, content, lang });
3966
- }
3967
- case "go": {
3968
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3969
- return parseSymbols9({ file, content, lang: "go" });
3970
- }
3971
- case "py": {
3972
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3973
- return parseSymbols9({ file, content, lang: "py" });
3974
- }
3975
- case "rs": {
3976
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3977
- return parseSymbols9({ file, content, lang: "rs" });
4906
+ for (const segment of segments) {
4907
+ if (segment === "super") base = path5.posix.dirname(base);
4908
+ else if (segment !== "self") break;
4909
+ }
4910
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
4911
+ return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
3978
4912
  }
3979
- case "json": {
3980
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3981
- return parseSymbols9({ file, content, lang: "json" });
4913
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
4914
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
4915
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
4916
+ );
4917
+ if (!crate) {
4918
+ return this.lookupWithExtensions(
4919
+ path5.posix.join(path5.posix.dirname(fromFile), ...segments),
4920
+ "rs"
4921
+ );
3982
4922
  }
3983
- case "yaml": {
3984
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3985
- return parseSymbols9({ file, content, lang: "yaml" });
4923
+ const rest = segments.slice(1);
4924
+ for (const base of crate.sourceRoots) {
4925
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
4926
+ const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
4927
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
4928
+ if (hit) return hit;
3986
4929
  }
3987
- // Phase 1: ten languages now route through the Tree-Sitter WASM
3988
- // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3989
- // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3990
- // or the parser returns zero symbols — preserving the indexable-file
3991
- // contract that "missing a parser must never mean skipping the file".
3992
- case "c":
3993
- case "cpp":
3994
- case "java":
3995
- case "csharp":
3996
- case "php":
3997
- case "ruby":
3998
- case "swift":
3999
- case "kotlin":
4000
- case "shell":
4001
- case "elixir": {
4002
- try {
4003
- const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
4004
- const parsed = await parseSymbols10({ file, content, lang });
4005
- if (parsed.symbols.length > 0) return parsed;
4006
- } catch {
4007
- }
4008
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4009
- return parseSymbols9({ file, content, lang });
4930
+ return void 0;
4931
+ }
4932
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
4933
+ resolveJvm(spec) {
4934
+ const segments = spec.split(".").filter(Boolean);
4935
+ if (segments.length === 0) return void 0;
4936
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
4937
+ const wildcard = segments[segments.length - 1] === "*";
4938
+ const parts = wildcard ? segments.slice(0, -1) : segments;
4939
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
4940
+ const target = path5.posix.join(base, ...parts);
4941
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
4942
+ if (hit) return hit;
4010
4943
  }
4011
- default: {
4012
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4013
- return parseSymbols9({ file, content, lang });
4944
+ return void 0;
4945
+ }
4946
+ /** `#include "foo/bar.h"` quoted form only; `<…>` is a system header. */
4947
+ resolveInclude(fromFile, spec) {
4948
+ const relative4 = this.lookupWithExtensions(
4949
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
4950
+ "c"
4951
+ );
4952
+ if (relative4) return relative4;
4953
+ for (const base of [
4954
+ path5.posix.join(this.structure.projectRoot, "include"),
4955
+ this.structure.projectRoot
4956
+ ]) {
4957
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
4958
+ if (hit) return hit;
4014
4959
  }
4960
+ return void 0;
4015
4961
  }
4016
- }
4017
- function withRelations(parsed, content, lang) {
4018
- let refs = parsed.refs ?? [];
4019
- if (refs.length === 0 && hasImportPatterns(lang)) {
4020
- refs = extractImports({ content, lang });
4962
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
4963
+ resolveRuby(fromFile, spec) {
4964
+ const relative4 = this.lookupWithExtensions(
4965
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
4966
+ "ruby"
4967
+ );
4968
+ if (relative4) return relative4;
4969
+ for (const base of [
4970
+ path5.posix.join(this.structure.projectRoot, "lib"),
4971
+ this.structure.projectRoot
4972
+ ]) {
4973
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
4974
+ if (hit) return hit;
4975
+ }
4976
+ return void 0;
4021
4977
  }
4022
- return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
4023
- }
4978
+ };
4979
+
4980
+ // src/codebase-index/indexer.ts
4981
+ init_parser_dispatch();
4024
4982
 
4025
4983
  // src/codebase-index/parser-worker-pool.ts
4984
+ import * as fs7 from "node:fs";
4985
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
4026
4986
  import { Worker } from "node:worker_threads";
4027
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4028
- import * as fs6 from "node:fs";
4029
4987
  var WORKER_POOL_THRESHOLD = 500;
4988
+ function resolveWorkerPoolThreshold() {
4989
+ const raw = process.env["WRONGSTACK_INDEX_WORKER_THRESHOLD"];
4990
+ if (raw === void 0) return WORKER_POOL_THRESHOLD;
4991
+ if (!/^\d+$/.test(raw)) return WORKER_POOL_THRESHOLD;
4992
+ return Number.parseInt(raw, 10);
4993
+ }
4030
4994
  var ParserWorkerPool = class {
4031
4995
  constructor(maxWorkers = defaultWorkerCount()) {
4032
4996
  this.maxWorkers = maxWorkers;
@@ -4070,7 +5034,8 @@ var ParserWorkerPool = class {
4070
5034
  w.unref();
4071
5035
  w.on("message", (msg) => this.handleMessage(msg));
4072
5036
  w.on("error", (err) => this.handleError(err, w));
4073
- this.workers.push({ worker: w, busy: false });
5037
+ w.on("exit", () => this.retireByReference(w));
5038
+ this.workers.push({ worker: w, workerId: w.threadId, busy: false });
4074
5039
  } catch {
4075
5040
  if (this.workers.length === 0) {
4076
5041
  this.unavailable = true;
@@ -4086,7 +5051,7 @@ var ParserWorkerPool = class {
4086
5051
  }
4087
5052
  /**
4088
5053
  * Parse files in parallel across the worker pool. Returns a flat
4089
- * `FileSymbols[]` in completion order (caller sorts if needed).
5054
+ * `FileSymbols[]` in completion order (caller matches by file path).
4090
5055
  *
4091
5056
  * Content is pre-read by the main thread (for the content-hash check)
4092
5057
  * and passed to workers to avoid a second disk read. Files are
@@ -4107,21 +5072,19 @@ var ParserWorkerPool = class {
4107
5072
  chunks[i % workerCount].push(files[i]);
4108
5073
  }
4109
5074
  return new Promise((resolve4, reject) => {
5075
+ const pendingChunks = /* @__PURE__ */ new Map();
4110
5076
  this.pending.set(batchId, {
4111
5077
  resolve: resolve4,
4112
5078
  reject,
4113
5079
  accumulated: [],
4114
- expectedWorkers: workerCount,
4115
- completedWorkers: 0
5080
+ pendingChunks,
5081
+ settled: false
4116
5082
  });
4117
5083
  for (let i = 0; i < workerCount; i++) {
4118
5084
  const pw = this.workers[i];
4119
5085
  pw.busy = true;
4120
- pw.worker.postMessage({
4121
- type: "parse",
4122
- id: batchId,
4123
- files: chunks[i]
4124
- });
5086
+ pendingChunks.set(pw.workerId, chunks[i]);
5087
+ pw.worker.postMessage({ type: "parse", id: batchId, files: chunks[i] });
4125
5088
  }
4126
5089
  });
4127
5090
  }
@@ -4130,6 +5093,12 @@ var ParserWorkerPool = class {
4130
5093
  const workers = this.workers.map((w) => w.worker);
4131
5094
  this.workers = [];
4132
5095
  this.unavailable = false;
5096
+ for (const [, p] of this.pending) {
5097
+ if (p.settled) continue;
5098
+ p.settled = true;
5099
+ p.reject(new Error("ParserWorkerPool shut down"));
5100
+ }
5101
+ this.pending.clear();
4133
5102
  for (const w of workers) {
4134
5103
  try {
4135
5104
  w.postMessage({ type: "shutdown" });
@@ -4150,39 +5119,117 @@ var ParserWorkerPool = class {
4150
5119
  })
4151
5120
  )
4152
5121
  );
4153
- for (const [, p] of this.pending) p.reject(new Error("ParserWorkerPool shut down"));
4154
- this.pending.clear();
4155
5122
  }
4156
5123
  handleMessage(msg) {
4157
5124
  const batch = this.pending.get(msg.id);
4158
5125
  if (!batch) return;
5126
+ const worker2 = this.workers.find((w) => w.workerId === msg.workerId);
5127
+ if (worker2) worker2.busy = false;
5128
+ batch.pendingChunks.delete(msg.workerId);
4159
5129
  batch.accumulated.push(...msg.results);
4160
- batch.completedWorkers++;
4161
- const freeWorker = this.workers.find((w) => w.busy);
4162
- if (freeWorker) freeWorker.busy = false;
4163
- if (batch.completedWorkers >= batch.expectedWorkers) {
5130
+ if (batch.pendingChunks.size === 0) {
5131
+ if (batch.settled) return;
5132
+ batch.settled = true;
4164
5133
  this.pending.delete(msg.id);
4165
5134
  batch.resolve(batch.accumulated);
4166
5135
  }
4167
5136
  }
4168
- handleError(err, source) {
4169
- this.workers = this.workers.filter((w) => w.worker !== source);
4170
- if (this.workers.length === 0) {
4171
- for (const [, p] of this.pending) p.reject(err);
4172
- this.pending.clear();
4173
- this.unavailable = true;
5137
+ /**
5138
+ * Remove a worker from the pool and salvage any chunk it still owed.
5139
+ *
5140
+ * Idempotent by workerId `error` and `exit` can both fire for one
5141
+ * death, and a worker may die while no batch references it. When the
5142
+ * dead worker owed files to an in-flight batch and other workers remain,
5143
+ * those files are re-parsed inline on this thread (one fewer worker
5144
+ * should cost latency, not correctness). When it was the last worker,
5145
+ * every remaining batch rejects so the indexer's existing inline
5146
+ * fallback takes over the whole batch.
5147
+ */
5148
+ retireWorker(workerId) {
5149
+ const entry = this.workers.find((w) => w.workerId === workerId);
5150
+ if (!entry) return;
5151
+ this.workers = this.workers.filter((w) => w.workerId !== workerId);
5152
+ for (const [batchId, batch] of [...this.pending]) {
5153
+ const orphaned = batch.pendingChunks.get(workerId);
5154
+ if (!orphaned) continue;
5155
+ if (this.workers.length === 0) {
5156
+ this.pending.delete(batchId);
5157
+ this.unavailable = true;
5158
+ if (!batch.settled) {
5159
+ batch.settled = true;
5160
+ batch.reject(new Error("ParserWorkerPool: all workers died mid-batch"));
5161
+ }
5162
+ continue;
5163
+ }
5164
+ void this.reparseInline(batch, orphaned, workerId);
5165
+ }
5166
+ }
5167
+ /**
5168
+ * Salvage path: re-parse an orphaned chunk on this thread. Files that
5169
+ * fail here stay absent from the results — same contract as a per-file
5170
+ * error inside a live worker (see handleMessage).
5171
+ */
5172
+ async reparseInline(batch, orphaned, workerId) {
5173
+ try {
5174
+ const { parseFileContent: parseFileContent2 } = await Promise.resolve().then(() => (init_parser_dispatch(), parser_dispatch_exports));
5175
+ for (const item of orphaned) {
5176
+ if (batch.settled) return;
5177
+ try {
5178
+ const parsed = await parseFileContent2(item.file, item.content, item.lang);
5179
+ batch.accumulated.push(parsed);
5180
+ } catch {
5181
+ }
5182
+ await new Promise((resolve4) => setImmediate(resolve4));
5183
+ }
5184
+ } finally {
5185
+ this.finishSalvage(batch, workerId);
5186
+ }
5187
+ }
5188
+ /**
5189
+ * Terminal tail of a salvage — runs on every exit path. Kept free of
5190
+ * control flow inside a `finally` (noUnsafeFinally): releases the
5191
+ * pending-marker and resolves the batch if this was its last chunk.
5192
+ */
5193
+ finishSalvage(batch, workerId) {
5194
+ batch.pendingChunks.delete(workerId);
5195
+ if (batch.pendingChunks.size === 0) {
5196
+ for (const [batchId, tracked] of this.pending) {
5197
+ if (tracked === batch) {
5198
+ if (batch.settled) return;
5199
+ batch.settled = true;
5200
+ this.pending.delete(batchId);
5201
+ batch.resolve(batch.accumulated);
5202
+ return;
5203
+ }
5204
+ }
4174
5205
  }
4175
5206
  }
5207
+ /**
5208
+ * Retire by worker object rather than threadId. `threadId` is -1 before
5209
+ * the worker emits `online`, so a death during script load would make a
5210
+ * threadId-keyed lookup silently no-op and leak the entry (with its
5211
+ * pending chunk) — reference identity is correct in every case.
5212
+ */
5213
+ retireByReference(source) {
5214
+ const entry = this.workers.find((w) => w.worker === source);
5215
+ if (entry) this.retireWorker(entry.workerId);
5216
+ }
5217
+ handleError(err, source) {
5218
+ void err;
5219
+ this.retireByReference(source);
5220
+ }
4176
5221
  };
4177
5222
  function defaultWorkerCount() {
4178
5223
  const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
4179
5224
  return Math.max(1, Math.min(4, cores - 1));
4180
5225
  }
4181
5226
  function resolveWorkerScriptUrl() {
5227
+ const override = process.env["WRONGSTACK_PARSER_WORKER_SCRIPT"];
5228
+ if (override) return pathToFileURL(override);
4182
5229
  for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
4183
5230
  try {
4184
5231
  const url = new URL(rel, import.meta.url);
4185
- if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
5232
+ if (url.protocol === "file:" && fs7.existsSync(fileURLToPath2(url))) return url;
4186
5233
  } catch {
4187
5234
  }
4188
5235
  }
@@ -4195,8 +5242,8 @@ function getParserPool() {
4195
5242
  }
4196
5243
 
4197
5244
  // src/codebase-index/writer.ts
4198
- import * as fs8 from "node:fs";
4199
- import * as path12 from "node:path";
5245
+ import * as fs9 from "node:fs";
5246
+ import * as path13 from "node:path";
4200
5247
 
4201
5248
  // src/codebase-index/bm25.ts
4202
5249
  var K1 = 1.5;
@@ -4291,11 +5338,11 @@ var Bm25Index = class {
4291
5338
  init_languages();
4292
5339
 
4293
5340
  // src/codebase-index/schema.ts
4294
- var SCHEMA_VERSION = 4;
5341
+ var SCHEMA_VERSION = 5;
4295
5342
 
4296
5343
  // src/codebase-index/sqlite-runtime.ts
4297
- import { createRequire } from "node:module";
4298
5344
  import { toErrorMessage } from "@wrongstack/core/utils";
5345
+ import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
4299
5346
  var warningSilenced = false;
4300
5347
  function silenceSqliteExperimentalWarning() {
4301
5348
  if (warningSilenced) return;
@@ -4313,11 +5360,10 @@ function loadDatabaseSync() {
4313
5360
  if (DatabaseSyncCtor) return DatabaseSyncCtor;
4314
5361
  silenceSqliteExperimentalWarning();
4315
5362
  try {
4316
- const req = createRequire(import.meta.url);
4317
- DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
5363
+ DatabaseSyncCtor = loadRuntimeDatabaseSync();
4318
5364
  } catch (err) {
4319
5365
  throw new Error(
4320
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
5366
+ `The codebase index needs node:sqlite (Node >= 22.5) or bun:sqlite. This runtime doesn't provide it: ${toErrorMessage(err)}`
4321
5367
  );
4322
5368
  }
4323
5369
  return DatabaseSyncCtor;
@@ -4360,9 +5406,87 @@ function runSqliteWithRetry(fn) {
4360
5406
  throw lastError;
4361
5407
  }
4362
5408
 
5409
+ // src/codebase-index/vector-search.ts
5410
+ var RRF_K = 60;
5411
+ function vectorEmbeddingEnabled() {
5412
+ return process.env["WRONGSTACK_INDEX_VECTORS"] === "1";
5413
+ }
5414
+ var VECTOR_DIMENSIONS = 384;
5415
+ var NGRAM_SIZE = 3;
5416
+ function embedText(text) {
5417
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
5418
+ const normalized = text.toLowerCase().trim();
5419
+ if (normalized.length < NGRAM_SIZE) {
5420
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5421
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5422
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
5423
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5424
+ vec[bucket] += 1;
5425
+ }
5426
+ } else {
5427
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5428
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
5429
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5430
+ vec[bucket] += 1;
5431
+ }
5432
+ }
5433
+ let norm = 0;
5434
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5435
+ norm += vec[i] * vec[i];
5436
+ }
5437
+ norm = Math.sqrt(norm);
5438
+ if (norm > 0) {
5439
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5440
+ vec[i] /= norm;
5441
+ }
5442
+ }
5443
+ return vec;
5444
+ }
5445
+ function hashNgram(str) {
5446
+ let hash = 2166136261;
5447
+ for (let i = 0; i < str.length; i++) {
5448
+ hash ^= str.charCodeAt(i);
5449
+ hash = Math.imul(hash, 16777619);
5450
+ }
5451
+ return hash >>> 0;
5452
+ }
5453
+ function cosineSimilarity(a, b) {
5454
+ let dot = 0;
5455
+ const len = Math.min(a.length, b.length);
5456
+ for (let i = 0; i < len; i++) {
5457
+ dot += a[i] * b[i];
5458
+ }
5459
+ return dot;
5460
+ }
5461
+ function encodeVector(vec) {
5462
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5463
+ }
5464
+ function decodeVector(buf) {
5465
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5466
+ const copy = new Float32Array(buf.byteLength / 4);
5467
+ for (let i = 0; i < copy.length; i++) {
5468
+ copy[i] = view.getFloat32(i * 4, true);
5469
+ }
5470
+ return copy;
5471
+ }
5472
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5473
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5474
+ const scored = [];
5475
+ for (const id of allIds) {
5476
+ const bm25Rank = bm25Ranks.get(id);
5477
+ const vecRank = vectorRanks.get(id);
5478
+ let score = 0;
5479
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5480
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5481
+ scored.push([id, score]);
5482
+ }
5483
+ scored.sort((a, b) => b[1] - a[1]);
5484
+ return scored;
5485
+ }
5486
+
4363
5487
  // src/codebase-index/writer-admin.ts
4364
- import * as fs7 from "node:fs";
4365
- import * as path11 from "node:path";
5488
+ import * as fs8 from "node:fs";
5489
+ import * as path12 from "node:path";
4366
5490
  var DB_FILE = "index.db";
4367
5491
  function getAllIndexableWithStatement(stmt) {
4368
5492
  return stmt("SELECT id, text FROM symbols").all().map(
@@ -4398,6 +5522,14 @@ function getMetadataWithStatement(stmt, key) {
4398
5522
  const rows = stmt("SELECT value FROM metadata WHERE key = ?").all(key);
4399
5523
  return rows[0]?.value;
4400
5524
  }
5525
+ function getIndexSummaryWithStatement(stmt) {
5526
+ const fileRows = stmt("SELECT COUNT(*) AS n FROM files").all();
5527
+ const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
5528
+ return {
5529
+ totalFiles: fileRows[0] ? Number(fileRows[0].n) : 0,
5530
+ lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null
5531
+ };
5532
+ }
4401
5533
  function getFileMetaWithStatement(stmt, file) {
4402
5534
  const rows = stmt(
4403
5535
  "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
@@ -4427,22 +5559,102 @@ function getAllFileMetasWithStatement(stmt) {
4427
5559
  }
4428
5560
  function getIndexDbSizeBytes(indexDir) {
4429
5561
  try {
4430
- return fs7.statSync(path11.join(indexDir, DB_FILE)).size;
5562
+ return fs8.statSync(path12.join(indexDir, DB_FILE)).size;
4431
5563
  } catch {
4432
5564
  return 0;
4433
5565
  }
4434
5566
  }
4435
5567
 
5568
+ // src/codebase-index/writer-helpers.ts
5569
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
5570
+ function escapeLike(value) {
5571
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
5572
+ }
5573
+ function ladderChunkSizes(total, max) {
5574
+ if (total <= 0) return [];
5575
+ const sizes = [];
5576
+ let remaining = total;
5577
+ while (remaining > 0) {
5578
+ const cap = Math.min(remaining, max);
5579
+ const pow = cap >= 1 ? 2 ** Math.floor(Math.log2(cap)) : 1;
5580
+ const take = Math.max(1, Math.min(pow, remaining));
5581
+ sizes.push(take);
5582
+ remaining -= take;
5583
+ }
5584
+ return sizes;
5585
+ }
5586
+ function nextPow2(count) {
5587
+ return count <= 1 ? 1 : 2 ** Math.ceil(Math.log2(count));
5588
+ }
5589
+ function padToInBucket(values) {
5590
+ if (values.length <= 1) return values.slice();
5591
+ const target = nextPow2(values.length);
5592
+ const padded = values.slice();
5593
+ while (padded.length < target) padded.push(padded[0]);
5594
+ return padded;
5595
+ }
5596
+ function placeholders(count) {
5597
+ return Array.from({ length: count }, () => "?").join(",");
5598
+ }
5599
+ function inListChunks(total, max) {
5600
+ if (total <= 0) return [];
5601
+ if (nextPow2(total) <= max) return [total];
5602
+ const powMax = Math.max(1, 2 ** Math.floor(Math.log2(max)));
5603
+ return ladderChunkSizes(total, powMax);
5604
+ }
5605
+ function posixIndexPath(file) {
5606
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
5607
+ }
5608
+ function indexedFileMatchSql(column = "file") {
5609
+ return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
5610
+ }
5611
+ function indexedFileMatchArgs(file) {
5612
+ const posix4 = posixIndexPath(file.trim());
5613
+ return [file, posix4, `%/${escapeLike(posix4)}`];
5614
+ }
5615
+ function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
5616
+ if (packageLabel === filter) return true;
5617
+ const posixFile = posixIndexPath(storedFile);
5618
+ const posixFilter = posixIndexPath(filter.trim());
5619
+ if (!posixFilter) return false;
5620
+ return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
5621
+ }
5622
+ function assignRefsToSymbols(refs, symbols) {
5623
+ if (refs.length === 0 || symbols.length === 0) return [];
5624
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
5625
+ const seen = /* @__PURE__ */ new Set();
5626
+ const assigned = [];
5627
+ for (const ref of refs) {
5628
+ let owner;
5629
+ for (const symbol of ordered) {
5630
+ if (symbol.line > ref.line) break;
5631
+ owner = symbol;
5632
+ }
5633
+ if (!owner && ref.callType === "import") owner = ordered[0];
5634
+ if (!owner || owner.id <= 0) continue;
5635
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5636
+ if (seen.has(key)) continue;
5637
+ seen.add(key);
5638
+ assigned.push({ ...ref, fromId: owner.id });
5639
+ }
5640
+ return assigned;
5641
+ }
5642
+ function resolveIndexDir(projectRoot, override) {
5643
+ return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
5644
+ }
5645
+
4436
5646
  // src/codebase-index/writer-bulk-insert.ts
4437
5647
  function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4438
5648
  if (rows.length === 0) return;
4439
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 12));
4440
- for (let i = 0; i < rows.length; i += chunkSize) {
4441
- const chunk = rows.slice(i, i + chunkSize);
4442
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5649
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 12)));
5650
+ let cursor = 0;
5651
+ for (const take of ladder) {
5652
+ const chunk = rows.slice(cursor, cursor + take);
5653
+ cursor += take;
5654
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4443
5655
  const insert = stmt(
4444
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
4445
- VALUES ${placeholders}`
5656
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text)
5657
+ VALUES ${placeholders2}`
4446
5658
  );
4447
5659
  const binds = [];
4448
5660
  for (const r of chunk) {
@@ -4457,8 +5669,7 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4457
5669
  r.signature,
4458
5670
  r.docComment,
4459
5671
  r.scope,
4460
- r.text,
4461
- r.file
5672
+ r.text
4462
5673
  );
4463
5674
  }
4464
5675
  insert.run(...binds);
@@ -4466,11 +5677,13 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4466
5677
  }
4467
5678
  function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4468
5679
  if (!ftsAvailable || rows.length === 0) return;
4469
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4470
- for (let i = 0; i < rows.length; i += chunkSize) {
4471
- const chunk = rows.slice(i, i + chunkSize);
4472
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4473
- const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
5680
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5681
+ let cursor = 0;
5682
+ for (const take of ladder) {
5683
+ const chunk = rows.slice(cursor, cursor + take);
5684
+ cursor += take;
5685
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5686
+ const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders2}`);
4474
5687
  const binds = [];
4475
5688
  for (const r of chunk) binds.push(r.id, r.text);
4476
5689
  insert.run(...binds);
@@ -4478,11 +5691,13 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4478
5691
  }
4479
5692
  function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4480
5693
  if (rows.length === 0) return;
4481
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4482
- for (let i = 0; i < rows.length; i += chunkSize) {
4483
- const chunk = rows.slice(i, i + chunkSize);
4484
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4485
- const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders}`);
5694
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5695
+ let cursor = 0;
5696
+ for (const take of ladder) {
5697
+ const chunk = rows.slice(cursor, cursor + take);
5698
+ cursor += take;
5699
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5700
+ const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders2}`);
4486
5701
  const binds = [];
4487
5702
  for (const r of chunk) binds.push(r.id, r.vector);
4488
5703
  insert.run(...binds);
@@ -4490,13 +5705,15 @@ function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4490
5705
  }
4491
5706
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
4492
5707
  if (refs.length === 0) return;
4493
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
4494
- for (let i = 0; i < refs.length; i += chunkSize) {
4495
- const chunk = refs.slice(i, i + chunkSize);
4496
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5708
+ const ladder = ladderChunkSizes(refs.length, Math.max(1, Math.floor(maxSqlVars / 8)));
5709
+ let cursor = 0;
5710
+ for (const take of ladder) {
5711
+ const chunk = refs.slice(cursor, cursor + take);
5712
+ cursor += take;
5713
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4497
5714
  const insert = stmt(
4498
5715
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
4499
- VALUES ${placeholders}`
5716
+ VALUES ${placeholders2}`
4500
5717
  );
4501
5718
  const binds = [];
4502
5719
  for (const ref of chunk) {
@@ -4650,52 +5867,6 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
4650
5867
  return edges;
4651
5868
  }
4652
5869
 
4653
- // src/codebase-index/writer-helpers.ts
4654
- import { resolveWstackPaths } from "@wrongstack/core/utils";
4655
- function escapeLike(value) {
4656
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
4657
- }
4658
- function posixIndexPath(file) {
4659
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
4660
- }
4661
- function indexedFileMatchSql(column = "file") {
4662
- return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
4663
- }
4664
- function indexedFileMatchArgs(file) {
4665
- const posix4 = posixIndexPath(file.trim());
4666
- return [file, posix4, `%/${escapeLike(posix4)}`];
4667
- }
4668
- function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
4669
- if (packageLabel === filter) return true;
4670
- const posixFile = posixIndexPath(storedFile);
4671
- const posixFilter = posixIndexPath(filter.trim());
4672
- if (!posixFilter) return false;
4673
- return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
4674
- }
4675
- function assignRefsToSymbols(refs, symbols) {
4676
- if (refs.length === 0 || symbols.length === 0) return [];
4677
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4678
- const seen = /* @__PURE__ */ new Set();
4679
- const assigned = [];
4680
- for (const ref of refs) {
4681
- let owner;
4682
- for (const symbol of ordered) {
4683
- if (symbol.line > ref.line) break;
4684
- owner = symbol;
4685
- }
4686
- if (!owner && ref.callType === "import") owner = ordered[0];
4687
- if (!owner || owner.id <= 0) continue;
4688
- const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4689
- if (seen.has(key)) continue;
4690
- seen.add(key);
4691
- assigned.push({ ...ref, fromId: owner.id });
4692
- }
4693
- return assigned;
4694
- }
4695
- function resolveIndexDir(projectRoot, override) {
4696
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4697
- }
4698
-
4699
5870
  // src/codebase-index/writer-ref-mapper.ts
4700
5871
  function mapWriterRefRow(row) {
4701
5872
  return {
@@ -4717,20 +5888,20 @@ function mapWriterRefRow(row) {
4717
5888
  var MAX_SQL_VARS = 900;
4718
5889
  function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
4719
5890
  const results = [];
4720
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4721
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4722
- const placeholders = chunk.map(() => "?").join(",");
4723
- const sql = buildSql(placeholders);
5891
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5892
+ const chunk = padToInBucket(ids.slice(0, take));
5893
+ ids = ids.slice(take);
5894
+ const sql = buildSql(placeholders(chunk.length));
4724
5895
  results.push(...stmt(sql).all(...chunk, ...extraArgs));
4725
5896
  }
4726
5897
  return results;
4727
5898
  }
4728
5899
  function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
4729
5900
  let total = 0;
4730
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4731
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4732
- const placeholders = chunk.map(() => "?").join(",");
4733
- const sql = buildSql(placeholders);
5901
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5902
+ const chunk = padToInBucket(ids.slice(0, take));
5903
+ ids = ids.slice(take);
5904
+ const sql = buildSql(placeholders(chunk.length));
4734
5905
  const rows = stmt(sql).all(...chunk, ...extraArgs);
4735
5906
  total += rows[0]?.n ?? 0;
4736
5907
  }
@@ -4759,16 +5930,24 @@ function resolveIndexedFiles(stmt, file) {
4759
5930
  }
4760
5931
  function resolveSymbolIds(stmt, symbolName, file) {
4761
5932
  if (!file) {
4762
- const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
4763
- return rows2.map((r) => r.id);
5933
+ const rows = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(
5934
+ symbolName
5935
+ );
5936
+ return rows.map((r) => r.id);
4764
5937
  }
4765
5938
  const indexedFiles = resolveIndexedFiles(stmt, file);
4766
5939
  if (indexedFiles.length === 0) return [];
4767
- const placeholders = indexedFiles.map(() => "?").join(",");
4768
- const rows = stmt(
4769
- `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
4770
- ).all(symbolName, ...indexedFiles);
4771
- return rows.map((r) => r.id);
5940
+ const ids = [];
5941
+ let cursor = 0;
5942
+ for (const take of inListChunks(indexedFiles.length, MAX_SQL_VARS)) {
5943
+ const files = padToInBucket(indexedFiles.slice(cursor, cursor + take));
5944
+ cursor += take;
5945
+ const rows = stmt(
5946
+ `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders(files.length)}) ORDER BY id`
5947
+ ).all(symbolName, ...files);
5948
+ ids.push(...rows.map((r) => r.id));
5949
+ }
5950
+ return ids;
4772
5951
  }
4773
5952
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4774
5953
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
@@ -5202,9 +6381,9 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5202
6381
  const loadedIds = new Set(syms.map((s) => s.id));
5203
6382
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
5204
6383
  if (missingIds.length > 0) {
5205
- const placeholders = missingIds.map(() => "?").join(",");
6384
+ const placeholders2 = missingIds.map(() => "?").join(",");
5206
6385
  const extras = stmt(
5207
- `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
6386
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders2})`
5208
6387
  ).all(...missingIds);
5209
6388
  for (const s of extras) symById.set(s.id, s);
5210
6389
  }
@@ -5217,81 +6396,6 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5217
6396
  return { nodes, edges };
5218
6397
  }
5219
6398
 
5220
- // src/codebase-index/vector-search.ts
5221
- var RRF_K = 60;
5222
- var VECTOR_DIMENSIONS = 384;
5223
- var NGRAM_SIZE = 3;
5224
- function embedText(text) {
5225
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5226
- const normalized = text.toLowerCase().trim();
5227
- if (normalized.length < NGRAM_SIZE) {
5228
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5229
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5230
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5231
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5232
- vec[bucket] += 1;
5233
- }
5234
- } else {
5235
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5236
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5237
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5238
- vec[bucket] += 1;
5239
- }
5240
- }
5241
- let norm = 0;
5242
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5243
- norm += vec[i] * vec[i];
5244
- }
5245
- norm = Math.sqrt(norm);
5246
- if (norm > 0) {
5247
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5248
- vec[i] /= norm;
5249
- }
5250
- }
5251
- return vec;
5252
- }
5253
- function hashNgram(str) {
5254
- let hash = 2166136261;
5255
- for (let i = 0; i < str.length; i++) {
5256
- hash ^= str.charCodeAt(i);
5257
- hash = Math.imul(hash, 16777619);
5258
- }
5259
- return hash >>> 0;
5260
- }
5261
- function cosineSimilarity(a, b) {
5262
- let dot = 0;
5263
- const len = Math.min(a.length, b.length);
5264
- for (let i = 0; i < len; i++) {
5265
- dot += a[i] * b[i];
5266
- }
5267
- return dot;
5268
- }
5269
- function encodeVector(vec) {
5270
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5271
- }
5272
- function decodeVector(buf) {
5273
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5274
- const copy = new Float32Array(buf.byteLength / 4);
5275
- for (let i = 0; i < copy.length; i++) {
5276
- copy[i] = view.getFloat32(i * 4, true);
5277
- }
5278
- return copy;
5279
- }
5280
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5281
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5282
- const scored = [];
5283
- for (const id of allIds) {
5284
- const bm25Rank = bm25Ranks.get(id);
5285
- const vecRank = vectorRanks.get(id);
5286
- let score = 0;
5287
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5288
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5289
- scored.push([id, score]);
5290
- }
5291
- scored.sort((a, b) => b[1] - a[1]);
5292
- return scored;
5293
- }
5294
-
5295
6399
  // src/codebase-index/writer-mutations.ts
5296
6400
  function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5297
6401
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
@@ -5303,24 +6407,29 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5303
6407
  for (const ref of entry.refs) affectedNames.add(ref.toName);
5304
6408
  }
5305
6409
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5306
- const placeholders = options.deleteForFiles.map(() => "?").join(",");
5307
6410
  for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5308
6411
  affectedNames.add(name);
5309
6412
  }
5310
- if (ftsAvailable) {
5311
- stmtFn(
5312
- `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5313
- ).run(...options.deleteForFiles);
5314
- }
5315
- if (vectorsAvailable) {
6413
+ let cursor = 0;
6414
+ for (const take of inListChunks(options.deleteForFiles.length, Math.floor(maxSqlVars / 4))) {
6415
+ const bucket = padToInBucket(options.deleteForFiles.slice(cursor, cursor + take));
6416
+ cursor += take;
6417
+ const ph = placeholders(bucket.length);
6418
+ if (ftsAvailable) {
6419
+ stmtFn(
6420
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6421
+ ).run(...bucket);
6422
+ }
6423
+ if (vectorsAvailable) {
6424
+ stmtFn(
6425
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6426
+ ).run(...bucket);
6427
+ }
5316
6428
  stmtFn(
5317
- `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5318
- ).run(...options.deleteForFiles);
6429
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6430
+ ).run(...bucket);
6431
+ stmtFn(`DELETE FROM symbols WHERE file IN (${ph})`).run(...bucket);
5319
6432
  }
5320
- stmtFn(
5321
- `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5322
- ).run(...options.deleteForFiles);
5323
- stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5324
6433
  }
5325
6434
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5326
6435
  let nextId = allocateSymbolIds(totalSymbols);
@@ -5352,12 +6461,14 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5352
6461
  text: buildIndexableText(s.name, s.signature, s.docComment)
5353
6462
  });
5354
6463
  }
5355
- vectorRows.push({
5356
- id,
5357
- vector: encodeVector(
5358
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5359
- )
5360
- });
6464
+ if (vectorsAvailable) {
6465
+ vectorRows.push({
6466
+ id,
6467
+ vector: encodeVector(
6468
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6469
+ )
6470
+ });
6471
+ }
5361
6472
  const inserted = { ...s, id };
5362
6473
  allInserted.push(inserted);
5363
6474
  insertedForEntry.push(inserted);
@@ -5449,9 +6560,8 @@ var CORE_TABLES_SQL = `
5449
6560
  signature TEXT NOT NULL DEFAULT '',
5450
6561
  doc_comment TEXT NOT NULL DEFAULT '',
5451
6562
  scope TEXT NOT NULL DEFAULT '',
5452
- text TEXT NOT NULL DEFAULT '',
5453
- file_fk TEXT NOT NULL
5454
- );
6563
+ text TEXT NOT NULL DEFAULT ''
6564
+ );
5455
6565
  `;
5456
6566
  var FILE_INDEX_SQL = [
5457
6567
  "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
@@ -5462,7 +6572,6 @@ var SYMBOL_INDEX_SQL = [
5462
6572
  "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
5463
6573
  "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
5464
6574
  "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
5465
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
5466
6575
  "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
5467
6576
  ];
5468
6577
  var REFS_TABLE_SQL = `
@@ -5537,11 +6646,12 @@ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5537
6646
  return stmtFn(base).all();
5538
6647
  }
5539
6648
  const out = [];
5540
- for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5541
- const chunk = onlyFiles.slice(i, i + maxSqlVars);
5542
- const placeholders = chunk.map(() => "?").join(",");
6649
+ let cursor = 0;
6650
+ for (const take of inListChunks(onlyFiles.length, maxSqlVars)) {
6651
+ const chunk = padToInBucket(onlyFiles.slice(cursor, cursor + take));
6652
+ cursor += take;
5543
6653
  out.push(
5544
- ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
6654
+ ...stmtFn(`${base} AND s.file IN (${placeholders(chunk.length)})`).all(...chunk)
5545
6655
  );
5546
6656
  }
5547
6657
  return out;
@@ -5612,16 +6722,18 @@ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVar
5612
6722
  )`
5613
6723
  );
5614
6724
  const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5615
- for (let i = 0; i < resolutions.length; i += chunkSize) {
5616
- const chunk = resolutions.slice(i, i + chunkSize);
5617
- const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
6725
+ let cursor = 0;
6726
+ for (const take of ladderChunkSizes(resolutions.length, chunkSize)) {
6727
+ const chunk = resolutions.slice(cursor, cursor + take);
6728
+ cursor += take;
6729
+ const valuesPh = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5618
6730
  const binds = [];
5619
6731
  for (const entry of chunk) {
5620
6732
  binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5621
6733
  }
5622
6734
  stmtFn(
5623
6735
  `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5624
- VALUES ${placeholders}`
6736
+ VALUES ${valuesPh}`
5625
6737
  ).run(...binds);
5626
6738
  }
5627
6739
  db.exec(
@@ -5658,9 +6770,11 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5658
6770
  const list = [...names].filter((name) => name.length > 0);
5659
6771
  if (list.length === 0) return 0;
5660
6772
  let total = 0;
5661
- for (let i = 0; i < list.length; i += maxSqlVars) {
5662
- const chunk = list.slice(i, i + maxSqlVars);
5663
- const placeholders = chunk.map(() => "?").join(",");
6773
+ let cursor = 0;
6774
+ for (const take of inListChunks(list.length, maxSqlVars)) {
6775
+ const chunk = padToInBucket(list.slice(cursor, cursor + take));
6776
+ cursor += take;
6777
+ const ph = placeholders(chunk.length);
5664
6778
  try {
5665
6779
  const result = stmtFn(
5666
6780
  `UPDATE refs
@@ -5669,16 +6783,16 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5669
6783
  SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5670
6784
  FROM symbols sym
5671
6785
  JOIN lang_family lf ON lf.lang = sym.lang
5672
- WHERE sym.name IN (${placeholders})
6786
+ WHERE sym.name IN (${ph})
5673
6787
  GROUP BY sym.name, lf.family
5674
6788
  UNION ALL
5675
6789
  SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5676
6790
  FROM symbols sym
5677
- WHERE sym.name IN (${placeholders})
6791
+ WHERE sym.name IN (${ph})
5678
6792
  GROUP BY sym.name
5679
6793
  ) AS s,
5680
6794
  lang_family AS rf
5681
- WHERE refs.to_name IN (${placeholders})
6795
+ WHERE refs.to_name IN (${ph})
5682
6796
  AND rf.lang = refs.lang
5683
6797
  AND s.name = refs.to_name
5684
6798
  AND s.family = rf.family`
@@ -5690,7 +6804,7 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5690
6804
  SELECT sym.id FROM symbols sym
5691
6805
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5692
6806
  ORDER BY sym.id LIMIT 1
5693
- ) WHERE refs.to_name IN (${placeholders})
6807
+ ) WHERE refs.to_name IN (${ph})
5694
6808
  AND EXISTS (
5695
6809
  SELECT 1 FROM symbols sym
5696
6810
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
@@ -5747,7 +6861,7 @@ function buildWriterSearchWhere(query, filter) {
5747
6861
  const conditions = [];
5748
6862
  const values = [];
5749
6863
  let effectiveKind = filter?.kind;
5750
- if (filter?.lspKind !== void 0) {
6864
+ if (filter?.lspKind != null) {
5751
6865
  const mapped = lspKindToInternalKind(filter.lspKind);
5752
6866
  if (mapped !== null) {
5753
6867
  effectiveKind = mapped;
@@ -5826,7 +6940,7 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5826
6940
  );
5827
6941
  }
5828
6942
  let effectiveKind = filter?.kind;
5829
- if (filter?.lspKind !== void 0) {
6943
+ if (filter?.lspKind != null) {
5830
6944
  const mapped = lspKindToInternalKind(filter.lspKind);
5831
6945
  if (mapped === null) return { results: [], total: 0 };
5832
6946
  effectiveKind = mapped;
@@ -5863,15 +6977,14 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5863
6977
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5864
6978
  }
5865
6979
  const where = conditions.join(" AND ");
5866
- const countRows = stmtFn(
5867
- `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5868
- ).all(...values);
5869
- const total = countRows[0] ? Number(countRows[0].n) : 0;
5870
- if (total === 0) return { results: [], total: 0 };
5871
6980
  const bm25Rows = stmtFn(
5872
6981
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5873
6982
  -bm25(symbols_fts) AS score,
5874
- snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
6983
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet,
6984
+ -- Keep this uncorrelated: referencing outer columns turns it into
6985
+ -- a per-row subquery and defeats the one-count-per-statement win.
6986
+ (SELECT COUNT(*) FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
6987
+ WHERE ${where}) AS total_count
5875
6988
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5876
6989
  WHERE ${where}
5877
6990
  ORDER BY
@@ -5880,13 +6993,15 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5880
6993
  ELSE 2 END,
5881
6994
  bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5882
6995
  LIMIT ?`
5883
- ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5884
- if (vectorsAvailable && bm25Rows.length > 0) {
6996
+ ).all(...values, ...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
6997
+ if (bm25Rows.length === 0) return { results: [], total: 0 };
6998
+ const total = Number(bm25Rows[0]?.total_count ?? 0);
6999
+ if (vectorsAvailable) {
5885
7000
  const queryVec = embedText(query);
5886
7001
  const candidateIds = bm25Rows.map((r) => r.id);
5887
- const placeholders = candidateIds.map(() => "?").join(",");
7002
+ const placeholders2 = candidateIds.map(() => "?").join(",");
5888
7003
  const vecRows = stmtFn(
5889
- `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
7004
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders2})`
5890
7005
  ).all(...candidateIds);
5891
7006
  const vecScores = vecRows.map((r) => ({
5892
7007
  id: r.symbol_id,
@@ -6074,9 +7189,9 @@ var IndexStore = class _IndexStore {
6074
7189
  }
6075
7190
  constructor(projectRoot, opts = {}) {
6076
7191
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6077
- fs8.mkdirSync(this.indexDir, { recursive: true });
7192
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6078
7193
  const Database = loadDatabaseSync();
6079
- this.db = new Database(path12.join(this.indexDir, DB_FILE2));
7194
+ this.db = new Database(path13.join(this.indexDir, DB_FILE2));
6080
7195
  applyIndexStorePragmas(this.db);
6081
7196
  this.initSchema();
6082
7197
  }
@@ -6205,7 +7320,11 @@ var IndexStore = class _IndexStore {
6205
7320
  );
6206
7321
  if (symbolCount !== ftsCount) {
6207
7322
  this.db.exec("DELETE FROM symbols_fts");
6208
- if (this.vectorsAvailable) this.db.exec("DELETE FROM symbol_vectors");
7323
+ if (vectorEmbeddingEnabled() && this.stmt(
7324
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbol_vectors'"
7325
+ ).get() !== void 0) {
7326
+ this.db.exec("DELETE FROM symbol_vectors");
7327
+ }
6209
7328
  const rows = this.stmt(
6210
7329
  "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
6211
7330
  ).all();
@@ -6224,8 +7343,13 @@ var IndexStore = class _IndexStore {
6224
7343
  this.ftsAvailable = false;
6225
7344
  }
6226
7345
  try {
6227
- this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
6228
- this.vectorsAvailable = true;
7346
+ if (vectorEmbeddingEnabled()) {
7347
+ this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
7348
+ this.vectorsAvailable = true;
7349
+ } else {
7350
+ this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
7351
+ this.vectorsAvailable = false;
7352
+ }
6229
7353
  } catch {
6230
7354
  this.vectorsAvailable = false;
6231
7355
  }
@@ -6260,14 +7384,22 @@ var IndexStore = class _IndexStore {
6260
7384
  }
6261
7385
  invalidateIncomingRefsForFiles(files) {
6262
7386
  if (files.length === 0) return /* @__PURE__ */ new Set();
6263
- const placeholders = files.map(() => "?").join(",");
6264
- const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
6265
- ...files
6266
- ).map((row) => row.name);
6267
- this.stmt(
6268
- `UPDATE refs SET to_id = NULL
6269
- WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6270
- ).run(...files);
7387
+ const names = [];
7388
+ let cursor = 0;
7389
+ for (const take of inListChunks(files.length, _IndexStore.MAX_SQL_VARS)) {
7390
+ const bucket = padToInBucket(files.slice(cursor, cursor + take));
7391
+ cursor += take;
7392
+ const ph = placeholders(bucket.length);
7393
+ for (const row of this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${ph})`).all(
7394
+ ...bucket
7395
+ )) {
7396
+ names.push(row.name);
7397
+ }
7398
+ this.stmt(
7399
+ `UPDATE refs SET to_id = NULL
7400
+ WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
7401
+ ).run(...bucket);
7402
+ }
6271
7403
  return new Set(names);
6272
7404
  }
6273
7405
  resolveRefsForNamesUnsafe(names) {
@@ -6301,6 +7433,14 @@ var IndexStore = class _IndexStore {
6301
7433
  if (this.ftsAvailable) {
6302
7434
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
6303
7435
  }
7436
+ if (this.vectorsAvailable) {
7437
+ vectorRows.push({
7438
+ id,
7439
+ vector: encodeVector(
7440
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
7441
+ )
7442
+ });
7443
+ }
6304
7444
  result.push({ ...s, id });
6305
7445
  }
6306
7446
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -6333,15 +7473,15 @@ var IndexStore = class _IndexStore {
6333
7473
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6334
7474
  if (this.ftsAvailable) {
6335
7475
  this.stmt(
6336
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7476
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6337
7477
  ).run(file);
6338
7478
  }
6339
7479
  if (this.vectorsAvailable) {
6340
7480
  this.stmt(
6341
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7481
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6342
7482
  ).run(file);
6343
7483
  }
6344
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7484
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6345
7485
  this.resolveRefsForNamesUnsafe(affectedNames);
6346
7486
  this.commitWriteTransaction(ownsTransaction);
6347
7487
  } catch (error) {
@@ -6358,18 +7498,18 @@ var IndexStore = class _IndexStore {
6358
7498
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6359
7499
  if (this.ftsAvailable) {
6360
7500
  this.stmt(
6361
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7501
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6362
7502
  ).run(file);
6363
7503
  }
6364
7504
  if (this.vectorsAvailable) {
6365
7505
  this.stmt(
6366
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7506
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6367
7507
  ).run(file);
6368
7508
  }
6369
- this.stmt(
6370
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6371
- ).run(file);
6372
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7509
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7510
+ file
7511
+ );
7512
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6373
7513
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
6374
7514
  this.resolveRefsForNamesUnsafe(affectedNames);
6375
7515
  this.commitWriteTransaction(ownsTransaction);
@@ -6473,6 +7613,10 @@ var IndexStore = class _IndexStore {
6473
7613
  getStats() {
6474
7614
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6475
7615
  }
7616
+ /** P2.5: minimal summary for search-response piggyback (see writer-admin). */
7617
+ getIndexSummary() {
7618
+ return getIndexSummaryWithStatement((sql) => this.stmt(sql));
7619
+ }
6476
7620
  setLastIndexed(ts2) {
6477
7621
  this.runWithRetry(() => {
6478
7622
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
@@ -6574,18 +7718,18 @@ var IndexStore = class _IndexStore {
6574
7718
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6575
7719
  if (this.ftsAvailable) {
6576
7720
  this.stmt(
6577
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7721
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6578
7722
  ).run(meta.file);
6579
7723
  }
6580
7724
  if (this.vectorsAvailable) {
6581
7725
  this.stmt(
6582
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7726
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6583
7727
  ).run(meta.file);
6584
7728
  }
6585
- this.stmt(
6586
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6587
- ).run(meta.file);
6588
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
7729
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7730
+ meta.file
7731
+ );
7732
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(meta.file);
6589
7733
  this.stmt(
6590
7734
  `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
6591
7735
  VALUES (?, ?, ?, ?, ?, ?)
@@ -6617,6 +7761,27 @@ var IndexStore = class _IndexStore {
6617
7761
  } catch {
6618
7762
  }
6619
7763
  }
7764
+ /**
7765
+ * P4.14: best-effort WAL checkpoint for idle-time maintenance.
7766
+ *
7767
+ * `wal_autocheckpoint` is PASSIVE and only attempts work after a COMMIT —
7768
+ * once writes stop, nothing fires again, so the WAL keeps whatever frames
7769
+ * the last burst left. This probes with PASSIVE first (never blocks; busy=1
7770
+ * means readers still hold WAL snapshots) and only issues the TRUNCATE —
7771
+ * which resets index.db-wal to zero bytes — when the checkpointer can
7772
+ * proceed immediately. Callers run this on the daemon's single thread, so
7773
+ * never wait on readers here: busy means "retry at the next idle window".
7774
+ */
7775
+ checkpointWal() {
7776
+ try {
7777
+ const probe = this.db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get();
7778
+ if (Number(probe?.busy ?? 1) !== 0) return false;
7779
+ const done = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
7780
+ return Number(done?.busy ?? 1) === 0;
7781
+ } catch {
7782
+ return false;
7783
+ }
7784
+ }
6620
7785
  compactIfNeeded(options = {}) {
6621
7786
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6622
7787
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6702,7 +7867,9 @@ function resolveParallelBatch() {
6702
7867
  return indexParallelBatchSize(availableParallelism());
6703
7868
  }
6704
7869
  function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
6705
- return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
7870
+ const threshold = resolveWorkerPoolThreshold();
7871
+ if (threshold === 0) return false;
7872
+ return !isFrugalPerf() && candidateFileCount >= threshold && parseBatchCount > 1;
6706
7873
  }
6707
7874
  function yieldEventLoop() {
6708
7875
  return new Promise((resolve4) => setImmediate(resolve4));
@@ -6725,15 +7892,15 @@ var IndexSourceChangedError = class extends Error {
6725
7892
  name = "IndexSourceChangedError";
6726
7893
  };
6727
7894
  function isWithinProject(projectRoot, file) {
6728
- const rel = path13.relative(projectRoot, file);
6729
- return rel !== "" && !rel.startsWith(`..${path13.sep}`) && rel !== ".." && !path13.isAbsolute(rel);
7895
+ const rel = path14.relative(projectRoot, file);
7896
+ return rel !== "" && !rel.startsWith(`..${path14.sep}`) && rel !== ".." && !path14.isAbsolute(rel);
6730
7897
  }
6731
7898
  function isMissingPathError(err) {
6732
7899
  const code = err?.code;
6733
7900
  return code === "ENOENT" || code === "ENOTDIR";
6734
7901
  }
6735
7902
  function normalizeComparablePath(value) {
6736
- const resolved = path13.resolve(value);
7903
+ const resolved = path14.resolve(value);
6737
7904
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6738
7905
  }
6739
7906
  function gitOutput(projectRoot, args) {
@@ -6779,24 +7946,24 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6779
7946
  const record = statusRecords[i];
6780
7947
  if (!record) continue;
6781
7948
  const status = record.slice(0, 2);
6782
- const changedPath = path13.resolve(projectRoot, record.slice(3));
7949
+ const changedPath = path14.resolve(projectRoot, record.slice(3));
6783
7950
  dirty.add(changedPath);
6784
7951
  if (status.includes("D")) deleted.add(changedPath);
6785
7952
  if (status.includes("R") || status.includes("C")) {
6786
7953
  const source = statusRecords[++i];
6787
- if (source) dirty.add(path13.resolve(projectRoot, source));
7954
+ if (source) dirty.add(path14.resolve(projectRoot, source));
6788
7955
  }
6789
7956
  }
6790
7957
  const files = [];
6791
7958
  for (const relative4 of output.toString("utf8").split("\0")) {
6792
7959
  if (!relative4) continue;
6793
7960
  const portable = relative4.replace(/\\/g, "/");
6794
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path13.posix.basename(portable))) {
7961
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path14.posix.basename(portable))) {
6795
7962
  continue;
6796
7963
  }
6797
- const full = path13.resolve(projectRoot, relative4);
7964
+ const full = path14.resolve(projectRoot, relative4);
6798
7965
  if (deleted.has(full)) continue;
6799
- const ext = path13.extname(relative4).toLowerCase();
7966
+ const ext = path14.extname(relative4).toLowerCase();
6800
7967
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6801
7968
  }
6802
7969
  const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
@@ -6804,7 +7971,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6804
7971
  for (const dirtyFile of [...dirty].sort()) {
6805
7972
  if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6806
7973
  snapshot.update("\0").update(dirtyFile).update("\0");
6807
- snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
7974
+ snapshot.update(xxhash64String(await fs10.readFile(dirtyFile, "utf8")));
6808
7975
  }
6809
7976
  return {
6810
7977
  files,
@@ -6840,7 +8007,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6840
8007
  }
6841
8008
  let entries;
6842
8009
  try {
6843
- entries = await fs9.readdir(dir, { withFileTypes: true });
8010
+ entries = await fs10.readdir(dir, { withFileTypes: true });
6844
8011
  } catch (err) {
6845
8012
  complete = false;
6846
8013
  errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6849,14 +8016,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6849
8016
  dirCount++;
6850
8017
  for (const e of entries) {
6851
8018
  if (ignoreSet.has(e.name)) continue;
6852
- const full = path13.join(dir, e.name);
6853
- const rel = path13.relative(projectRoot, full).replace(/\\/g, "/");
8019
+ const full = path14.join(dir, e.name);
8020
+ const rel = path14.relative(projectRoot, full).replace(/\\/g, "/");
6854
8021
  if (e.isDirectory()) {
6855
8022
  if (isGitIgnored(rel, true)) continue;
6856
8023
  await walk(full);
6857
8024
  } else if (e.isFile()) {
6858
8025
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
6859
- const ext = path13.extname(e.name).toLowerCase();
8026
+ const ext = path14.extname(e.name).toLowerCase();
6860
8027
  if (indexableExts.has(ext) || detectLang(full) !== null) {
6861
8028
  results.push(full);
6862
8029
  }
@@ -6942,10 +8109,10 @@ async function runIndexerAtomic(store, opts) {
6942
8109
  let trustedUnchanged;
6943
8110
  let discoverySnapshotKey;
6944
8111
  if (opts.files && opts.files.length > 0) {
6945
- files = opts.files.map((f) => path13.resolve(projectRoot, f)).filter((f) => {
8112
+ files = opts.files.map((f) => path14.resolve(projectRoot, f)).filter((f) => {
6946
8113
  if (!isWithinProject(projectRoot, f)) return false;
6947
- const rel = path13.relative(projectRoot, f).replace(/\\/g, "/");
6948
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path13.basename(f)) && !isGitIgnored(rel, false);
8114
+ const rel = path14.relative(projectRoot, f).replace(/\\/g, "/");
8115
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path14.basename(f)) && !isGitIgnored(rel, false);
6949
8116
  });
6950
8117
  } else {
6951
8118
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -6978,7 +8145,6 @@ async function runIndexerAtomic(store, opts) {
6978
8145
  if (!meta || !trustedUnchanged.has(file)) return true;
6979
8146
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
6980
8147
  symbolsIndexed += meta.symbolCount;
6981
- filesIndexed++;
6982
8148
  filesSkipped++;
6983
8149
  filesPreSkipped++;
6984
8150
  return false;
@@ -7007,7 +8173,7 @@ async function runIndexerAtomic(store, opts) {
7007
8173
  async (file) => {
7008
8174
  let stat3;
7009
8175
  try {
7010
- stat3 = await fs9.stat(file, statOpts);
8176
+ stat3 = await fs10.stat(file, statOpts);
7011
8177
  } catch (e) {
7012
8178
  if (isAbortError(e)) throw e;
7013
8179
  return {
@@ -7034,7 +8200,7 @@ async function runIndexerAtomic(store, opts) {
7034
8200
  const meta = existingMeta.get(file);
7035
8201
  let content;
7036
8202
  try {
7037
- content = await fs9.readFile(file, { encoding: "utf8", signal });
8203
+ content = await fs10.readFile(file, { encoding: "utf8", signal });
7038
8204
  } catch (e) {
7039
8205
  if (isAbortError(e)) throw e;
7040
8206
  return {
@@ -7099,22 +8265,19 @@ async function runIndexerAtomic(store, opts) {
7099
8265
  }
7100
8266
  }
7101
8267
  if (!pool) {
7102
- await Promise.all(
7103
- toParse.map(async (item) => {
7104
- try {
7105
- const parsed = await parseFileContent(item.file, item.content, item.lang);
7106
- const settled = statReadParse[item.index];
7107
- if (settled.status === "fulfilled") {
7108
- settled.value.parsed = parsed;
7109
- }
7110
- } catch (e) {
7111
- const settled = statReadParse[item.index];
7112
- if (settled.status === "fulfilled") {
7113
- settled.value.error = `parse error: ${e instanceof Error ? e.message : String(e)}`;
7114
- }
7115
- }
7116
- })
8268
+ const parsedAll = await parseFilesContent(
8269
+ toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
7117
8270
  );
8271
+ for (let pi2 = 0; pi2 < parsedAll.length && pi2 < toParse.length; pi2++) {
8272
+ const settled = statReadParse[toParse[pi2].index];
8273
+ if (settled.status !== "fulfilled") continue;
8274
+ const slot = parsedAll[pi2];
8275
+ if (slot.result) {
8276
+ settled.value.parsed = slot.result;
8277
+ } else {
8278
+ settled.value.error = `parse error: ${slot.error ?? `no result for ${toParse[pi2].file}`}`;
8279
+ }
8280
+ }
7118
8281
  }
7119
8282
  }
7120
8283
  const batchEntries = [];
@@ -7140,7 +8303,6 @@ async function runIndexerAtomic(store, opts) {
7140
8303
  if (result.skippedMeta) {
7141
8304
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
7142
8305
  symbolsIndexed += result.skippedMeta.symbolCount;
7143
- filesIndexed++;
7144
8306
  filesSkipped++;
7145
8307
  const stored = existingMeta.get(file);
7146
8308
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
@@ -7165,7 +8327,6 @@ async function runIndexerAtomic(store, opts) {
7165
8327
  lastIndexed: Date.now(),
7166
8328
  contentHash: result.contentHash ?? ""
7167
8329
  });
7168
- filesIndexed++;
7169
8330
  filesEmpty++;
7170
8331
  }
7171
8332
  continue;
@@ -7179,7 +8340,6 @@ async function runIndexerAtomic(store, opts) {
7179
8340
  lastIndexed: Date.now(),
7180
8341
  contentHash: result.contentHash ?? ""
7181
8342
  });
7182
- filesIndexed++;
7183
8343
  filesEmpty++;
7184
8344
  continue;
7185
8345
  }
@@ -7309,7 +8469,7 @@ async function indexService(args, hooks = {}) {
7309
8469
  function searchService(args) {
7310
8470
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
7311
8471
  try {
7312
- return store.searchRanked(
8472
+ const result = store.searchRanked(
7313
8473
  args.query,
7314
8474
  {
7315
8475
  kind: args.kind,
@@ -7319,6 +8479,10 @@ function searchService(args) {
7319
8479
  },
7320
8480
  args.limit
7321
8481
  );
8482
+ if (result.total === 0) {
8483
+ return { ...result, indexSummary: store.getIndexSummary() };
8484
+ }
8485
+ return result;
7322
8486
  } finally {
7323
8487
  indexStorePool.release(store);
7324
8488
  }
@@ -7382,34 +8546,71 @@ function outgoingCallsService(args) {
7382
8546
  init_languages();
7383
8547
 
7384
8548
  // src/codebase-index/project-server-client.ts
7385
- import { spawn as spawn3 } from "node:child_process";
7386
- import * as fs12 from "node:fs";
8549
+ import { spawn as spawn4 } from "node:child_process";
8550
+ import * as fs13 from "node:fs";
7387
8551
  import * as net from "node:net";
7388
- import { StringDecoder } from "node:string_decoder";
7389
8552
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7390
8553
 
7391
8554
  // src/codebase-index/binary-frame.ts
7392
8555
  import { decode, encode } from "@msgpack/msgpack";
7393
8556
  var BINARY_FRAME_MAGIC = 87;
8557
+ var MAX_BINARY_FRAME_BYTES = 256 * 1024 * 1024;
8558
+ var MAX_INBOUND_BINARY_FRAME_BYTES = 64 * 1024 * 1024;
7394
8559
  function isBinaryFrame(firstByte) {
7395
8560
  return firstByte === BINARY_FRAME_MAGIC;
7396
8561
  }
7397
8562
  function encodeBinaryFrame(message) {
7398
- const payload = encode(message);
8563
+ const payload = encode(normalizeUndefined(message));
7399
8564
  const header = Buffer.allocUnsafe(5);
7400
8565
  header[0] = BINARY_FRAME_MAGIC;
7401
8566
  header.writeUInt32BE(payload.length, 1);
7402
8567
  return Buffer.concat([header, payload], 5 + payload.length);
7403
8568
  }
8569
+ function normalizeUndefined(value) {
8570
+ if (value instanceof Date) {
8571
+ const time = value.getTime();
8572
+ return Number.isNaN(time) ? null : value.toISOString();
8573
+ }
8574
+ if (value instanceof Map) return normalizeUndefined(Object.fromEntries(value));
8575
+ if (value instanceof Set) return normalizeUndefined([...value]);
8576
+ if (value instanceof Error) {
8577
+ return normalizeUndefined({ name: value.name, message: value.message, stack: value.stack });
8578
+ }
8579
+ if (value instanceof RegExp) return String(value);
8580
+ if (value instanceof URL) return value.toJSON();
8581
+ if (Buffer.isBuffer(value)) return { type: "Buffer", data: [...value] };
8582
+ if (Array.isArray(value)) return value.map((entry) => normalizeUndefined(entry));
8583
+ if (!isPlainObject(value)) return value;
8584
+ const out = {};
8585
+ for (const [key, entry] of Object.entries(value)) {
8586
+ if (entry === void 0) continue;
8587
+ out[key] = normalizeUndefined(entry);
8588
+ }
8589
+ return out;
8590
+ }
8591
+ function isPlainObject(value) {
8592
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
8593
+ const proto = Object.getPrototypeOf(value);
8594
+ return proto === Object.prototype || proto === null;
8595
+ }
7404
8596
  function decodeBinaryFrame(payload) {
7405
8597
  return decode(payload);
7406
8598
  }
8599
+ function encodeJsonFrame(message) {
8600
+ return `${JSON.stringify(normalizeUndefined(message))}
8601
+ `;
8602
+ }
8603
+
8604
+ // src/codebase-index/project-server-client-state.ts
8605
+ import * as fs12 from "node:fs";
8606
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8607
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
7407
8608
 
7408
8609
  // src/codebase-index/project-server-endpoint.ts
7409
8610
  import { createHash as createHash3 } from "node:crypto";
7410
- import * as fs10 from "node:fs";
7411
- import * as os3 from "node:os";
7412
- import * as path14 from "node:path";
8611
+ import * as fs11 from "node:fs";
8612
+ import * as os4 from "node:os";
8613
+ import * as path15 from "node:path";
7413
8614
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7414
8615
  var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
7415
8616
  var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
@@ -7418,21 +8619,21 @@ var buildIdCache;
7418
8619
  function projectIndexServerBuildId(entrypoint) {
7419
8620
  const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
7420
8621
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
7421
- const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path14.resolve(cleanHref);
8622
+ const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path15.resolve(cleanHref);
7422
8623
  try {
7423
- const stat3 = fs10.statSync(file);
8624
+ const stat3 = fs11.statSync(file);
7424
8625
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7425
8626
  return buildIdCache.buildId;
7426
8627
  }
7427
- const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
8628
+ const buildId = createHash3("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
7428
8629
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7429
8630
  return buildId;
7430
8631
  } catch {
7431
- return `unreadable:${path14.basename(file)}`;
8632
+ return `unreadable:${path15.basename(file)}`;
7432
8633
  }
7433
8634
  }
7434
8635
  function normalizeLocalPath(value) {
7435
- const resolved = path14.resolve(value);
8636
+ const resolved = path15.resolve(value);
7436
8637
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7437
8638
  }
7438
8639
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -7444,26 +8645,16 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
7444
8645
  if (process.platform === "win32") {
7445
8646
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
7446
8647
  }
7447
- return path14.join(os3.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
8648
+ return path15.join(os4.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
7448
8649
  }
7449
8650
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
7450
- return path14.join(
7451
- path14.resolve(resolveIndexDir(projectRoot, indexDir)),
8651
+ return path15.join(
8652
+ path15.resolve(resolveIndexDir(projectRoot, indexDir)),
7452
8653
  PROJECT_INDEX_SERVER_METADATA_FILE
7453
8654
  );
7454
8655
  }
7455
8656
 
7456
- // src/codebase-index/project-server-protocol.ts
7457
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
7458
- function encodeProjectServerMessage(message) {
7459
- return `${JSON.stringify(message)}
7460
- `;
7461
- }
7462
-
7463
8657
  // src/codebase-index/project-server-client-state.ts
7464
- import * as fs11 from "node:fs";
7465
- import { fileURLToPath as fileURLToPath4 } from "node:url";
7466
- import { checkUnixSocketPath } from "@wrongstack/core/utils";
7467
8658
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
7468
8659
  var SERVER_START_TIMEOUT_MS = 1e4;
7469
8660
  var SERVER_CONTROL_TIMEOUT_MS = 5e3;
@@ -7494,7 +8685,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
7494
8685
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
7495
8686
  try {
7496
8687
  const url = new URL(rel, import.meta.url);
7497
- if (url.protocol === "file:" && fs11.existsSync(fileURLToPath4(url))) {
8688
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath4(url))) {
7498
8689
  builtUrl = url;
7499
8690
  break;
7500
8691
  }
@@ -7582,6 +8773,12 @@ function cancellationError(signal) {
7582
8773
  return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
7583
8774
  }
7584
8775
 
8776
+ // src/codebase-index/project-server-protocol.ts
8777
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
8778
+ function encodeProjectServerMessage(message) {
8779
+ return encodeJsonFrame(message);
8780
+ }
8781
+
7585
8782
  // src/codebase-index/project-server-client.ts
7586
8783
  var ProjectServerConnection = class {
7587
8784
  constructor(projectRoot, indexDir, endpoint) {
@@ -7594,11 +8791,13 @@ var ProjectServerConnection = class {
7594
8791
  indexDir;
7595
8792
  endpoint;
7596
8793
  socket = null;
7597
- buffer = "";
7598
- /** P6: binary frame buffer accumulates raw bytes when in binary mode. */
7599
- binaryBuffer = [];
7600
- /** P6: StringDecoder for safe UTF-8 multibyte handling in JSON mode. */
7601
- textDecoder = null;
8794
+ /**
8795
+ * Raw inbound bytes for the unified per-frame reader. Frames are sniffed
8796
+ * individually — JSON text (newline-terminated) or binary (magic 0x57) —
8797
+ * instead of latching a read mode, so a JSON broadcast between binary
8798
+ * frames cannot desynchronize the reader.
8799
+ */
8800
+ readBuffer = Buffer.alloc(0);
7602
8801
  /** P6: true once the server advertises binary support and client accepts. */
7603
8802
  useBinary = false;
7604
8803
  info = null;
@@ -7740,7 +8939,7 @@ var ProjectServerConnection = class {
7740
8939
  this.activity = null;
7741
8940
  this.health = null;
7742
8941
  this.useBinary = false;
7743
- this.binaryBuffer = [];
8942
+ this.readBuffer = Buffer.alloc(0);
7744
8943
  this.connectReject?.(new Error("codebase-index client disconnected"));
7745
8944
  this.connectResolve = null;
7746
8945
  this.connectReject = null;
@@ -7761,7 +8960,7 @@ var ProjectServerConnection = class {
7761
8960
  currentAuthToken() {
7762
8961
  if (this.authToken === void 0) {
7763
8962
  try {
7764
- const raw = fs12.readFileSync(
8963
+ const raw = fs13.readFileSync(
7765
8964
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
7766
8965
  "utf8"
7767
8966
  );
@@ -7868,10 +9067,8 @@ var ProjectServerConnection = class {
7868
9067
  this.info = null;
7869
9068
  this.activity = null;
7870
9069
  this.health = null;
7871
- this.buffer = "";
7872
- this.binaryBuffer = [];
9070
+ this.readBuffer = Buffer.alloc(0);
7873
9071
  this.useBinary = false;
7874
- this.textDecoder = null;
7875
9072
  return new Promise((resolve4, reject) => {
7876
9073
  const socket = net.createConnection(this.endpoint);
7877
9074
  this.socket = socket;
@@ -7901,18 +9098,47 @@ var ProjectServerConnection = class {
7901
9098
  socket.on("close", () => this.onClose(socket));
7902
9099
  });
7903
9100
  }
9101
+ /**
9102
+ * Unified per-frame reader. Each frame is sniffed by its first byte:
9103
+ * `0x57` ('W') → length-prefixed MessagePack binary, anything else →
9104
+ * newline-delimited JSON text. Sniffing per frame (instead of latching a
9105
+ * mode) is what makes mixed streams work: the server may interleave a JSON
9106
+ * `index-state` broadcast between binary responses, and an old JSON-only
9107
+ * server stays readable while `useBinary` is armed.
9108
+ *
9109
+ * Multibyte UTF-8 in JSON frames is safe: raw `0x0a` only occurs as the
9110
+ * JSON delimiter (inside JSON strings `\n` is escaped), so a complete line
9111
+ * is always complete UTF-8.
9112
+ */
7904
9113
  onData(socket, chunk) {
7905
9114
  if (socket !== this.socket) return;
7906
- if (this.useBinary) {
7907
- this.onBinaryData(socket, chunk);
7908
- return;
7909
- }
7910
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
7911
- this.buffer += this.textDecoder.write(chunk);
9115
+ this.readBuffer = this.readBuffer.length === 0 ? chunk : Buffer.concat([this.readBuffer, chunk]);
7912
9116
  while (true) {
7913
- const newline = this.buffer.indexOf("\n");
9117
+ if (this.readBuffer.length === 0) return;
9118
+ if (this.useBinary && isBinaryFrame(this.readBuffer[0])) {
9119
+ if (this.readBuffer.length < 5) return;
9120
+ const frameLen = this.readBuffer.readUInt32BE(1);
9121
+ if (frameLen > MAX_BINARY_FRAME_BYTES) {
9122
+ socket.destroy();
9123
+ this.transition("offline", { error: "binary frame length exceeds the IPC limit" });
9124
+ return;
9125
+ }
9126
+ if (this.readBuffer.length < 5 + frameLen) return;
9127
+ const payload = this.readBuffer.subarray(5, 5 + frameLen);
9128
+ this.readBuffer = this.readBuffer.subarray(5 + frameLen);
9129
+ let message2;
9130
+ try {
9131
+ message2 = decodeBinaryFrame(payload);
9132
+ } catch {
9133
+ socket.destroy(new Error("invalid binary codebase-index server response"));
9134
+ return;
9135
+ }
9136
+ this.onMessage(message2);
9137
+ continue;
9138
+ }
9139
+ const newline = this.readBuffer.indexOf(10);
7914
9140
  if (newline < 0) {
7915
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
9141
+ if (this.readBuffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
7916
9142
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7917
9143
  }
7918
9144
  return;
@@ -7921,8 +9147,8 @@ var ProjectServerConnection = class {
7921
9147
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7922
9148
  return;
7923
9149
  }
7924
- const line = this.buffer.slice(0, newline);
7925
- this.buffer = this.buffer.slice(newline + 1);
9150
+ const line = this.readBuffer.subarray(0, newline).toString("utf8");
9151
+ this.readBuffer = this.readBuffer.subarray(newline + 1);
7926
9152
  if (!line) continue;
7927
9153
  let message;
7928
9154
  try {
@@ -7934,44 +9160,6 @@ var ProjectServerConnection = class {
7934
9160
  this.onMessage(message);
7935
9161
  }
7936
9162
  }
7937
- /**
7938
- * P6: Parse binary frames from the raw buffer.
7939
- *
7940
- * Each frame is: [1 byte magic 0x57] [4 bytes uint32 BE length] [payload].
7941
- * The magic byte distinguishes binary from JSON — a JSON frame's first byte
7942
- * is `{` (0x7B), so there is no ambiguity even in a mixed-mode buffer.
7943
- */
7944
- onBinaryData(socket, chunk) {
7945
- this.binaryBuffer.push(chunk);
7946
- const all = Buffer.concat(this.binaryBuffer);
7947
- let offset = 0;
7948
- while (offset + 5 <= all.length) {
7949
- if (!isBinaryFrame(all[offset])) {
7950
- this.useBinary = false;
7951
- this.buffer += all.subarray(offset).toString("utf8");
7952
- this.binaryBuffer = [];
7953
- return;
7954
- }
7955
- const frameLen = all.readUInt32BE(offset + 1);
7956
- if (frameLen > 256 * 1024 * 1024) {
7957
- socket.destroy();
7958
- this.transition("offline", { error: "binary frame length exceeds 256MB limit" });
7959
- return;
7960
- }
7961
- const totalLen = 5 + frameLen;
7962
- if (offset + totalLen > all.length) break;
7963
- const payload = all.subarray(offset + 5, offset + 5 + frameLen);
7964
- try {
7965
- const message = decodeBinaryFrame(payload);
7966
- this.onMessage(message);
7967
- } catch {
7968
- socket.destroy(new Error("invalid binary codebase-index server response"));
7969
- return;
7970
- }
7971
- offset += totalLen;
7972
- }
7973
- this.binaryBuffer = offset < all.length ? [all.subarray(offset)] : [];
7974
- }
7975
9163
  onMessage(message) {
7976
9164
  if (message.type === "hello") {
7977
9165
  if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
@@ -7991,7 +9179,7 @@ var ProjectServerConnection = class {
7991
9179
  }
7992
9180
  this.info = message;
7993
9181
  this.markResponsive();
7994
- if (message.binarySupported) this.useBinary = true;
9182
+ if (message.binarySupported && binaryFramingEnabled()) this.useBinary = true;
7995
9183
  this.transition("connected", { pid: message.pid });
7996
9184
  ensureHeartbeatLoop();
7997
9185
  this.connectResolve?.();
@@ -8077,13 +9265,13 @@ var ProjectServerConnection = class {
8077
9265
  if (!url) throw new Error("built codebase-index project server is unavailable");
8078
9266
  if (process.platform !== "win32") {
8079
9267
  try {
8080
- fs12.rmSync(this.endpoint, { force: true });
9268
+ fs13.rmSync(this.endpoint, { force: true });
8081
9269
  } catch {
8082
9270
  }
8083
9271
  }
8084
9272
  const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
8085
9273
  if (this.indexDir) args.push("--index-dir", this.indexDir);
8086
- const child = spawn3(process.execPath, args, {
9274
+ const child = spawn4(process.execPath, args, {
8087
9275
  detached: true,
8088
9276
  stdio: "ignore",
8089
9277
  windowsHide: true,
@@ -8101,8 +9289,8 @@ var ProjectServerConnection = class {
8101
9289
  process.kill(pid);
8102
9290
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
8103
9291
  try {
8104
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
8105
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
9292
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
9293
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
8106
9294
  } catch {
8107
9295
  }
8108
9296
  return true;
@@ -8114,6 +9302,10 @@ var ProjectServerConnection = class {
8114
9302
  var connections = /* @__PURE__ */ new Map();
8115
9303
  var MAX_CACHED_CONNECTIONS = 8;
8116
9304
  var heartbeatTimer;
9305
+ function binaryFramingEnabled() {
9306
+ const flag = process.env["WRONGSTACK_INDEX_BINARY"];
9307
+ return flag === "1" || flag === "true";
9308
+ }
8117
9309
  function forgetConnection(endpoint, connection) {
8118
9310
  if (connections.get(endpoint) === connection) connections.delete(endpoint);
8119
9311
  connection.close();
@@ -8203,7 +9395,7 @@ function resolveWorkerUrl() {
8203
9395
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8204
9396
  try {
8205
9397
  const url = new URL(rel, import.meta.url);
8206
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
9398
+ if (url.protocol === "file:" && fs14.existsSync(fileURLToPath6(url))) return url;
8207
9399
  } catch {
8208
9400
  }
8209
9401
  }
@@ -8566,14 +9758,14 @@ var replaceTool = {
8566
9758
  const dryRun = input.dry_run ?? true;
8567
9759
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
8568
9760
  const fileList = await resolveFiles(filesInput, ctx, globRe);
8569
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
9761
+ const realRoot = await fs15.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
8570
9762
  const results = [];
8571
9763
  let totalReplacements = 0;
8572
9764
  let diffBytesUsed = 0;
8573
9765
  let diffsOmitted = 0;
8574
9766
  let diffsTruncated = 0;
8575
9767
  for (const absPath of fileList) {
8576
- const lstat2 = await fs14.lstat(absPath).catch((err) => {
9768
+ const lstat2 = await fs15.lstat(absPath).catch((err) => {
8577
9769
  if (err.code === "ENOENT") return null;
8578
9770
  throw err;
8579
9771
  });
@@ -8581,17 +9773,17 @@ var replaceTool = {
8581
9773
  if (lstat2.isSymbolicLink()) continue;
8582
9774
  let realPath;
8583
9775
  try {
8584
- realPath = await fs14.realpath(absPath);
9776
+ realPath = await fs15.realpath(absPath);
8585
9777
  } catch {
8586
9778
  continue;
8587
9779
  }
8588
- const rel = path15.relative(realRoot, realPath);
8589
- if (rel.startsWith("..") || path15.isAbsolute(rel)) continue;
8590
- const stat3 = await fs14.stat(realPath).catch(() => null);
9780
+ const rel = path16.relative(realRoot, realPath);
9781
+ if (rel.startsWith("..") || path16.isAbsolute(rel)) continue;
9782
+ const stat3 = await fs15.stat(realPath).catch(() => null);
8591
9783
  if (!stat3?.isFile()) continue;
8592
9784
  let content;
8593
9785
  try {
8594
- const buf = await fs14.readFile(realPath);
9786
+ const buf = await fs15.readFile(realPath);
8595
9787
  if (isBinaryBuffer(buf)) continue;
8596
9788
  content = buf.toString("utf8");
8597
9789
  } catch {
@@ -8614,7 +9806,7 @@ var replaceTool = {
8614
9806
  if (!dryRun) {
8615
9807
  const newContent = toStyle(newContentLf, style);
8616
9808
  await atomicWrite(realPath, newContent, { mode: stat3.mode & 511 });
8617
- const written = await fs14.stat(realPath).catch(() => null);
9809
+ const written = await fs15.stat(realPath).catch(() => null);
8618
9810
  if (written) {
8619
9811
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
8620
9812
  }
@@ -8714,8 +9906,8 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
8714
9906
  const resolved = [];
8715
9907
  for (const p of parts) {
8716
9908
  const absPath = await safeResolveReal(p, ctx);
8717
- if (extraGlob && !passesExtraGlob(extraGlob, path15.basename(absPath), absPath)) continue;
8718
- const stat3 = await fs14.stat(absPath).catch(() => null);
9909
+ if (extraGlob && !passesExtraGlob(extraGlob, path16.basename(absPath), absPath)) continue;
9910
+ const stat3 = await fs15.stat(absPath).catch(() => null);
8719
9911
  if (stat3?.isFile()) {
8720
9912
  resolved.push(absPath);
8721
9913
  }
@@ -8729,7 +9921,7 @@ async function globFiles(pattern, base, extraGlob) {
8729
9921
  const { promise } = spawnRgFind(pattern, base);
8730
9922
  const files = await promise;
8731
9923
  if (extraGlob) {
8732
- return files.filter((f) => passesExtraGlob(extraGlob, path15.basename(f), f));
9924
+ return files.filter((f) => passesExtraGlob(extraGlob, path16.basename(f), f));
8733
9925
  }
8734
9926
  return files;
8735
9927
  } catch {
@@ -8744,7 +9936,7 @@ function __resetRgDetectionForTests() {
8744
9936
  function checkRg() {
8745
9937
  rgAvailabilityCache ??= new Promise((resolve4) => {
8746
9938
  try {
8747
- const p = spawn4("rg", ["--version"], {
9939
+ const p = spawn5("rg", ["--version"], {
8748
9940
  env: buildChildEnv(),
8749
9941
  stdio: "ignore",
8750
9942
  windowsHide: true
@@ -8759,7 +9951,7 @@ function checkRg() {
8759
9951
  }
8760
9952
  function spawnRgFind(pattern, base) {
8761
9953
  const args = ["--files", "--glob", pattern, base];
8762
- const child = spawn4("rg", args, {
9954
+ const child = spawn5("rg", args, {
8763
9955
  signal: AbortSignal.timeout(3e4),
8764
9956
  env: buildChildEnv(),
8765
9957
  stdio: ["ignore", "pipe", "pipe"],
@@ -8792,15 +9984,15 @@ async function globNative(pattern, base, extraGlob) {
8792
9984
  const walk = async (dir) => {
8793
9985
  let entries;
8794
9986
  try {
8795
- entries = await fs14.readdir(dir, { withFileTypes: true });
9987
+ entries = await fs15.readdir(dir, { withFileTypes: true });
8796
9988
  } catch {
8797
9989
  return;
8798
9990
  }
8799
9991
  for (const e of entries) {
8800
9992
  if (DEFAULT_IGNORE2.includes(e.name)) continue;
8801
- const full = path15.join(dir, e.name);
9993
+ const full = path16.join(dir, e.name);
8802
9994
  try {
8803
- const stat3 = await fs14.lstat(full);
9995
+ const stat3 = await fs15.lstat(full);
8804
9996
  if (stat3.isSymbolicLink()) continue;
8805
9997
  } catch {
8806
9998
  continue;