@wrongstack/tools 0.309.0 → 0.310.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/_regex.d.ts +6 -34
  2. package/dist/audit.js +3 -3
  3. package/dist/bash.js +4 -4
  4. package/dist/builtin.js +3020 -1680
  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 +2784 -1464
  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 +8 -6
  39. package/dist/format.js +3 -3
  40. package/dist/git.js +6 -0
  41. package/dist/grep.js +5 -124
  42. package/dist/index.js +3016 -1739
  43. package/dist/install.js +3 -3
  44. package/dist/json.js +5 -124
  45. package/dist/kanban.js +130 -0
  46. package/dist/languages/index.js +3 -3
  47. package/dist/lint.js +3 -3
  48. package/dist/logs.js +5 -121
  49. package/dist/outdated.js +3 -3
  50. package/dist/pack.js +3020 -1680
  51. package/dist/patch.js +2524 -1216
  52. package/dist/plan.js +106 -0
  53. package/dist/process-registry.js +1 -1
  54. package/dist/read.js +2506 -1198
  55. package/dist/replace.js +2487 -1295
  56. package/dist/search.js +6 -2
  57. package/dist/session-kanban.js +24 -16
  58. package/dist/task.js +106 -0
  59. package/dist/test.js +3 -3
  60. package/dist/todo.js +106 -0
  61. package/dist/tool-tier.d.ts +11 -0
  62. package/dist/tool-tier.js +3028 -1680
  63. package/dist/tree.js +14 -3
  64. package/dist/typecheck.js +3 -3
  65. package/dist/win32.js +5 -5
  66. package/dist/write.js +2513 -1205
  67. package/package.json +5 -4
package/dist/edit.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 loadTypescript2() {
183
- tsLoad2 ??= import("@typescript/typescript6").then((m) => {
184
- ts = m.default ?? m;
185
- return ts;
186
- });
187
- return tsLoad2;
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 loadTypescript2();
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 lineAt2(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: lineAt2(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, tsLoad2, 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
- tsLoad2 = 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 path8 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(path8.join(os.tmpdir(), prefix));
483
+ const scriptPath = path8.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(path8.join(os.tmpdir(), "ws-go-parse-"));
643
- scriptPath = path8.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 path9 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 loadTypescript2() {
1803
+ tsLoad2 ??= import("@typescript/typescript6").then((m) => {
1804
+ ts = m.default ?? m;
1805
+ return ts;
1806
+ });
1807
+ return tsLoad2;
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 loadTypescript2();
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, tsLoad2, kindMapCache;
2022
+ var init_ts_parser = __esm({
2023
+ "src/codebase-index/ts-parser.ts"() {
2024
+ "use strict";
2025
+ init_languages();
2026
+ tsLoad2 = 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 path10 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(path10.join(os3.tmpdir(), "ws-go-parse-"));
2141
+ scriptPath = path10.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 path10 from "node:path";
2592
+ import * as path11 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 = path10.basename(file).toLowerCase();
2604
+ const basename7 = path11.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: path10.basename(file),
2630
+ name: path11.basename(file),
1836
2631
  kind: "object",
1837
2632
  line,
1838
2633
  col: 0,
1839
- signature: `"${path10.basename(file)}" = { ... }`,
2634
+ signature: `"${path11.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 path11 from "node:path";
3744
+ import * as path12 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 = path11.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
3767
+ const wasmPath = path12.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 path11.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
3788
+ return path12.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
2662
3789
  }
2663
3790
  async function parseSymbols8(opts) {
2664
3791
  const { file, content, lang } = opts;
@@ -2674,89 +3801,229 @@ 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));
2678
- parser.delete();
2679
- tree.delete();
2680
- return { file, lang, symbols, refs: [], mtimeMs: Date.now() };
2681
- } catch {
2682
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2683
- }
2684
- }
2685
- async function loadTreeSitterLanguage(lang) {
2686
- const cached = await loadLanguage(lang);
2687
- return cached.Language;
2688
- }
2689
- async function __smokeRootType(opts) {
2690
- if (!isTreeSitterSupported(opts.lang)) {
2691
- throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
2692
- }
2693
- const { Parser } = await getRuntime();
2694
- const cached = await loadLanguage(opts.lang);
2695
- const parser = new Parser();
2696
- parser.setLanguage(cached.Language);
2697
- let tree = null;
2698
- try {
2699
- tree = parser.parse(opts.content);
2700
- if (!tree) throw new Error("tree-sitter: parser.parse returned null");
2701
- return tree.rootNode.type;
2702
- } finally {
2703
- tree?.delete();
2704
- parser.delete();
3804
+ const { symbols, refs } = visitTree(tree, content, file, lang, getQueries(lang));
3805
+ parser.delete();
3806
+ tree.delete();
3807
+ return { file, lang, symbols, refs, mtimeMs: Date.now() };
3808
+ } catch {
3809
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
3810
+ }
3811
+ }
3812
+ async function loadTreeSitterLanguage(lang) {
3813
+ const cached = await loadLanguage(lang);
3814
+ return cached.Language;
3815
+ }
3816
+ async function __smokeRootType(opts) {
3817
+ if (!isTreeSitterSupported(opts.lang)) {
3818
+ throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
3819
+ }
3820
+ const { Parser } = await getRuntime();
3821
+ const cached = await loadLanguage(opts.lang);
3822
+ const parser = new Parser();
3823
+ parser.setLanguage(cached.Language);
3824
+ let tree = null;
3825
+ try {
3826
+ tree = parser.parse(opts.content);
3827
+ if (!tree) throw new Error("tree-sitter: parser.parse returned null");
3828
+ return tree.rootNode.type;
3829
+ } finally {
3830
+ tree?.delete();
3831
+ parser.delete();
3832
+ }
3833
+ }
3834
+ async function parseTreeSitterAst(opts) {
3835
+ const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
3836
+ if (!grammar) return null;
3837
+ try {
3838
+ const { Parser, Language, init } = await getRuntime();
3839
+ await init();
3840
+ const wasmPath = path12.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
3841
+ const languageObj = await Language.load(wasmPath);
3842
+ const parser = new Parser();
3843
+ parser.setLanguage(languageObj);
3844
+ const tree = parser.parse(opts.content);
3845
+ if (!tree) {
3846
+ parser.delete();
3847
+ return null;
3848
+ }
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 = path12.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
+ }
2705
4007
  }
2706
4008
  }
2707
- async function parseTreeSitterAst(opts) {
2708
- const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
2709
- if (!grammar) return null;
2710
- try {
2711
- const { Parser, Language, init } = await getRuntime();
2712
- await init();
2713
- const wasmPath = path11.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
2714
- const languageObj = await Language.load(wasmPath);
2715
- const parser = new Parser();
2716
- parser.setLanguage(languageObj);
2717
- const tree = parser.parse(opts.content);
2718
- if (!tree) {
2719
- parser.delete();
2720
- return null;
2721
- }
2722
- return { tree, parser };
2723
- } catch {
2724
- return null;
4009
+ function withRelations(parsed, content, lang) {
4010
+ let refs = parsed.refs ?? [];
4011
+ if (refs.length === 0 && hasImportPatterns(lang)) {
4012
+ refs = extractImports({ content, lang });
2725
4013
  }
4014
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
2726
4015
  }
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"() {
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 = path11.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/edit.ts
2759
- import * as fs14 from "node:fs/promises";
4026
+ import * as fs15 from "node:fs/promises";
2760
4027
  import {
2761
4028
  atomicWrite,
2762
4029
  detectNewlineStyle,
@@ -3155,7 +4422,7 @@ function takeHeadBytes(s, maxBytes) {
3155
4422
  }
3156
4423
 
3157
4424
  // src/codebase-index/background-indexer.ts
3158
- import * as fs13 from "node:fs";
4425
+ import * as fs14 from "node:fs";
3159
4426
  import { fileURLToPath as fileURLToPath6 } from "node:url";
3160
4427
  import { Worker as Worker2 } from "node:worker_threads";
3161
4428
 
@@ -3243,9 +4510,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
3243
4510
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3244
4511
  import { execFile } from "node:child_process";
3245
4512
  import { createHash as createHash2 } from "node:crypto";
3246
- import * as fs9 from "node:fs/promises";
4513
+ import * as fs10 from "node:fs/promises";
3247
4514
  import { availableParallelism } from "node:os";
3248
- import * as path14 from "node:path";
4515
+ import * as path15 from "node:path";
3249
4516
  import {
3250
4517
  DEFAULT_WALK_IGNORE_DIRS,
3251
4518
  indexParallelBatchSize,
@@ -3913,283 +5180,96 @@ var ModuleResolver = class {
3913
5180
  const segments = spec.split("::").filter(Boolean);
3914
5181
  if (segments.length === 0) return void 0;
3915
5182
  const head = segments[0];
3916
- if (head === "self" || head === "super") {
3917
- let base = path6.posix.dirname(fromFile);
3918
- for (const segment of segments) {
3919
- if (segment === "super") base = path6.posix.dirname(base);
3920
- else if (segment !== "self") break;
3921
- }
3922
- const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
3923
- return this.lookupWithExtensions(path6.posix.join(base, ...rest2), "rs");
3924
- }
3925
- const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
3926
- const crate = head === "crate" ? owningCrate : this.structure.roots.find(
3927
- (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
3928
- );
3929
- if (!crate) {
3930
- return this.lookupWithExtensions(
3931
- path6.posix.join(path6.posix.dirname(fromFile), ...segments),
3932
- "rs"
3933
- );
3934
- }
3935
- const rest = segments.slice(1);
3936
- for (const base of crate.sourceRoots) {
3937
- const parent = rest.length > 1 ? this.lookupWithExtensions(path6.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
3938
- const exact = this.lookupWithExtensions(path6.posix.join(base, ...rest), "rs");
3939
- const hit = exact ?? parent ?? this.lookupWithExtensions(path6.posix.join(base, "lib"), "rs");
3940
- if (hit) return hit;
3941
- }
3942
- return void 0;
3943
- }
3944
- /** `com.example.Thing` and `com.example.*` against JVM source roots. */
3945
- resolveJvm(spec) {
3946
- const segments = spec.split(".").filter(Boolean);
3947
- if (segments.length === 0) return void 0;
3948
- const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
3949
- const wildcard = segments[segments.length - 1] === "*";
3950
- const parts = wildcard ? segments.slice(0, -1) : segments;
3951
- for (const base of [...sourceRoots, this.structure.projectRoot]) {
3952
- const target = path6.posix.join(base, ...parts);
3953
- const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
3954
- if (hit) return hit;
3955
- }
3956
- return void 0;
3957
- }
3958
- /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
3959
- resolveInclude(fromFile, spec) {
3960
- const relative3 = this.lookupWithExtensions(
3961
- path6.posix.join(path6.posix.dirname(fromFile), spec),
3962
- "c"
3963
- );
3964
- if (relative3) return relative3;
3965
- for (const base of [
3966
- path6.posix.join(this.structure.projectRoot, "include"),
3967
- this.structure.projectRoot
3968
- ]) {
3969
- const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "c");
3970
- if (hit) return hit;
3971
- }
3972
- return void 0;
3973
- }
3974
- /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
3975
- resolveRuby(fromFile, spec) {
3976
- const relative3 = this.lookupWithExtensions(
3977
- path6.posix.join(path6.posix.dirname(fromFile), spec),
3978
- "ruby"
3979
- );
3980
- if (relative3) return relative3;
3981
- for (const base of [
3982
- path6.posix.join(this.structure.projectRoot, "lib"),
3983
- this.structure.projectRoot
3984
- ]) {
3985
- const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "ruby");
3986
- if (hit) return hit;
3987
- }
3988
- return void 0;
3989
- }
3990
- };
3991
-
3992
- // src/codebase-index/import-extractor.ts
3993
- var IMPORT_MAX_FILE_CHARS = 512 * 1024;
3994
- var IMPORT_MAX_PER_FILE = 400;
3995
- var DOTTED_IMPORT = [
3996
- { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
3997
- ];
3998
- var LANG_IMPORTS = {
3999
- // Go and Python have real AST extractors; these patterns are the fallback for
4000
- // machines with no Go toolchain or Python interpreter installed, where the
4001
- // parser degrades to regex symbols and would otherwise contribute no edges.
4002
- go: [
4003
- { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
4004
- // Grouped form: inside `import ( … )` each line is an optional alias plus a
4005
- // quoted path. A stray match elsewhere resolves to no file and is dropped.
4006
- { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
4007
- ],
4008
- py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
4009
- rs: [
4010
- // use a::b::C; | use a::b::{C, D}; → the path before any brace
4011
- { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
4012
- // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
4013
- { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
4014
- ],
4015
- java: DOTTED_IMPORT,
4016
- kotlin: DOTTED_IMPORT,
4017
- scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
4018
- csharp: [
4019
- // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
4020
- { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
4021
- { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
4022
- ],
4023
- // Quoted includes only: <stdio.h> is a system header with no indexed file.
4024
- c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
4025
- cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
4026
- ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
4027
- php: [
4028
- // `use A\B\C` imports the class C, which is what the index has a symbol
4029
- // for — the namespace symbol only covers the `A\B` prefix.
4030
- { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
4031
- { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
4032
- ],
4033
- swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
4034
- dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
4035
- lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
4036
- elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
4037
- haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
4038
- zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
4039
- proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
4040
- // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
4041
- css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
4042
- // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
4043
- vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
4044
- svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
4045
- html: [
4046
- { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
4047
- { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
4048
- ],
4049
- shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
4050
- r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
4051
- };
4052
- function lastSegment(specifier) {
4053
- const pathLike = /[/\\]|::/.test(specifier);
4054
- const segments = specifier.split(/[/\\]|::/).filter(Boolean);
4055
- let last = segments[segments.length - 1] ?? specifier;
4056
- if (last === "*" || last === "_") {
4057
- last = segments[segments.length - 2] ?? specifier;
4058
- }
4059
- if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
4060
- const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
4061
- return dotted[dotted.length - 1] ?? last;
4062
- }
4063
- function newlineOffsets(content) {
4064
- const offsets = [];
4065
- for (let i = 0; i < content.length; i++) {
4066
- if (content.charCodeAt(i) === 10) offsets.push(i);
4067
- }
4068
- return offsets;
4069
- }
4070
- function lineAt2(offsets, index) {
4071
- let low = 0;
4072
- let high = offsets.length;
4073
- while (low < high) {
4074
- const mid = low + high >>> 1;
4075
- if ((offsets[mid] ?? 0) < index) low = mid + 1;
4076
- else high = mid;
4077
- }
4078
- return low + 1;
4079
- }
4080
- function hasImportPatterns(lang) {
4081
- return LANG_IMPORTS[lang] !== void 0;
4082
- }
4083
- function extractImports(opts) {
4084
- const patterns = LANG_IMPORTS[opts.lang];
4085
- if (!patterns || !opts.content) return [];
4086
- const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
4087
- const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
4088
- const refs = [];
4089
- const seen = /* @__PURE__ */ new Set();
4090
- const offsets = newlineOffsets(content);
4091
- for (const pattern of patterns) {
4092
- const re = new RegExp(pattern.re.source, pattern.re.flags);
4093
- for (const match of content.matchAll(re)) {
4094
- if (refs.length >= limit) return refs;
4095
- const specifier = match[1]?.trim();
4096
- if (!specifier) continue;
4097
- const module = specifier;
4098
- const toName = pattern.name === "full" ? module : lastSegment(module);
4099
- if (!toName) continue;
4100
- const key = `${module}\0${toName}`;
4101
- if (seen.has(key)) continue;
4102
- seen.add(key);
4103
- refs.push({
4104
- fromId: 0,
4105
- toName,
4106
- callType: "import",
4107
- line: lineAt2(offsets, match.index ?? 0),
4108
- lang: opts.lang,
4109
- module
4110
- });
4111
- }
4112
- }
4113
- return refs;
4114
- }
4115
-
4116
- // src/codebase-index/parser-dispatch.ts
4117
- async function parseFileContent(file, content, lang) {
4118
- const parsed = await dispatch(file, content, lang);
4119
- return withRelations(parsed, content, lang);
4120
- }
4121
- async function dispatch(file, content, lang) {
4122
- switch (lang) {
4123
- case "ts":
4124
- case "tsx":
4125
- case "js":
4126
- case "jsx": {
4127
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
4128
- return parseSymbols9({ file, content, lang });
4129
- }
4130
- case "go": {
4131
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
4132
- return parseSymbols9({ file, content, lang: "go" });
4133
- }
4134
- case "py": {
4135
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
4136
- return parseSymbols9({ file, content, lang: "py" });
4137
- }
4138
- case "rs": {
4139
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
4140
- return parseSymbols9({ file, content, lang: "rs" });
5183
+ if (head === "self" || head === "super") {
5184
+ let base = path6.posix.dirname(fromFile);
5185
+ for (const segment of segments) {
5186
+ if (segment === "super") base = path6.posix.dirname(base);
5187
+ else if (segment !== "self") break;
5188
+ }
5189
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
5190
+ return this.lookupWithExtensions(path6.posix.join(base, ...rest2), "rs");
4141
5191
  }
4142
- case "json": {
4143
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
4144
- return parseSymbols9({ file, content, lang: "json" });
5192
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
5193
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
5194
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
5195
+ );
5196
+ if (!crate) {
5197
+ return this.lookupWithExtensions(
5198
+ path6.posix.join(path6.posix.dirname(fromFile), ...segments),
5199
+ "rs"
5200
+ );
4145
5201
  }
4146
- case "yaml": {
4147
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
4148
- return parseSymbols9({ file, content, lang: "yaml" });
5202
+ const rest = segments.slice(1);
5203
+ for (const base of crate.sourceRoots) {
5204
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path6.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
5205
+ const exact = this.lookupWithExtensions(path6.posix.join(base, ...rest), "rs");
5206
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path6.posix.join(base, "lib"), "rs");
5207
+ if (hit) return hit;
4149
5208
  }
4150
- // Phase 1: ten languages now route through the Tree-Sitter WASM
4151
- // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
4152
- // the regex extractor in `generic-parser.ts` whenever WASM loading fails
4153
- // or the parser returns zero symbols — preserving the indexable-file
4154
- // contract that "missing a parser must never mean skipping the file".
4155
- case "c":
4156
- case "cpp":
4157
- case "java":
4158
- case "csharp":
4159
- case "php":
4160
- case "ruby":
4161
- case "swift":
4162
- case "kotlin":
4163
- case "shell":
4164
- case "elixir": {
4165
- try {
4166
- const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
4167
- const parsed = await parseSymbols10({ file, content, lang });
4168
- if (parsed.symbols.length > 0) return parsed;
4169
- } catch {
4170
- }
4171
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4172
- return parseSymbols9({ file, content, lang });
5209
+ return void 0;
5210
+ }
5211
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
5212
+ resolveJvm(spec) {
5213
+ const segments = spec.split(".").filter(Boolean);
5214
+ if (segments.length === 0) return void 0;
5215
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
5216
+ const wildcard = segments[segments.length - 1] === "*";
5217
+ const parts = wildcard ? segments.slice(0, -1) : segments;
5218
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
5219
+ const target = path6.posix.join(base, ...parts);
5220
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
5221
+ if (hit) return hit;
4173
5222
  }
4174
- default: {
4175
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4176
- return parseSymbols9({ file, content, lang });
5223
+ return void 0;
5224
+ }
5225
+ /** `#include "foo/bar.h"` quoted form only; `<…>` is a system header. */
5226
+ resolveInclude(fromFile, spec) {
5227
+ const relative3 = this.lookupWithExtensions(
5228
+ path6.posix.join(path6.posix.dirname(fromFile), spec),
5229
+ "c"
5230
+ );
5231
+ if (relative3) return relative3;
5232
+ for (const base of [
5233
+ path6.posix.join(this.structure.projectRoot, "include"),
5234
+ this.structure.projectRoot
5235
+ ]) {
5236
+ const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "c");
5237
+ if (hit) return hit;
4177
5238
  }
5239
+ return void 0;
4178
5240
  }
4179
- }
4180
- function withRelations(parsed, content, lang) {
4181
- let refs = parsed.refs ?? [];
4182
- if (refs.length === 0 && hasImportPatterns(lang)) {
4183
- refs = extractImports({ content, lang });
5241
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
5242
+ resolveRuby(fromFile, spec) {
5243
+ const relative3 = this.lookupWithExtensions(
5244
+ path6.posix.join(path6.posix.dirname(fromFile), spec),
5245
+ "ruby"
5246
+ );
5247
+ if (relative3) return relative3;
5248
+ for (const base of [
5249
+ path6.posix.join(this.structure.projectRoot, "lib"),
5250
+ this.structure.projectRoot
5251
+ ]) {
5252
+ const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "ruby");
5253
+ if (hit) return hit;
5254
+ }
5255
+ return void 0;
4184
5256
  }
4185
- return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
4186
- }
5257
+ };
5258
+
5259
+ // src/codebase-index/indexer.ts
5260
+ init_parser_dispatch();
4187
5261
 
4188
5262
  // src/codebase-index/parser-worker-pool.ts
5263
+ import * as fs7 from "node:fs";
5264
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
4189
5265
  import { Worker } from "node:worker_threads";
4190
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4191
- import * as fs6 from "node:fs";
4192
5266
  var WORKER_POOL_THRESHOLD = 500;
5267
+ function resolveWorkerPoolThreshold() {
5268
+ const raw = process.env["WRONGSTACK_INDEX_WORKER_THRESHOLD"];
5269
+ if (raw === void 0) return WORKER_POOL_THRESHOLD;
5270
+ if (!/^\d+$/.test(raw)) return WORKER_POOL_THRESHOLD;
5271
+ return Number.parseInt(raw, 10);
5272
+ }
4193
5273
  var ParserWorkerPool = class {
4194
5274
  constructor(maxWorkers = defaultWorkerCount()) {
4195
5275
  this.maxWorkers = maxWorkers;
@@ -4233,7 +5313,8 @@ var ParserWorkerPool = class {
4233
5313
  w.unref();
4234
5314
  w.on("message", (msg) => this.handleMessage(msg));
4235
5315
  w.on("error", (err) => this.handleError(err, w));
4236
- this.workers.push({ worker: w, busy: false });
5316
+ w.on("exit", () => this.retireByReference(w));
5317
+ this.workers.push({ worker: w, workerId: w.threadId, busy: false });
4237
5318
  } catch {
4238
5319
  if (this.workers.length === 0) {
4239
5320
  this.unavailable = true;
@@ -4249,7 +5330,7 @@ var ParserWorkerPool = class {
4249
5330
  }
4250
5331
  /**
4251
5332
  * Parse files in parallel across the worker pool. Returns a flat
4252
- * `FileSymbols[]` in completion order (caller sorts if needed).
5333
+ * `FileSymbols[]` in completion order (caller matches by file path).
4253
5334
  *
4254
5335
  * Content is pre-read by the main thread (for the content-hash check)
4255
5336
  * and passed to workers to avoid a second disk read. Files are
@@ -4270,21 +5351,19 @@ var ParserWorkerPool = class {
4270
5351
  chunks[i % workerCount].push(files[i]);
4271
5352
  }
4272
5353
  return new Promise((resolve4, reject) => {
5354
+ const pendingChunks = /* @__PURE__ */ new Map();
4273
5355
  this.pending.set(batchId, {
4274
5356
  resolve: resolve4,
4275
5357
  reject,
4276
5358
  accumulated: [],
4277
- expectedWorkers: workerCount,
4278
- completedWorkers: 0
5359
+ pendingChunks,
5360
+ settled: false
4279
5361
  });
4280
5362
  for (let i = 0; i < workerCount; i++) {
4281
5363
  const pw = this.workers[i];
4282
5364
  pw.busy = true;
4283
- pw.worker.postMessage({
4284
- type: "parse",
4285
- id: batchId,
4286
- files: chunks[i]
4287
- });
5365
+ pendingChunks.set(pw.workerId, chunks[i]);
5366
+ pw.worker.postMessage({ type: "parse", id: batchId, files: chunks[i] });
4288
5367
  }
4289
5368
  });
4290
5369
  }
@@ -4293,6 +5372,12 @@ var ParserWorkerPool = class {
4293
5372
  const workers = this.workers.map((w) => w.worker);
4294
5373
  this.workers = [];
4295
5374
  this.unavailable = false;
5375
+ for (const [, p] of this.pending) {
5376
+ if (p.settled) continue;
5377
+ p.settled = true;
5378
+ p.reject(new Error("ParserWorkerPool shut down"));
5379
+ }
5380
+ this.pending.clear();
4296
5381
  for (const w of workers) {
4297
5382
  try {
4298
5383
  w.postMessage({ type: "shutdown" });
@@ -4313,39 +5398,117 @@ var ParserWorkerPool = class {
4313
5398
  })
4314
5399
  )
4315
5400
  );
4316
- for (const [, p] of this.pending) p.reject(new Error("ParserWorkerPool shut down"));
4317
- this.pending.clear();
4318
5401
  }
4319
5402
  handleMessage(msg) {
4320
5403
  const batch = this.pending.get(msg.id);
4321
5404
  if (!batch) return;
5405
+ const worker2 = this.workers.find((w) => w.workerId === msg.workerId);
5406
+ if (worker2) worker2.busy = false;
5407
+ batch.pendingChunks.delete(msg.workerId);
4322
5408
  batch.accumulated.push(...msg.results);
4323
- batch.completedWorkers++;
4324
- const freeWorker = this.workers.find((w) => w.busy);
4325
- if (freeWorker) freeWorker.busy = false;
4326
- if (batch.completedWorkers >= batch.expectedWorkers) {
5409
+ if (batch.pendingChunks.size === 0) {
5410
+ if (batch.settled) return;
5411
+ batch.settled = true;
4327
5412
  this.pending.delete(msg.id);
4328
5413
  batch.resolve(batch.accumulated);
4329
5414
  }
4330
5415
  }
4331
- handleError(err, source) {
4332
- this.workers = this.workers.filter((w) => w.worker !== source);
4333
- if (this.workers.length === 0) {
4334
- for (const [, p] of this.pending) p.reject(err);
4335
- this.pending.clear();
4336
- this.unavailable = true;
5416
+ /**
5417
+ * Remove a worker from the pool and salvage any chunk it still owed.
5418
+ *
5419
+ * Idempotent by workerId `error` and `exit` can both fire for one
5420
+ * death, and a worker may die while no batch references it. When the
5421
+ * dead worker owed files to an in-flight batch and other workers remain,
5422
+ * those files are re-parsed inline on this thread (one fewer worker
5423
+ * should cost latency, not correctness). When it was the last worker,
5424
+ * every remaining batch rejects so the indexer's existing inline
5425
+ * fallback takes over the whole batch.
5426
+ */
5427
+ retireWorker(workerId) {
5428
+ const entry = this.workers.find((w) => w.workerId === workerId);
5429
+ if (!entry) return;
5430
+ this.workers = this.workers.filter((w) => w.workerId !== workerId);
5431
+ for (const [batchId, batch] of [...this.pending]) {
5432
+ const orphaned = batch.pendingChunks.get(workerId);
5433
+ if (!orphaned) continue;
5434
+ if (this.workers.length === 0) {
5435
+ this.pending.delete(batchId);
5436
+ this.unavailable = true;
5437
+ if (!batch.settled) {
5438
+ batch.settled = true;
5439
+ batch.reject(new Error("ParserWorkerPool: all workers died mid-batch"));
5440
+ }
5441
+ continue;
5442
+ }
5443
+ void this.reparseInline(batch, orphaned, workerId);
5444
+ }
5445
+ }
5446
+ /**
5447
+ * Salvage path: re-parse an orphaned chunk on this thread. Files that
5448
+ * fail here stay absent from the results — same contract as a per-file
5449
+ * error inside a live worker (see handleMessage).
5450
+ */
5451
+ async reparseInline(batch, orphaned, workerId) {
5452
+ try {
5453
+ const { parseFileContent: parseFileContent2 } = await Promise.resolve().then(() => (init_parser_dispatch(), parser_dispatch_exports));
5454
+ for (const item of orphaned) {
5455
+ if (batch.settled) return;
5456
+ try {
5457
+ const parsed = await parseFileContent2(item.file, item.content, item.lang);
5458
+ batch.accumulated.push(parsed);
5459
+ } catch {
5460
+ }
5461
+ await new Promise((resolve4) => setImmediate(resolve4));
5462
+ }
5463
+ } finally {
5464
+ this.finishSalvage(batch, workerId);
5465
+ }
5466
+ }
5467
+ /**
5468
+ * Terminal tail of a salvage — runs on every exit path. Kept free of
5469
+ * control flow inside a `finally` (noUnsafeFinally): releases the
5470
+ * pending-marker and resolves the batch if this was its last chunk.
5471
+ */
5472
+ finishSalvage(batch, workerId) {
5473
+ batch.pendingChunks.delete(workerId);
5474
+ if (batch.pendingChunks.size === 0) {
5475
+ for (const [batchId, tracked] of this.pending) {
5476
+ if (tracked === batch) {
5477
+ if (batch.settled) return;
5478
+ batch.settled = true;
5479
+ this.pending.delete(batchId);
5480
+ batch.resolve(batch.accumulated);
5481
+ return;
5482
+ }
5483
+ }
4337
5484
  }
4338
5485
  }
5486
+ /**
5487
+ * Retire by worker object rather than threadId. `threadId` is -1 before
5488
+ * the worker emits `online`, so a death during script load would make a
5489
+ * threadId-keyed lookup silently no-op and leak the entry (with its
5490
+ * pending chunk) — reference identity is correct in every case.
5491
+ */
5492
+ retireByReference(source) {
5493
+ const entry = this.workers.find((w) => w.worker === source);
5494
+ if (entry) this.retireWorker(entry.workerId);
5495
+ }
5496
+ handleError(err, source) {
5497
+ void err;
5498
+ this.retireByReference(source);
5499
+ }
4339
5500
  };
4340
5501
  function defaultWorkerCount() {
4341
5502
  const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
4342
5503
  return Math.max(1, Math.min(4, cores - 1));
4343
5504
  }
4344
5505
  function resolveWorkerScriptUrl() {
5506
+ const override = process.env["WRONGSTACK_PARSER_WORKER_SCRIPT"];
5507
+ if (override) return pathToFileURL(override);
4345
5508
  for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
4346
5509
  try {
4347
5510
  const url = new URL(rel, import.meta.url);
4348
- if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
5511
+ if (url.protocol === "file:" && fs7.existsSync(fileURLToPath2(url))) return url;
4349
5512
  } catch {
4350
5513
  }
4351
5514
  }
@@ -4358,8 +5521,8 @@ function getParserPool() {
4358
5521
  }
4359
5522
 
4360
5523
  // src/codebase-index/writer.ts
4361
- import * as fs8 from "node:fs";
4362
- import * as path13 from "node:path";
5524
+ import * as fs9 from "node:fs";
5525
+ import * as path14 from "node:path";
4363
5526
 
4364
5527
  // src/codebase-index/bm25.ts
4365
5528
  var K1 = 1.5;
@@ -4454,11 +5617,11 @@ var Bm25Index = class {
4454
5617
  init_languages();
4455
5618
 
4456
5619
  // src/codebase-index/schema.ts
4457
- var SCHEMA_VERSION = 4;
5620
+ var SCHEMA_VERSION = 5;
4458
5621
 
4459
5622
  // src/codebase-index/sqlite-runtime.ts
4460
- import { createRequire } from "node:module";
4461
5623
  import { toErrorMessage } from "@wrongstack/core/utils";
5624
+ import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
4462
5625
  var warningSilenced = false;
4463
5626
  function silenceSqliteExperimentalWarning() {
4464
5627
  if (warningSilenced) return;
@@ -4476,11 +5639,10 @@ function loadDatabaseSync() {
4476
5639
  if (DatabaseSyncCtor) return DatabaseSyncCtor;
4477
5640
  silenceSqliteExperimentalWarning();
4478
5641
  try {
4479
- const req = createRequire(import.meta.url);
4480
- DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
5642
+ DatabaseSyncCtor = loadRuntimeDatabaseSync();
4481
5643
  } catch (err) {
4482
5644
  throw new Error(
4483
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
5645
+ `The codebase index needs node:sqlite (Node >= 22.5) or bun:sqlite. This runtime doesn't provide it: ${toErrorMessage(err)}`
4484
5646
  );
4485
5647
  }
4486
5648
  return DatabaseSyncCtor;
@@ -4523,9 +5685,87 @@ function runSqliteWithRetry(fn) {
4523
5685
  throw lastError;
4524
5686
  }
4525
5687
 
5688
+ // src/codebase-index/vector-search.ts
5689
+ var RRF_K = 60;
5690
+ function vectorEmbeddingEnabled() {
5691
+ return process.env["WRONGSTACK_INDEX_VECTORS"] === "1";
5692
+ }
5693
+ var VECTOR_DIMENSIONS = 384;
5694
+ var NGRAM_SIZE = 3;
5695
+ function embedText(text) {
5696
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
5697
+ const normalized = text.toLowerCase().trim();
5698
+ if (normalized.length < NGRAM_SIZE) {
5699
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5700
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5701
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
5702
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5703
+ vec[bucket] += 1;
5704
+ }
5705
+ } else {
5706
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5707
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
5708
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5709
+ vec[bucket] += 1;
5710
+ }
5711
+ }
5712
+ let norm = 0;
5713
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5714
+ norm += vec[i] * vec[i];
5715
+ }
5716
+ norm = Math.sqrt(norm);
5717
+ if (norm > 0) {
5718
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5719
+ vec[i] /= norm;
5720
+ }
5721
+ }
5722
+ return vec;
5723
+ }
5724
+ function hashNgram(str) {
5725
+ let hash = 2166136261;
5726
+ for (let i = 0; i < str.length; i++) {
5727
+ hash ^= str.charCodeAt(i);
5728
+ hash = Math.imul(hash, 16777619);
5729
+ }
5730
+ return hash >>> 0;
5731
+ }
5732
+ function cosineSimilarity(a, b) {
5733
+ let dot = 0;
5734
+ const len = Math.min(a.length, b.length);
5735
+ for (let i = 0; i < len; i++) {
5736
+ dot += a[i] * b[i];
5737
+ }
5738
+ return dot;
5739
+ }
5740
+ function encodeVector(vec) {
5741
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5742
+ }
5743
+ function decodeVector(buf) {
5744
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5745
+ const copy = new Float32Array(buf.byteLength / 4);
5746
+ for (let i = 0; i < copy.length; i++) {
5747
+ copy[i] = view.getFloat32(i * 4, true);
5748
+ }
5749
+ return copy;
5750
+ }
5751
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5752
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5753
+ const scored = [];
5754
+ for (const id of allIds) {
5755
+ const bm25Rank = bm25Ranks.get(id);
5756
+ const vecRank = vectorRanks.get(id);
5757
+ let score = 0;
5758
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5759
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5760
+ scored.push([id, score]);
5761
+ }
5762
+ scored.sort((a, b) => b[1] - a[1]);
5763
+ return scored;
5764
+ }
5765
+
4526
5766
  // src/codebase-index/writer-admin.ts
4527
- import * as fs7 from "node:fs";
4528
- import * as path12 from "node:path";
5767
+ import * as fs8 from "node:fs";
5768
+ import * as path13 from "node:path";
4529
5769
  var DB_FILE = "index.db";
4530
5770
  function getAllIndexableWithStatement(stmt) {
4531
5771
  return stmt("SELECT id, text FROM symbols").all().map(
@@ -4561,6 +5801,14 @@ function getMetadataWithStatement(stmt, key) {
4561
5801
  const rows = stmt("SELECT value FROM metadata WHERE key = ?").all(key);
4562
5802
  return rows[0]?.value;
4563
5803
  }
5804
+ function getIndexSummaryWithStatement(stmt) {
5805
+ const fileRows = stmt("SELECT COUNT(*) AS n FROM files").all();
5806
+ const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
5807
+ return {
5808
+ totalFiles: fileRows[0] ? Number(fileRows[0].n) : 0,
5809
+ lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null
5810
+ };
5811
+ }
4564
5812
  function getFileMetaWithStatement(stmt, file) {
4565
5813
  const rows = stmt(
4566
5814
  "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
@@ -4590,22 +5838,102 @@ function getAllFileMetasWithStatement(stmt) {
4590
5838
  }
4591
5839
  function getIndexDbSizeBytes(indexDir) {
4592
5840
  try {
4593
- return fs7.statSync(path12.join(indexDir, DB_FILE)).size;
5841
+ return fs8.statSync(path13.join(indexDir, DB_FILE)).size;
4594
5842
  } catch {
4595
5843
  return 0;
4596
5844
  }
4597
5845
  }
4598
5846
 
5847
+ // src/codebase-index/writer-helpers.ts
5848
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
5849
+ function escapeLike(value) {
5850
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
5851
+ }
5852
+ function ladderChunkSizes(total, max) {
5853
+ if (total <= 0) return [];
5854
+ const sizes = [];
5855
+ let remaining = total;
5856
+ while (remaining > 0) {
5857
+ const cap = Math.min(remaining, max);
5858
+ const pow = cap >= 1 ? 2 ** Math.floor(Math.log2(cap)) : 1;
5859
+ const take = Math.max(1, Math.min(pow, remaining));
5860
+ sizes.push(take);
5861
+ remaining -= take;
5862
+ }
5863
+ return sizes;
5864
+ }
5865
+ function nextPow2(count) {
5866
+ return count <= 1 ? 1 : 2 ** Math.ceil(Math.log2(count));
5867
+ }
5868
+ function padToInBucket(values) {
5869
+ if (values.length <= 1) return values.slice();
5870
+ const target = nextPow2(values.length);
5871
+ const padded = values.slice();
5872
+ while (padded.length < target) padded.push(padded[0]);
5873
+ return padded;
5874
+ }
5875
+ function placeholders(count) {
5876
+ return Array.from({ length: count }, () => "?").join(",");
5877
+ }
5878
+ function inListChunks(total, max) {
5879
+ if (total <= 0) return [];
5880
+ if (nextPow2(total) <= max) return [total];
5881
+ const powMax = Math.max(1, 2 ** Math.floor(Math.log2(max)));
5882
+ return ladderChunkSizes(total, powMax);
5883
+ }
5884
+ function posixIndexPath(file) {
5885
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
5886
+ }
5887
+ function indexedFileMatchSql(column = "file") {
5888
+ return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
5889
+ }
5890
+ function indexedFileMatchArgs(file) {
5891
+ const posix4 = posixIndexPath(file.trim());
5892
+ return [file, posix4, `%/${escapeLike(posix4)}`];
5893
+ }
5894
+ function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
5895
+ if (packageLabel === filter) return true;
5896
+ const posixFile = posixIndexPath(storedFile);
5897
+ const posixFilter = posixIndexPath(filter.trim());
5898
+ if (!posixFilter) return false;
5899
+ return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
5900
+ }
5901
+ function assignRefsToSymbols(refs, symbols) {
5902
+ if (refs.length === 0 || symbols.length === 0) return [];
5903
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
5904
+ const seen = /* @__PURE__ */ new Set();
5905
+ const assigned = [];
5906
+ for (const ref of refs) {
5907
+ let owner;
5908
+ for (const symbol of ordered) {
5909
+ if (symbol.line > ref.line) break;
5910
+ owner = symbol;
5911
+ }
5912
+ if (!owner && ref.callType === "import") owner = ordered[0];
5913
+ if (!owner || owner.id <= 0) continue;
5914
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5915
+ if (seen.has(key)) continue;
5916
+ seen.add(key);
5917
+ assigned.push({ ...ref, fromId: owner.id });
5918
+ }
5919
+ return assigned;
5920
+ }
5921
+ function resolveIndexDir(projectRoot, override) {
5922
+ return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
5923
+ }
5924
+
4599
5925
  // src/codebase-index/writer-bulk-insert.ts
4600
5926
  function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4601
5927
  if (rows.length === 0) return;
4602
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 12));
4603
- for (let i = 0; i < rows.length; i += chunkSize) {
4604
- const chunk = rows.slice(i, i + chunkSize);
4605
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5928
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 12)));
5929
+ let cursor = 0;
5930
+ for (const take of ladder) {
5931
+ const chunk = rows.slice(cursor, cursor + take);
5932
+ cursor += take;
5933
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4606
5934
  const insert = stmt(
4607
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
4608
- VALUES ${placeholders}`
5935
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text)
5936
+ VALUES ${placeholders2}`
4609
5937
  );
4610
5938
  const binds = [];
4611
5939
  for (const r of chunk) {
@@ -4620,8 +5948,7 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4620
5948
  r.signature,
4621
5949
  r.docComment,
4622
5950
  r.scope,
4623
- r.text,
4624
- r.file
5951
+ r.text
4625
5952
  );
4626
5953
  }
4627
5954
  insert.run(...binds);
@@ -4629,11 +5956,13 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4629
5956
  }
4630
5957
  function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4631
5958
  if (!ftsAvailable || rows.length === 0) return;
4632
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4633
- for (let i = 0; i < rows.length; i += chunkSize) {
4634
- const chunk = rows.slice(i, i + chunkSize);
4635
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4636
- const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
5959
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5960
+ let cursor = 0;
5961
+ for (const take of ladder) {
5962
+ const chunk = rows.slice(cursor, cursor + take);
5963
+ cursor += take;
5964
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5965
+ const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders2}`);
4637
5966
  const binds = [];
4638
5967
  for (const r of chunk) binds.push(r.id, r.text);
4639
5968
  insert.run(...binds);
@@ -4641,11 +5970,13 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4641
5970
  }
4642
5971
  function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4643
5972
  if (rows.length === 0) return;
4644
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4645
- for (let i = 0; i < rows.length; i += chunkSize) {
4646
- const chunk = rows.slice(i, i + chunkSize);
4647
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4648
- const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders}`);
5973
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5974
+ let cursor = 0;
5975
+ for (const take of ladder) {
5976
+ const chunk = rows.slice(cursor, cursor + take);
5977
+ cursor += take;
5978
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5979
+ const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders2}`);
4649
5980
  const binds = [];
4650
5981
  for (const r of chunk) binds.push(r.id, r.vector);
4651
5982
  insert.run(...binds);
@@ -4653,13 +5984,15 @@ function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4653
5984
  }
4654
5985
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
4655
5986
  if (refs.length === 0) return;
4656
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
4657
- for (let i = 0; i < refs.length; i += chunkSize) {
4658
- const chunk = refs.slice(i, i + chunkSize);
4659
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5987
+ const ladder = ladderChunkSizes(refs.length, Math.max(1, Math.floor(maxSqlVars / 8)));
5988
+ let cursor = 0;
5989
+ for (const take of ladder) {
5990
+ const chunk = refs.slice(cursor, cursor + take);
5991
+ cursor += take;
5992
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4660
5993
  const insert = stmt(
4661
5994
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
4662
- VALUES ${placeholders}`
5995
+ VALUES ${placeholders2}`
4663
5996
  );
4664
5997
  const binds = [];
4665
5998
  for (const ref of chunk) {
@@ -4813,52 +6146,6 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
4813
6146
  return edges;
4814
6147
  }
4815
6148
 
4816
- // src/codebase-index/writer-helpers.ts
4817
- import { resolveWstackPaths } from "@wrongstack/core/utils";
4818
- function escapeLike(value) {
4819
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
4820
- }
4821
- function posixIndexPath(file) {
4822
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
4823
- }
4824
- function indexedFileMatchSql(column = "file") {
4825
- return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
4826
- }
4827
- function indexedFileMatchArgs(file) {
4828
- const posix4 = posixIndexPath(file.trim());
4829
- return [file, posix4, `%/${escapeLike(posix4)}`];
4830
- }
4831
- function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
4832
- if (packageLabel === filter) return true;
4833
- const posixFile = posixIndexPath(storedFile);
4834
- const posixFilter = posixIndexPath(filter.trim());
4835
- if (!posixFilter) return false;
4836
- return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
4837
- }
4838
- function assignRefsToSymbols(refs, symbols) {
4839
- if (refs.length === 0 || symbols.length === 0) return [];
4840
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4841
- const seen = /* @__PURE__ */ new Set();
4842
- const assigned = [];
4843
- for (const ref of refs) {
4844
- let owner;
4845
- for (const symbol of ordered) {
4846
- if (symbol.line > ref.line) break;
4847
- owner = symbol;
4848
- }
4849
- if (!owner && ref.callType === "import") owner = ordered[0];
4850
- if (!owner || owner.id <= 0) continue;
4851
- const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4852
- if (seen.has(key)) continue;
4853
- seen.add(key);
4854
- assigned.push({ ...ref, fromId: owner.id });
4855
- }
4856
- return assigned;
4857
- }
4858
- function resolveIndexDir(projectRoot, override) {
4859
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4860
- }
4861
-
4862
6149
  // src/codebase-index/writer-ref-mapper.ts
4863
6150
  function mapWriterRefRow(row) {
4864
6151
  return {
@@ -4880,20 +6167,20 @@ function mapWriterRefRow(row) {
4880
6167
  var MAX_SQL_VARS = 900;
4881
6168
  function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
4882
6169
  const results = [];
4883
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4884
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4885
- const placeholders = chunk.map(() => "?").join(",");
4886
- const sql = buildSql(placeholders);
6170
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
6171
+ const chunk = padToInBucket(ids.slice(0, take));
6172
+ ids = ids.slice(take);
6173
+ const sql = buildSql(placeholders(chunk.length));
4887
6174
  results.push(...stmt(sql).all(...chunk, ...extraArgs));
4888
6175
  }
4889
6176
  return results;
4890
6177
  }
4891
6178
  function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
4892
6179
  let total = 0;
4893
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4894
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4895
- const placeholders = chunk.map(() => "?").join(",");
4896
- const sql = buildSql(placeholders);
6180
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
6181
+ const chunk = padToInBucket(ids.slice(0, take));
6182
+ ids = ids.slice(take);
6183
+ const sql = buildSql(placeholders(chunk.length));
4897
6184
  const rows = stmt(sql).all(...chunk, ...extraArgs);
4898
6185
  total += rows[0]?.n ?? 0;
4899
6186
  }
@@ -4922,16 +6209,24 @@ function resolveIndexedFiles(stmt, file) {
4922
6209
  }
4923
6210
  function resolveSymbolIds(stmt, symbolName, file) {
4924
6211
  if (!file) {
4925
- const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
4926
- return rows2.map((r) => r.id);
6212
+ const rows = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(
6213
+ symbolName
6214
+ );
6215
+ return rows.map((r) => r.id);
4927
6216
  }
4928
6217
  const indexedFiles = resolveIndexedFiles(stmt, file);
4929
6218
  if (indexedFiles.length === 0) return [];
4930
- const placeholders = indexedFiles.map(() => "?").join(",");
4931
- const rows = stmt(
4932
- `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
4933
- ).all(symbolName, ...indexedFiles);
4934
- return rows.map((r) => r.id);
6219
+ const ids = [];
6220
+ let cursor = 0;
6221
+ for (const take of inListChunks(indexedFiles.length, MAX_SQL_VARS)) {
6222
+ const files = padToInBucket(indexedFiles.slice(cursor, cursor + take));
6223
+ cursor += take;
6224
+ const rows = stmt(
6225
+ `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders(files.length)}) ORDER BY id`
6226
+ ).all(symbolName, ...files);
6227
+ ids.push(...rows.map((r) => r.id));
6228
+ }
6229
+ return ids;
4935
6230
  }
4936
6231
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4937
6232
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
@@ -5365,9 +6660,9 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5365
6660
  const loadedIds = new Set(syms.map((s) => s.id));
5366
6661
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
5367
6662
  if (missingIds.length > 0) {
5368
- const placeholders = missingIds.map(() => "?").join(",");
6663
+ const placeholders2 = missingIds.map(() => "?").join(",");
5369
6664
  const extras = stmt(
5370
- `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
6665
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders2})`
5371
6666
  ).all(...missingIds);
5372
6667
  for (const s of extras) symById.set(s.id, s);
5373
6668
  }
@@ -5380,81 +6675,6 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5380
6675
  return { nodes, edges };
5381
6676
  }
5382
6677
 
5383
- // src/codebase-index/vector-search.ts
5384
- var RRF_K = 60;
5385
- var VECTOR_DIMENSIONS = 384;
5386
- var NGRAM_SIZE = 3;
5387
- function embedText(text) {
5388
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5389
- const normalized = text.toLowerCase().trim();
5390
- if (normalized.length < NGRAM_SIZE) {
5391
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5392
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5393
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5394
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5395
- vec[bucket] += 1;
5396
- }
5397
- } else {
5398
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5399
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5400
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5401
- vec[bucket] += 1;
5402
- }
5403
- }
5404
- let norm = 0;
5405
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5406
- norm += vec[i] * vec[i];
5407
- }
5408
- norm = Math.sqrt(norm);
5409
- if (norm > 0) {
5410
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5411
- vec[i] /= norm;
5412
- }
5413
- }
5414
- return vec;
5415
- }
5416
- function hashNgram(str) {
5417
- let hash = 2166136261;
5418
- for (let i = 0; i < str.length; i++) {
5419
- hash ^= str.charCodeAt(i);
5420
- hash = Math.imul(hash, 16777619);
5421
- }
5422
- return hash >>> 0;
5423
- }
5424
- function cosineSimilarity(a, b) {
5425
- let dot = 0;
5426
- const len = Math.min(a.length, b.length);
5427
- for (let i = 0; i < len; i++) {
5428
- dot += a[i] * b[i];
5429
- }
5430
- return dot;
5431
- }
5432
- function encodeVector(vec) {
5433
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5434
- }
5435
- function decodeVector(buf) {
5436
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5437
- const copy = new Float32Array(buf.byteLength / 4);
5438
- for (let i = 0; i < copy.length; i++) {
5439
- copy[i] = view.getFloat32(i * 4, true);
5440
- }
5441
- return copy;
5442
- }
5443
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5444
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5445
- const scored = [];
5446
- for (const id of allIds) {
5447
- const bm25Rank = bm25Ranks.get(id);
5448
- const vecRank = vectorRanks.get(id);
5449
- let score = 0;
5450
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5451
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5452
- scored.push([id, score]);
5453
- }
5454
- scored.sort((a, b) => b[1] - a[1]);
5455
- return scored;
5456
- }
5457
-
5458
6678
  // src/codebase-index/writer-mutations.ts
5459
6679
  function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5460
6680
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
@@ -5466,24 +6686,29 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5466
6686
  for (const ref of entry.refs) affectedNames.add(ref.toName);
5467
6687
  }
5468
6688
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5469
- const placeholders = options.deleteForFiles.map(() => "?").join(",");
5470
6689
  for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5471
6690
  affectedNames.add(name);
5472
6691
  }
5473
- if (ftsAvailable) {
5474
- stmtFn(
5475
- `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5476
- ).run(...options.deleteForFiles);
5477
- }
5478
- if (vectorsAvailable) {
6692
+ let cursor = 0;
6693
+ for (const take of inListChunks(options.deleteForFiles.length, Math.floor(maxSqlVars / 4))) {
6694
+ const bucket = padToInBucket(options.deleteForFiles.slice(cursor, cursor + take));
6695
+ cursor += take;
6696
+ const ph = placeholders(bucket.length);
6697
+ if (ftsAvailable) {
6698
+ stmtFn(
6699
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6700
+ ).run(...bucket);
6701
+ }
6702
+ if (vectorsAvailable) {
6703
+ stmtFn(
6704
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6705
+ ).run(...bucket);
6706
+ }
5479
6707
  stmtFn(
5480
- `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5481
- ).run(...options.deleteForFiles);
6708
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6709
+ ).run(...bucket);
6710
+ stmtFn(`DELETE FROM symbols WHERE file IN (${ph})`).run(...bucket);
5482
6711
  }
5483
- stmtFn(
5484
- `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5485
- ).run(...options.deleteForFiles);
5486
- stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5487
6712
  }
5488
6713
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5489
6714
  let nextId = allocateSymbolIds(totalSymbols);
@@ -5515,12 +6740,14 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5515
6740
  text: buildIndexableText(s.name, s.signature, s.docComment)
5516
6741
  });
5517
6742
  }
5518
- vectorRows.push({
5519
- id,
5520
- vector: encodeVector(
5521
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5522
- )
5523
- });
6743
+ if (vectorsAvailable) {
6744
+ vectorRows.push({
6745
+ id,
6746
+ vector: encodeVector(
6747
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6748
+ )
6749
+ });
6750
+ }
5524
6751
  const inserted = { ...s, id };
5525
6752
  allInserted.push(inserted);
5526
6753
  insertedForEntry.push(inserted);
@@ -5612,9 +6839,8 @@ var CORE_TABLES_SQL = `
5612
6839
  signature TEXT NOT NULL DEFAULT '',
5613
6840
  doc_comment TEXT NOT NULL DEFAULT '',
5614
6841
  scope TEXT NOT NULL DEFAULT '',
5615
- text TEXT NOT NULL DEFAULT '',
5616
- file_fk TEXT NOT NULL
5617
- );
6842
+ text TEXT NOT NULL DEFAULT ''
6843
+ );
5618
6844
  `;
5619
6845
  var FILE_INDEX_SQL = [
5620
6846
  "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
@@ -5625,7 +6851,6 @@ var SYMBOL_INDEX_SQL = [
5625
6851
  "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
5626
6852
  "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
5627
6853
  "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
5628
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
5629
6854
  "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
5630
6855
  ];
5631
6856
  var REFS_TABLE_SQL = `
@@ -5700,11 +6925,12 @@ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5700
6925
  return stmtFn(base).all();
5701
6926
  }
5702
6927
  const out = [];
5703
- for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5704
- const chunk = onlyFiles.slice(i, i + maxSqlVars);
5705
- const placeholders = chunk.map(() => "?").join(",");
6928
+ let cursor = 0;
6929
+ for (const take of inListChunks(onlyFiles.length, maxSqlVars)) {
6930
+ const chunk = padToInBucket(onlyFiles.slice(cursor, cursor + take));
6931
+ cursor += take;
5706
6932
  out.push(
5707
- ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
6933
+ ...stmtFn(`${base} AND s.file IN (${placeholders(chunk.length)})`).all(...chunk)
5708
6934
  );
5709
6935
  }
5710
6936
  return out;
@@ -5775,16 +7001,18 @@ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVar
5775
7001
  )`
5776
7002
  );
5777
7003
  const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5778
- for (let i = 0; i < resolutions.length; i += chunkSize) {
5779
- const chunk = resolutions.slice(i, i + chunkSize);
5780
- const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
7004
+ let cursor = 0;
7005
+ for (const take of ladderChunkSizes(resolutions.length, chunkSize)) {
7006
+ const chunk = resolutions.slice(cursor, cursor + take);
7007
+ cursor += take;
7008
+ const valuesPh = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5781
7009
  const binds = [];
5782
7010
  for (const entry of chunk) {
5783
7011
  binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5784
7012
  }
5785
7013
  stmtFn(
5786
7014
  `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5787
- VALUES ${placeholders}`
7015
+ VALUES ${valuesPh}`
5788
7016
  ).run(...binds);
5789
7017
  }
5790
7018
  db.exec(
@@ -5821,9 +7049,11 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5821
7049
  const list = [...names].filter((name) => name.length > 0);
5822
7050
  if (list.length === 0) return 0;
5823
7051
  let total = 0;
5824
- for (let i = 0; i < list.length; i += maxSqlVars) {
5825
- const chunk = list.slice(i, i + maxSqlVars);
5826
- const placeholders = chunk.map(() => "?").join(",");
7052
+ let cursor = 0;
7053
+ for (const take of inListChunks(list.length, maxSqlVars)) {
7054
+ const chunk = padToInBucket(list.slice(cursor, cursor + take));
7055
+ cursor += take;
7056
+ const ph = placeholders(chunk.length);
5827
7057
  try {
5828
7058
  const result = stmtFn(
5829
7059
  `UPDATE refs
@@ -5832,16 +7062,16 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5832
7062
  SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5833
7063
  FROM symbols sym
5834
7064
  JOIN lang_family lf ON lf.lang = sym.lang
5835
- WHERE sym.name IN (${placeholders})
7065
+ WHERE sym.name IN (${ph})
5836
7066
  GROUP BY sym.name, lf.family
5837
7067
  UNION ALL
5838
7068
  SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5839
7069
  FROM symbols sym
5840
- WHERE sym.name IN (${placeholders})
7070
+ WHERE sym.name IN (${ph})
5841
7071
  GROUP BY sym.name
5842
7072
  ) AS s,
5843
7073
  lang_family AS rf
5844
- WHERE refs.to_name IN (${placeholders})
7074
+ WHERE refs.to_name IN (${ph})
5845
7075
  AND rf.lang = refs.lang
5846
7076
  AND s.name = refs.to_name
5847
7077
  AND s.family = rf.family`
@@ -5853,7 +7083,7 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5853
7083
  SELECT sym.id FROM symbols sym
5854
7084
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5855
7085
  ORDER BY sym.id LIMIT 1
5856
- ) WHERE refs.to_name IN (${placeholders})
7086
+ ) WHERE refs.to_name IN (${ph})
5857
7087
  AND EXISTS (
5858
7088
  SELECT 1 FROM symbols sym
5859
7089
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
@@ -5910,7 +7140,7 @@ function buildWriterSearchWhere(query, filter) {
5910
7140
  const conditions = [];
5911
7141
  const values = [];
5912
7142
  let effectiveKind = filter?.kind;
5913
- if (filter?.lspKind !== void 0) {
7143
+ if (filter?.lspKind != null) {
5914
7144
  const mapped = lspKindToInternalKind(filter.lspKind);
5915
7145
  if (mapped !== null) {
5916
7146
  effectiveKind = mapped;
@@ -5989,7 +7219,7 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5989
7219
  );
5990
7220
  }
5991
7221
  let effectiveKind = filter?.kind;
5992
- if (filter?.lspKind !== void 0) {
7222
+ if (filter?.lspKind != null) {
5993
7223
  const mapped = lspKindToInternalKind(filter.lspKind);
5994
7224
  if (mapped === null) return { results: [], total: 0 };
5995
7225
  effectiveKind = mapped;
@@ -6026,15 +7256,14 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
6026
7256
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
6027
7257
  }
6028
7258
  const where = conditions.join(" AND ");
6029
- const countRows = stmtFn(
6030
- `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
6031
- ).all(...values);
6032
- const total = countRows[0] ? Number(countRows[0].n) : 0;
6033
- if (total === 0) return { results: [], total: 0 };
6034
7259
  const bm25Rows = stmtFn(
6035
7260
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
6036
7261
  -bm25(symbols_fts) AS score,
6037
- snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
7262
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet,
7263
+ -- Keep this uncorrelated: referencing outer columns turns it into
7264
+ -- a per-row subquery and defeats the one-count-per-statement win.
7265
+ (SELECT COUNT(*) FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
7266
+ WHERE ${where}) AS total_count
6038
7267
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
6039
7268
  WHERE ${where}
6040
7269
  ORDER BY
@@ -6043,13 +7272,15 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
6043
7272
  ELSE 2 END,
6044
7273
  bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
6045
7274
  LIMIT ?`
6046
- ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
6047
- if (vectorsAvailable && bm25Rows.length > 0) {
7275
+ ).all(...values, ...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
7276
+ if (bm25Rows.length === 0) return { results: [], total: 0 };
7277
+ const total = Number(bm25Rows[0]?.total_count ?? 0);
7278
+ if (vectorsAvailable) {
6048
7279
  const queryVec = embedText(query);
6049
7280
  const candidateIds = bm25Rows.map((r) => r.id);
6050
- const placeholders = candidateIds.map(() => "?").join(",");
7281
+ const placeholders2 = candidateIds.map(() => "?").join(",");
6051
7282
  const vecRows = stmtFn(
6052
- `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
7283
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders2})`
6053
7284
  ).all(...candidateIds);
6054
7285
  const vecScores = vecRows.map((r) => ({
6055
7286
  id: r.symbol_id,
@@ -6237,9 +7468,9 @@ var IndexStore = class _IndexStore {
6237
7468
  }
6238
7469
  constructor(projectRoot, opts = {}) {
6239
7470
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6240
- fs8.mkdirSync(this.indexDir, { recursive: true });
7471
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6241
7472
  const Database = loadDatabaseSync();
6242
- this.db = new Database(path13.join(this.indexDir, DB_FILE2));
7473
+ this.db = new Database(path14.join(this.indexDir, DB_FILE2));
6243
7474
  applyIndexStorePragmas(this.db);
6244
7475
  this.initSchema();
6245
7476
  }
@@ -6368,7 +7599,11 @@ var IndexStore = class _IndexStore {
6368
7599
  );
6369
7600
  if (symbolCount !== ftsCount) {
6370
7601
  this.db.exec("DELETE FROM symbols_fts");
6371
- if (this.vectorsAvailable) this.db.exec("DELETE FROM symbol_vectors");
7602
+ if (vectorEmbeddingEnabled() && this.stmt(
7603
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbol_vectors'"
7604
+ ).get() !== void 0) {
7605
+ this.db.exec("DELETE FROM symbol_vectors");
7606
+ }
6372
7607
  const rows = this.stmt(
6373
7608
  "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
6374
7609
  ).all();
@@ -6387,8 +7622,13 @@ var IndexStore = class _IndexStore {
6387
7622
  this.ftsAvailable = false;
6388
7623
  }
6389
7624
  try {
6390
- this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
6391
- this.vectorsAvailable = true;
7625
+ if (vectorEmbeddingEnabled()) {
7626
+ this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
7627
+ this.vectorsAvailable = true;
7628
+ } else {
7629
+ this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
7630
+ this.vectorsAvailable = false;
7631
+ }
6392
7632
  } catch {
6393
7633
  this.vectorsAvailable = false;
6394
7634
  }
@@ -6423,14 +7663,22 @@ var IndexStore = class _IndexStore {
6423
7663
  }
6424
7664
  invalidateIncomingRefsForFiles(files) {
6425
7665
  if (files.length === 0) return /* @__PURE__ */ new Set();
6426
- const placeholders = files.map(() => "?").join(",");
6427
- const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
6428
- ...files
6429
- ).map((row) => row.name);
6430
- this.stmt(
6431
- `UPDATE refs SET to_id = NULL
6432
- WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6433
- ).run(...files);
7666
+ const names = [];
7667
+ let cursor = 0;
7668
+ for (const take of inListChunks(files.length, _IndexStore.MAX_SQL_VARS)) {
7669
+ const bucket = padToInBucket(files.slice(cursor, cursor + take));
7670
+ cursor += take;
7671
+ const ph = placeholders(bucket.length);
7672
+ for (const row of this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${ph})`).all(
7673
+ ...bucket
7674
+ )) {
7675
+ names.push(row.name);
7676
+ }
7677
+ this.stmt(
7678
+ `UPDATE refs SET to_id = NULL
7679
+ WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
7680
+ ).run(...bucket);
7681
+ }
6434
7682
  return new Set(names);
6435
7683
  }
6436
7684
  resolveRefsForNamesUnsafe(names) {
@@ -6464,6 +7712,14 @@ var IndexStore = class _IndexStore {
6464
7712
  if (this.ftsAvailable) {
6465
7713
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
6466
7714
  }
7715
+ if (this.vectorsAvailable) {
7716
+ vectorRows.push({
7717
+ id,
7718
+ vector: encodeVector(
7719
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
7720
+ )
7721
+ });
7722
+ }
6467
7723
  result.push({ ...s, id });
6468
7724
  }
6469
7725
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -6496,15 +7752,15 @@ var IndexStore = class _IndexStore {
6496
7752
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6497
7753
  if (this.ftsAvailable) {
6498
7754
  this.stmt(
6499
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7755
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6500
7756
  ).run(file);
6501
7757
  }
6502
7758
  if (this.vectorsAvailable) {
6503
7759
  this.stmt(
6504
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7760
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6505
7761
  ).run(file);
6506
7762
  }
6507
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7763
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6508
7764
  this.resolveRefsForNamesUnsafe(affectedNames);
6509
7765
  this.commitWriteTransaction(ownsTransaction);
6510
7766
  } catch (error) {
@@ -6521,18 +7777,18 @@ var IndexStore = class _IndexStore {
6521
7777
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6522
7778
  if (this.ftsAvailable) {
6523
7779
  this.stmt(
6524
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7780
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6525
7781
  ).run(file);
6526
7782
  }
6527
7783
  if (this.vectorsAvailable) {
6528
7784
  this.stmt(
6529
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7785
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6530
7786
  ).run(file);
6531
7787
  }
6532
- this.stmt(
6533
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6534
- ).run(file);
6535
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7788
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7789
+ file
7790
+ );
7791
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6536
7792
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
6537
7793
  this.resolveRefsForNamesUnsafe(affectedNames);
6538
7794
  this.commitWriteTransaction(ownsTransaction);
@@ -6636,6 +7892,10 @@ var IndexStore = class _IndexStore {
6636
7892
  getStats() {
6637
7893
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6638
7894
  }
7895
+ /** P2.5: minimal summary for search-response piggyback (see writer-admin). */
7896
+ getIndexSummary() {
7897
+ return getIndexSummaryWithStatement((sql) => this.stmt(sql));
7898
+ }
6639
7899
  setLastIndexed(ts2) {
6640
7900
  this.runWithRetry(() => {
6641
7901
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
@@ -6737,18 +7997,18 @@ var IndexStore = class _IndexStore {
6737
7997
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6738
7998
  if (this.ftsAvailable) {
6739
7999
  this.stmt(
6740
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8000
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6741
8001
  ).run(meta.file);
6742
8002
  }
6743
8003
  if (this.vectorsAvailable) {
6744
8004
  this.stmt(
6745
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8005
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6746
8006
  ).run(meta.file);
6747
8007
  }
6748
- this.stmt(
6749
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6750
- ).run(meta.file);
6751
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
8008
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
8009
+ meta.file
8010
+ );
8011
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(meta.file);
6752
8012
  this.stmt(
6753
8013
  `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
6754
8014
  VALUES (?, ?, ?, ?, ?, ?)
@@ -6780,6 +8040,27 @@ var IndexStore = class _IndexStore {
6780
8040
  } catch {
6781
8041
  }
6782
8042
  }
8043
+ /**
8044
+ * P4.14: best-effort WAL checkpoint for idle-time maintenance.
8045
+ *
8046
+ * `wal_autocheckpoint` is PASSIVE and only attempts work after a COMMIT —
8047
+ * once writes stop, nothing fires again, so the WAL keeps whatever frames
8048
+ * the last burst left. This probes with PASSIVE first (never blocks; busy=1
8049
+ * means readers still hold WAL snapshots) and only issues the TRUNCATE —
8050
+ * which resets index.db-wal to zero bytes — when the checkpointer can
8051
+ * proceed immediately. Callers run this on the daemon's single thread, so
8052
+ * never wait on readers here: busy means "retry at the next idle window".
8053
+ */
8054
+ checkpointWal() {
8055
+ try {
8056
+ const probe = this.db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get();
8057
+ if (Number(probe?.busy ?? 1) !== 0) return false;
8058
+ const done = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
8059
+ return Number(done?.busy ?? 1) === 0;
8060
+ } catch {
8061
+ return false;
8062
+ }
8063
+ }
6783
8064
  compactIfNeeded(options = {}) {
6784
8065
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6785
8066
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6865,7 +8146,9 @@ function resolveParallelBatch() {
6865
8146
  return indexParallelBatchSize(availableParallelism());
6866
8147
  }
6867
8148
  function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
6868
- return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
8149
+ const threshold = resolveWorkerPoolThreshold();
8150
+ if (threshold === 0) return false;
8151
+ return !isFrugalPerf() && candidateFileCount >= threshold && parseBatchCount > 1;
6869
8152
  }
6870
8153
  function yieldEventLoop() {
6871
8154
  return new Promise((resolve4) => setImmediate(resolve4));
@@ -6888,15 +8171,15 @@ var IndexSourceChangedError = class extends Error {
6888
8171
  name = "IndexSourceChangedError";
6889
8172
  };
6890
8173
  function isWithinProject(projectRoot, file) {
6891
- const rel = path14.relative(projectRoot, file);
6892
- return rel !== "" && !rel.startsWith(`..${path14.sep}`) && rel !== ".." && !path14.isAbsolute(rel);
8174
+ const rel = path15.relative(projectRoot, file);
8175
+ return rel !== "" && !rel.startsWith(`..${path15.sep}`) && rel !== ".." && !path15.isAbsolute(rel);
6893
8176
  }
6894
8177
  function isMissingPathError(err) {
6895
8178
  const code = err?.code;
6896
8179
  return code === "ENOENT" || code === "ENOTDIR";
6897
8180
  }
6898
8181
  function normalizeComparablePath(value) {
6899
- const resolved = path14.resolve(value);
8182
+ const resolved = path15.resolve(value);
6900
8183
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6901
8184
  }
6902
8185
  function gitOutput(projectRoot, args) {
@@ -6942,24 +8225,24 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6942
8225
  const record = statusRecords[i];
6943
8226
  if (!record) continue;
6944
8227
  const status = record.slice(0, 2);
6945
- const changedPath = path14.resolve(projectRoot, record.slice(3));
8228
+ const changedPath = path15.resolve(projectRoot, record.slice(3));
6946
8229
  dirty.add(changedPath);
6947
8230
  if (status.includes("D")) deleted.add(changedPath);
6948
8231
  if (status.includes("R") || status.includes("C")) {
6949
8232
  const source = statusRecords[++i];
6950
- if (source) dirty.add(path14.resolve(projectRoot, source));
8233
+ if (source) dirty.add(path15.resolve(projectRoot, source));
6951
8234
  }
6952
8235
  }
6953
8236
  const files = [];
6954
8237
  for (const relative3 of output.toString("utf8").split("\0")) {
6955
8238
  if (!relative3) continue;
6956
8239
  const portable = relative3.replace(/\\/g, "/");
6957
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path14.posix.basename(portable))) {
8240
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path15.posix.basename(portable))) {
6958
8241
  continue;
6959
8242
  }
6960
- const full = path14.resolve(projectRoot, relative3);
8243
+ const full = path15.resolve(projectRoot, relative3);
6961
8244
  if (deleted.has(full)) continue;
6962
- const ext = path14.extname(relative3).toLowerCase();
8245
+ const ext = path15.extname(relative3).toLowerCase();
6963
8246
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6964
8247
  }
6965
8248
  const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
@@ -6967,7 +8250,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6967
8250
  for (const dirtyFile of [...dirty].sort()) {
6968
8251
  if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6969
8252
  snapshot.update("\0").update(dirtyFile).update("\0");
6970
- snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
8253
+ snapshot.update(xxhash64String(await fs10.readFile(dirtyFile, "utf8")));
6971
8254
  }
6972
8255
  return {
6973
8256
  files,
@@ -7003,7 +8286,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
7003
8286
  }
7004
8287
  let entries;
7005
8288
  try {
7006
- entries = await fs9.readdir(dir, { withFileTypes: true });
8289
+ entries = await fs10.readdir(dir, { withFileTypes: true });
7007
8290
  } catch (err) {
7008
8291
  complete = false;
7009
8292
  errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -7012,14 +8295,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
7012
8295
  dirCount++;
7013
8296
  for (const e of entries) {
7014
8297
  if (ignoreSet.has(e.name)) continue;
7015
- const full = path14.join(dir, e.name);
7016
- const rel = path14.relative(projectRoot, full).replace(/\\/g, "/");
8298
+ const full = path15.join(dir, e.name);
8299
+ const rel = path15.relative(projectRoot, full).replace(/\\/g, "/");
7017
8300
  if (e.isDirectory()) {
7018
8301
  if (isGitIgnored(rel, true)) continue;
7019
8302
  await walk(full);
7020
8303
  } else if (e.isFile()) {
7021
8304
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
7022
- const ext = path14.extname(e.name).toLowerCase();
8305
+ const ext = path15.extname(e.name).toLowerCase();
7023
8306
  if (indexableExts.has(ext) || detectLang(full) !== null) {
7024
8307
  results.push(full);
7025
8308
  }
@@ -7105,10 +8388,10 @@ async function runIndexerAtomic(store, opts) {
7105
8388
  let trustedUnchanged;
7106
8389
  let discoverySnapshotKey;
7107
8390
  if (opts.files && opts.files.length > 0) {
7108
- files = opts.files.map((f) => path14.resolve(projectRoot, f)).filter((f) => {
8391
+ files = opts.files.map((f) => path15.resolve(projectRoot, f)).filter((f) => {
7109
8392
  if (!isWithinProject(projectRoot, f)) return false;
7110
- const rel = path14.relative(projectRoot, f).replace(/\\/g, "/");
7111
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path14.basename(f)) && !isGitIgnored(rel, false);
8393
+ const rel = path15.relative(projectRoot, f).replace(/\\/g, "/");
8394
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path15.basename(f)) && !isGitIgnored(rel, false);
7112
8395
  });
7113
8396
  } else {
7114
8397
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -7141,7 +8424,6 @@ async function runIndexerAtomic(store, opts) {
7141
8424
  if (!meta || !trustedUnchanged.has(file)) return true;
7142
8425
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
7143
8426
  symbolsIndexed += meta.symbolCount;
7144
- filesIndexed++;
7145
8427
  filesSkipped++;
7146
8428
  filesPreSkipped++;
7147
8429
  return false;
@@ -7170,7 +8452,7 @@ async function runIndexerAtomic(store, opts) {
7170
8452
  async (file) => {
7171
8453
  let stat3;
7172
8454
  try {
7173
- stat3 = await fs9.stat(file, statOpts);
8455
+ stat3 = await fs10.stat(file, statOpts);
7174
8456
  } catch (e) {
7175
8457
  if (isAbortError(e)) throw e;
7176
8458
  return {
@@ -7197,7 +8479,7 @@ async function runIndexerAtomic(store, opts) {
7197
8479
  const meta = existingMeta.get(file);
7198
8480
  let content;
7199
8481
  try {
7200
- content = await fs9.readFile(file, { encoding: "utf8", signal });
8482
+ content = await fs10.readFile(file, { encoding: "utf8", signal });
7201
8483
  } catch (e) {
7202
8484
  if (isAbortError(e)) throw e;
7203
8485
  return {
@@ -7262,22 +8544,19 @@ async function runIndexerAtomic(store, opts) {
7262
8544
  }
7263
8545
  }
7264
8546
  if (!pool) {
7265
- await Promise.all(
7266
- toParse.map(async (item) => {
7267
- try {
7268
- const parsed = await parseFileContent(item.file, item.content, item.lang);
7269
- const settled = statReadParse[item.index];
7270
- if (settled.status === "fulfilled") {
7271
- settled.value.parsed = parsed;
7272
- }
7273
- } catch (e) {
7274
- const settled = statReadParse[item.index];
7275
- if (settled.status === "fulfilled") {
7276
- settled.value.error = `parse error: ${e instanceof Error ? e.message : String(e)}`;
7277
- }
7278
- }
7279
- })
8547
+ const parsedAll = await parseFilesContent(
8548
+ toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
7280
8549
  );
8550
+ for (let pi2 = 0; pi2 < parsedAll.length && pi2 < toParse.length; pi2++) {
8551
+ const settled = statReadParse[toParse[pi2].index];
8552
+ if (settled.status !== "fulfilled") continue;
8553
+ const slot = parsedAll[pi2];
8554
+ if (slot.result) {
8555
+ settled.value.parsed = slot.result;
8556
+ } else {
8557
+ settled.value.error = `parse error: ${slot.error ?? `no result for ${toParse[pi2].file}`}`;
8558
+ }
8559
+ }
7281
8560
  }
7282
8561
  }
7283
8562
  const batchEntries = [];
@@ -7303,7 +8582,6 @@ async function runIndexerAtomic(store, opts) {
7303
8582
  if (result.skippedMeta) {
7304
8583
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
7305
8584
  symbolsIndexed += result.skippedMeta.symbolCount;
7306
- filesIndexed++;
7307
8585
  filesSkipped++;
7308
8586
  const stored = existingMeta.get(file);
7309
8587
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
@@ -7328,7 +8606,6 @@ async function runIndexerAtomic(store, opts) {
7328
8606
  lastIndexed: Date.now(),
7329
8607
  contentHash: result.contentHash ?? ""
7330
8608
  });
7331
- filesIndexed++;
7332
8609
  filesEmpty++;
7333
8610
  }
7334
8611
  continue;
@@ -7342,7 +8619,6 @@ async function runIndexerAtomic(store, opts) {
7342
8619
  lastIndexed: Date.now(),
7343
8620
  contentHash: result.contentHash ?? ""
7344
8621
  });
7345
- filesIndexed++;
7346
8622
  filesEmpty++;
7347
8623
  continue;
7348
8624
  }
@@ -7472,7 +8748,7 @@ async function indexService(args, hooks = {}) {
7472
8748
  function searchService(args) {
7473
8749
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
7474
8750
  try {
7475
- return store.searchRanked(
8751
+ const result = store.searchRanked(
7476
8752
  args.query,
7477
8753
  {
7478
8754
  kind: args.kind,
@@ -7482,6 +8758,10 @@ function searchService(args) {
7482
8758
  },
7483
8759
  args.limit
7484
8760
  );
8761
+ if (result.total === 0) {
8762
+ return { ...result, indexSummary: store.getIndexSummary() };
8763
+ }
8764
+ return result;
7485
8765
  } finally {
7486
8766
  indexStorePool.release(store);
7487
8767
  }
@@ -7545,34 +8825,71 @@ function outgoingCallsService(args) {
7545
8825
  init_languages();
7546
8826
 
7547
8827
  // src/codebase-index/project-server-client.ts
7548
- import { spawn as spawn3 } from "node:child_process";
7549
- import * as fs12 from "node:fs";
8828
+ import { spawn as spawn4 } from "node:child_process";
8829
+ import * as fs13 from "node:fs";
7550
8830
  import * as net from "node:net";
7551
- import { StringDecoder } from "node:string_decoder";
7552
8831
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7553
8832
 
7554
8833
  // src/codebase-index/binary-frame.ts
7555
8834
  import { decode, encode } from "@msgpack/msgpack";
7556
8835
  var BINARY_FRAME_MAGIC = 87;
8836
+ var MAX_BINARY_FRAME_BYTES = 256 * 1024 * 1024;
8837
+ var MAX_INBOUND_BINARY_FRAME_BYTES = 64 * 1024 * 1024;
7557
8838
  function isBinaryFrame(firstByte) {
7558
8839
  return firstByte === BINARY_FRAME_MAGIC;
7559
8840
  }
7560
8841
  function encodeBinaryFrame(message) {
7561
- const payload = encode(message);
8842
+ const payload = encode(normalizeUndefined(message));
7562
8843
  const header = Buffer.allocUnsafe(5);
7563
8844
  header[0] = BINARY_FRAME_MAGIC;
7564
8845
  header.writeUInt32BE(payload.length, 1);
7565
8846
  return Buffer.concat([header, payload], 5 + payload.length);
7566
8847
  }
8848
+ function normalizeUndefined(value) {
8849
+ if (value instanceof Date) {
8850
+ const time = value.getTime();
8851
+ return Number.isNaN(time) ? null : value.toISOString();
8852
+ }
8853
+ if (value instanceof Map) return normalizeUndefined(Object.fromEntries(value));
8854
+ if (value instanceof Set) return normalizeUndefined([...value]);
8855
+ if (value instanceof Error) {
8856
+ return normalizeUndefined({ name: value.name, message: value.message, stack: value.stack });
8857
+ }
8858
+ if (value instanceof RegExp) return String(value);
8859
+ if (value instanceof URL) return value.toJSON();
8860
+ if (Buffer.isBuffer(value)) return { type: "Buffer", data: [...value] };
8861
+ if (Array.isArray(value)) return value.map((entry) => normalizeUndefined(entry));
8862
+ if (!isPlainObject(value)) return value;
8863
+ const out = {};
8864
+ for (const [key, entry] of Object.entries(value)) {
8865
+ if (entry === void 0) continue;
8866
+ out[key] = normalizeUndefined(entry);
8867
+ }
8868
+ return out;
8869
+ }
8870
+ function isPlainObject(value) {
8871
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
8872
+ const proto = Object.getPrototypeOf(value);
8873
+ return proto === Object.prototype || proto === null;
8874
+ }
7567
8875
  function decodeBinaryFrame(payload) {
7568
8876
  return decode(payload);
7569
8877
  }
8878
+ function encodeJsonFrame(message) {
8879
+ return `${JSON.stringify(normalizeUndefined(message))}
8880
+ `;
8881
+ }
8882
+
8883
+ // src/codebase-index/project-server-client-state.ts
8884
+ import * as fs12 from "node:fs";
8885
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8886
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
7570
8887
 
7571
8888
  // src/codebase-index/project-server-endpoint.ts
7572
8889
  import { createHash as createHash3 } from "node:crypto";
7573
- import * as fs10 from "node:fs";
7574
- import * as os3 from "node:os";
7575
- import * as path15 from "node:path";
8890
+ import * as fs11 from "node:fs";
8891
+ import * as os4 from "node:os";
8892
+ import * as path16 from "node:path";
7576
8893
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7577
8894
  var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
7578
8895
  var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
@@ -7581,21 +8898,21 @@ var buildIdCache;
7581
8898
  function projectIndexServerBuildId(entrypoint) {
7582
8899
  const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
7583
8900
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
7584
- const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path15.resolve(cleanHref);
8901
+ const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path16.resolve(cleanHref);
7585
8902
  try {
7586
- const stat3 = fs10.statSync(file);
8903
+ const stat3 = fs11.statSync(file);
7587
8904
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7588
8905
  return buildIdCache.buildId;
7589
8906
  }
7590
- const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
8907
+ const buildId = createHash3("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
7591
8908
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7592
8909
  return buildId;
7593
8910
  } catch {
7594
- return `unreadable:${path15.basename(file)}`;
8911
+ return `unreadable:${path16.basename(file)}`;
7595
8912
  }
7596
8913
  }
7597
8914
  function normalizeLocalPath(value) {
7598
- const resolved = path15.resolve(value);
8915
+ const resolved = path16.resolve(value);
7599
8916
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7600
8917
  }
7601
8918
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -7607,26 +8924,16 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
7607
8924
  if (process.platform === "win32") {
7608
8925
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
7609
8926
  }
7610
- return path15.join(os3.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
8927
+ return path16.join(os4.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
7611
8928
  }
7612
8929
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
7613
- return path15.join(
7614
- path15.resolve(resolveIndexDir(projectRoot, indexDir)),
8930
+ return path16.join(
8931
+ path16.resolve(resolveIndexDir(projectRoot, indexDir)),
7615
8932
  PROJECT_INDEX_SERVER_METADATA_FILE
7616
8933
  );
7617
8934
  }
7618
8935
 
7619
- // src/codebase-index/project-server-protocol.ts
7620
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
7621
- function encodeProjectServerMessage(message) {
7622
- return `${JSON.stringify(message)}
7623
- `;
7624
- }
7625
-
7626
8936
  // src/codebase-index/project-server-client-state.ts
7627
- import * as fs11 from "node:fs";
7628
- import { fileURLToPath as fileURLToPath4 } from "node:url";
7629
- import { checkUnixSocketPath } from "@wrongstack/core/utils";
7630
8937
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
7631
8938
  var SERVER_START_TIMEOUT_MS = 1e4;
7632
8939
  var SERVER_CONTROL_TIMEOUT_MS = 5e3;
@@ -7657,7 +8964,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
7657
8964
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
7658
8965
  try {
7659
8966
  const url = new URL(rel, import.meta.url);
7660
- if (url.protocol === "file:" && fs11.existsSync(fileURLToPath4(url))) {
8967
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath4(url))) {
7661
8968
  builtUrl = url;
7662
8969
  break;
7663
8970
  }
@@ -7745,6 +9052,12 @@ function cancellationError(signal) {
7745
9052
  return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
7746
9053
  }
7747
9054
 
9055
+ // src/codebase-index/project-server-protocol.ts
9056
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
9057
+ function encodeProjectServerMessage(message) {
9058
+ return encodeJsonFrame(message);
9059
+ }
9060
+
7748
9061
  // src/codebase-index/project-server-client.ts
7749
9062
  var ProjectServerConnection = class {
7750
9063
  constructor(projectRoot, indexDir, endpoint) {
@@ -7757,11 +9070,13 @@ var ProjectServerConnection = class {
7757
9070
  indexDir;
7758
9071
  endpoint;
7759
9072
  socket = null;
7760
- buffer = "";
7761
- /** P6: binary frame buffer accumulates raw bytes when in binary mode. */
7762
- binaryBuffer = [];
7763
- /** P6: StringDecoder for safe UTF-8 multibyte handling in JSON mode. */
7764
- textDecoder = null;
9073
+ /**
9074
+ * Raw inbound bytes for the unified per-frame reader. Frames are sniffed
9075
+ * individually — JSON text (newline-terminated) or binary (magic 0x57) —
9076
+ * instead of latching a read mode, so a JSON broadcast between binary
9077
+ * frames cannot desynchronize the reader.
9078
+ */
9079
+ readBuffer = Buffer.alloc(0);
7765
9080
  /** P6: true once the server advertises binary support and client accepts. */
7766
9081
  useBinary = false;
7767
9082
  info = null;
@@ -7903,7 +9218,7 @@ var ProjectServerConnection = class {
7903
9218
  this.activity = null;
7904
9219
  this.health = null;
7905
9220
  this.useBinary = false;
7906
- this.binaryBuffer = [];
9221
+ this.readBuffer = Buffer.alloc(0);
7907
9222
  this.connectReject?.(new Error("codebase-index client disconnected"));
7908
9223
  this.connectResolve = null;
7909
9224
  this.connectReject = null;
@@ -7924,7 +9239,7 @@ var ProjectServerConnection = class {
7924
9239
  currentAuthToken() {
7925
9240
  if (this.authToken === void 0) {
7926
9241
  try {
7927
- const raw = fs12.readFileSync(
9242
+ const raw = fs13.readFileSync(
7928
9243
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
7929
9244
  "utf8"
7930
9245
  );
@@ -8031,10 +9346,8 @@ var ProjectServerConnection = class {
8031
9346
  this.info = null;
8032
9347
  this.activity = null;
8033
9348
  this.health = null;
8034
- this.buffer = "";
8035
- this.binaryBuffer = [];
9349
+ this.readBuffer = Buffer.alloc(0);
8036
9350
  this.useBinary = false;
8037
- this.textDecoder = null;
8038
9351
  return new Promise((resolve4, reject) => {
8039
9352
  const socket = net.createConnection(this.endpoint);
8040
9353
  this.socket = socket;
@@ -8064,18 +9377,47 @@ var ProjectServerConnection = class {
8064
9377
  socket.on("close", () => this.onClose(socket));
8065
9378
  });
8066
9379
  }
9380
+ /**
9381
+ * Unified per-frame reader. Each frame is sniffed by its first byte:
9382
+ * `0x57` ('W') → length-prefixed MessagePack binary, anything else →
9383
+ * newline-delimited JSON text. Sniffing per frame (instead of latching a
9384
+ * mode) is what makes mixed streams work: the server may interleave a JSON
9385
+ * `index-state` broadcast between binary responses, and an old JSON-only
9386
+ * server stays readable while `useBinary` is armed.
9387
+ *
9388
+ * Multibyte UTF-8 in JSON frames is safe: raw `0x0a` only occurs as the
9389
+ * JSON delimiter (inside JSON strings `\n` is escaped), so a complete line
9390
+ * is always complete UTF-8.
9391
+ */
8067
9392
  onData(socket, chunk) {
8068
9393
  if (socket !== this.socket) return;
8069
- if (this.useBinary) {
8070
- this.onBinaryData(socket, chunk);
8071
- return;
8072
- }
8073
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
8074
- this.buffer += this.textDecoder.write(chunk);
9394
+ this.readBuffer = this.readBuffer.length === 0 ? chunk : Buffer.concat([this.readBuffer, chunk]);
8075
9395
  while (true) {
8076
- const newline = this.buffer.indexOf("\n");
9396
+ if (this.readBuffer.length === 0) return;
9397
+ if (this.useBinary && isBinaryFrame(this.readBuffer[0])) {
9398
+ if (this.readBuffer.length < 5) return;
9399
+ const frameLen = this.readBuffer.readUInt32BE(1);
9400
+ if (frameLen > MAX_BINARY_FRAME_BYTES) {
9401
+ socket.destroy();
9402
+ this.transition("offline", { error: "binary frame length exceeds the IPC limit" });
9403
+ return;
9404
+ }
9405
+ if (this.readBuffer.length < 5 + frameLen) return;
9406
+ const payload = this.readBuffer.subarray(5, 5 + frameLen);
9407
+ this.readBuffer = this.readBuffer.subarray(5 + frameLen);
9408
+ let message2;
9409
+ try {
9410
+ message2 = decodeBinaryFrame(payload);
9411
+ } catch {
9412
+ socket.destroy(new Error("invalid binary codebase-index server response"));
9413
+ return;
9414
+ }
9415
+ this.onMessage(message2);
9416
+ continue;
9417
+ }
9418
+ const newline = this.readBuffer.indexOf(10);
8077
9419
  if (newline < 0) {
8078
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
9420
+ if (this.readBuffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
8079
9421
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
8080
9422
  }
8081
9423
  return;
@@ -8084,8 +9426,8 @@ var ProjectServerConnection = class {
8084
9426
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
8085
9427
  return;
8086
9428
  }
8087
- const line = this.buffer.slice(0, newline);
8088
- this.buffer = this.buffer.slice(newline + 1);
9429
+ const line = this.readBuffer.subarray(0, newline).toString("utf8");
9430
+ this.readBuffer = this.readBuffer.subarray(newline + 1);
8089
9431
  if (!line) continue;
8090
9432
  let message;
8091
9433
  try {
@@ -8097,44 +9439,6 @@ var ProjectServerConnection = class {
8097
9439
  this.onMessage(message);
8098
9440
  }
8099
9441
  }
8100
- /**
8101
- * P6: Parse binary frames from the raw buffer.
8102
- *
8103
- * Each frame is: [1 byte magic 0x57] [4 bytes uint32 BE length] [payload].
8104
- * The magic byte distinguishes binary from JSON — a JSON frame's first byte
8105
- * is `{` (0x7B), so there is no ambiguity even in a mixed-mode buffer.
8106
- */
8107
- onBinaryData(socket, chunk) {
8108
- this.binaryBuffer.push(chunk);
8109
- const all = Buffer.concat(this.binaryBuffer);
8110
- let offset = 0;
8111
- while (offset + 5 <= all.length) {
8112
- if (!isBinaryFrame(all[offset])) {
8113
- this.useBinary = false;
8114
- this.buffer += all.subarray(offset).toString("utf8");
8115
- this.binaryBuffer = [];
8116
- return;
8117
- }
8118
- const frameLen = all.readUInt32BE(offset + 1);
8119
- if (frameLen > 256 * 1024 * 1024) {
8120
- socket.destroy();
8121
- this.transition("offline", { error: "binary frame length exceeds 256MB limit" });
8122
- return;
8123
- }
8124
- const totalLen = 5 + frameLen;
8125
- if (offset + totalLen > all.length) break;
8126
- const payload = all.subarray(offset + 5, offset + 5 + frameLen);
8127
- try {
8128
- const message = decodeBinaryFrame(payload);
8129
- this.onMessage(message);
8130
- } catch {
8131
- socket.destroy(new Error("invalid binary codebase-index server response"));
8132
- return;
8133
- }
8134
- offset += totalLen;
8135
- }
8136
- this.binaryBuffer = offset < all.length ? [all.subarray(offset)] : [];
8137
- }
8138
9442
  onMessage(message) {
8139
9443
  if (message.type === "hello") {
8140
9444
  if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
@@ -8154,7 +9458,7 @@ var ProjectServerConnection = class {
8154
9458
  }
8155
9459
  this.info = message;
8156
9460
  this.markResponsive();
8157
- if (message.binarySupported) this.useBinary = true;
9461
+ if (message.binarySupported && binaryFramingEnabled()) this.useBinary = true;
8158
9462
  this.transition("connected", { pid: message.pid });
8159
9463
  ensureHeartbeatLoop();
8160
9464
  this.connectResolve?.();
@@ -8240,13 +9544,13 @@ var ProjectServerConnection = class {
8240
9544
  if (!url) throw new Error("built codebase-index project server is unavailable");
8241
9545
  if (process.platform !== "win32") {
8242
9546
  try {
8243
- fs12.rmSync(this.endpoint, { force: true });
9547
+ fs13.rmSync(this.endpoint, { force: true });
8244
9548
  } catch {
8245
9549
  }
8246
9550
  }
8247
9551
  const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
8248
9552
  if (this.indexDir) args.push("--index-dir", this.indexDir);
8249
- const child = spawn3(process.execPath, args, {
9553
+ const child = spawn4(process.execPath, args, {
8250
9554
  detached: true,
8251
9555
  stdio: "ignore",
8252
9556
  windowsHide: true,
@@ -8264,8 +9568,8 @@ var ProjectServerConnection = class {
8264
9568
  process.kill(pid);
8265
9569
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
8266
9570
  try {
8267
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
8268
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
9571
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
9572
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
8269
9573
  } catch {
8270
9574
  }
8271
9575
  return true;
@@ -8277,6 +9581,10 @@ var ProjectServerConnection = class {
8277
9581
  var connections = /* @__PURE__ */ new Map();
8278
9582
  var MAX_CACHED_CONNECTIONS = 8;
8279
9583
  var heartbeatTimer;
9584
+ function binaryFramingEnabled() {
9585
+ const flag = process.env["WRONGSTACK_INDEX_BINARY"];
9586
+ return flag === "1" || flag === "true";
9587
+ }
8280
9588
  function forgetConnection(endpoint, connection) {
8281
9589
  if (connections.get(endpoint) === connection) connections.delete(endpoint);
8282
9590
  connection.close();
@@ -8366,7 +9674,7 @@ function resolveWorkerUrl() {
8366
9674
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8367
9675
  try {
8368
9676
  const url = new URL(rel, import.meta.url);
8369
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
9677
+ if (url.protocol === "file:" && fs14.existsSync(fileURLToPath6(url))) return url;
8370
9678
  } catch {
8371
9679
  }
8372
9680
  }
@@ -8723,7 +10031,7 @@ var editTool = {
8723
10031
  });
8724
10032
  }
8725
10033
  const absPath = await safeResolveReal(input.path, ctx);
8726
- const stat3 = await fs14.stat(absPath).catch((err) => {
10034
+ const stat3 = await fs15.stat(absPath).catch((err) => {
8727
10035
  if (err.code === "ENOENT") {
8728
10036
  throw new ToolValidationError({
8729
10037
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -8740,8 +10048,8 @@ var editTool = {
8740
10048
  });
8741
10049
  }
8742
10050
  const autoRead = !ctx.hasRead(absPath);
8743
- const original = await fs14.readFile(absPath, "utf8");
8744
- const updated = await fs14.stat(absPath);
10051
+ const original = await fs15.readFile(absPath, "utf8");
10052
+ const updated = await fs15.stat(absPath);
8745
10053
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
8746
10054
  const originalHash = sha256hex(original);
8747
10055
  const lastReadHash = ctx.lastReadHash?.(absPath);
@@ -8849,9 +10157,9 @@ var editTool = {
8849
10157
  enqueueReindex({ projectRoot: ctx.projectRoot, files: [absPath] });
8850
10158
  } catch {
8851
10159
  }
8852
- const written = await fs14.stat(absPath);
10160
+ const written = await fs15.stat(absPath);
8853
10161
  ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
8854
- ctx.session.recordFileChange({
10162
+ ctx.session?.recordFileChange?.({
8855
10163
  path: absPath,
8856
10164
  action: "modified",
8857
10165
  before: original,