@wrongstack/tools 0.309.1 → 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 (57) hide show
  1. package/dist/_regex.d.ts +6 -34
  2. package/dist/bash.js +3 -3
  3. package/dist/builtin.js +3011 -1677
  4. package/dist/codebase-index/binary-frame.d.ts +57 -8
  5. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +6 -0
  6. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +6 -0
  7. package/dist/codebase-index/codebase-search-tool.d.ts +15 -5
  8. package/dist/codebase-index/index-service.d.ts +3 -19
  9. package/dist/codebase-index/index.js +2735 -1415
  10. package/dist/codebase-index/indexer.d.ts +3 -0
  11. package/dist/codebase-index/parser-batch.d.ts +53 -0
  12. package/dist/codebase-index/parser-dispatch.d.ts +32 -0
  13. package/dist/codebase-index/parser-output.d.ts +14 -0
  14. package/dist/codebase-index/parser-worker-pool.d.ts +57 -4
  15. package/dist/codebase-index/parser-worker-script.d.ts +5 -2
  16. package/dist/codebase-index/parser-worker-script.js +4042 -0
  17. package/dist/codebase-index/project-server-cache.d.ts +16 -0
  18. package/dist/codebase-index/project-server-client.d.ts +2 -2
  19. package/dist/codebase-index/project-server-query-cache.d.ts +88 -0
  20. package/dist/codebase-index/project-server.js +2846 -1308
  21. package/dist/codebase-index/py-parser.d.ts +5 -0
  22. package/dist/codebase-index/schema.d.ts +14 -1
  23. package/dist/codebase-index/sqlite-runtime.d.ts +2 -2
  24. package/dist/codebase-index/tree-sitter/queries.d.ts +30 -3
  25. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  26. package/dist/codebase-index/vector-search.d.ts +12 -0
  27. package/dist/codebase-index/wal-maintenance.d.ts +58 -0
  28. package/dist/codebase-index/worker-protocol/contracts.d.ts +44 -0
  29. package/dist/codebase-index/worker-protocol.d.ts +17 -1
  30. package/dist/codebase-index/worker.js +2300 -1020
  31. package/dist/codebase-index/writer-admin.d.ts +11 -0
  32. package/dist/codebase-index/writer-helpers.d.ts +31 -1
  33. package/dist/codebase-index/writer-mutations.d.ts +0 -6
  34. package/dist/codebase-index/writer-schema.d.ts +2 -2
  35. package/dist/codebase-index/writer.d.ts +15 -0
  36. package/dist/edit.js +2511 -1203
  37. package/dist/exec.js +5 -3
  38. package/dist/grep.js +5 -124
  39. package/dist/index.js +3007 -1736
  40. package/dist/json.js +5 -124
  41. package/dist/kanban.js +130 -0
  42. package/dist/logs.js +5 -121
  43. package/dist/pack.js +3011 -1677
  44. package/dist/patch.js +2524 -1216
  45. package/dist/plan.js +106 -0
  46. package/dist/read.js +2506 -1198
  47. package/dist/replace.js +2487 -1295
  48. package/dist/search.js +6 -2
  49. package/dist/session-kanban.js +24 -16
  50. package/dist/task.js +106 -0
  51. package/dist/todo.js +106 -0
  52. package/dist/tool-tier.d.ts +11 -0
  53. package/dist/tool-tier.js +3019 -1677
  54. package/dist/tree.js +14 -3
  55. package/dist/win32.js +3 -3
  56. package/dist/write.js +2513 -1205
  57. package/package.json +5 -4
package/dist/read.js CHANGED
@@ -170,238 +170,133 @@ var init_languages = __esm({
170
170
  }
171
171
  });
172
172
 
173
- // src/codebase-index/ts-parser.ts
174
- var ts_parser_exports = {};
175
- __export(ts_parser_exports, {
176
- detectLang: () => detectLang,
177
- parseSymbols: () => parseSymbols
178
- });
179
- function loadTypescript() {
180
- tsLoad ??= import("@typescript/typescript6").then((m) => {
181
- ts = m.default ?? m;
182
- return ts;
183
- });
184
- return tsLoad;
185
- }
186
- function kindMap() {
187
- kindMapCache ??= {
188
- [ts.SyntaxKind.ClassDeclaration]: "class",
189
- [ts.SyntaxKind.InterfaceDeclaration]: "interface",
190
- [ts.SyntaxKind.EnumDeclaration]: "enum",
191
- [ts.SyntaxKind.TypeAliasDeclaration]: "type",
192
- [ts.SyntaxKind.FunctionDeclaration]: "function",
193
- [ts.SyntaxKind.MethodDeclaration]: "method",
194
- [ts.SyntaxKind.GetAccessor]: "property",
195
- [ts.SyntaxKind.SetAccessor]: "property",
196
- [ts.SyntaxKind.PropertyDeclaration]: "property",
197
- [ts.SyntaxKind.Parameter]: "parameter",
198
- [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
199
- };
200
- return kindMapCache;
201
- }
202
- function kindOf(node) {
203
- if (ts.isVariableDeclaration(node)) {
204
- const parent = node.parent;
205
- if (ts.isVariableDeclarationList(parent)) {
206
- const flags = parent.flags;
207
- if (flags & ts.NodeFlags.Let) return "let";
208
- if (flags & ts.NodeFlags.Const) return "const";
209
- return "var";
210
- }
211
- }
212
- if (ts.isModuleDeclaration(node)) return "namespace";
213
- return kindMap()[node.kind] ?? null;
214
- }
215
- function getSignature(printer, node, sourceFile) {
216
- const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
217
- return raw.replace(/\s+/g, " ").slice(0, 500);
218
- }
219
- function getJsDoc(node, sourceFile) {
220
- const fullText = sourceFile.getFullText();
221
- const nodePos = node.getFullStart();
222
- const comments = ts.getLeadingCommentRanges(fullText, nodePos);
223
- if (!comments) return "";
224
- for (const range of comments) {
225
- const commentText = fullText.slice(range.pos, range.end);
226
- const trimmed = commentText.trim();
227
- if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
228
- const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
229
- return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
230
- }
173
+ // src/codebase-index/import-extractor.ts
174
+ function lastSegment(specifier) {
175
+ const pathLike = /[/\\]|::/.test(specifier);
176
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
177
+ let last = segments[segments.length - 1] ?? specifier;
178
+ if (last === "*" || last === "_") {
179
+ last = segments[segments.length - 2] ?? specifier;
231
180
  }
232
- return "";
181
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
182
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
183
+ return dotted[dotted.length - 1] ?? last;
233
184
  }
234
- function pushScopeName(node, parts) {
235
- if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
236
- parts.push(node.name?.text ?? "Anon");
237
- } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
238
- if (node.name && ts.isIdentifier(node.name)) {
239
- parts.push(node.name.text);
240
- }
185
+ function newlineOffsets(content) {
186
+ const offsets = [];
187
+ for (let i = 0; i < content.length; i++) {
188
+ if (content.charCodeAt(i) === 10) offsets.push(i);
241
189
  }
190
+ return offsets;
242
191
  }
243
- async function parseSymbols(opts) {
244
- const { file, content, lang } = opts;
245
- await loadTypescript();
246
- let sourceFile;
247
- try {
248
- sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
249
- } catch {
250
- return { file, lang, symbols: [], mtimeMs: Date.now() };
251
- }
252
- const symbols = [];
253
- const refs = [];
254
- const printer = ts.createPrinter({});
255
- function visit(node, funcDepth, scopeParts) {
256
- const kind = kindOf(node);
257
- if (kind) {
258
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
259
- } else {
260
- const nameNode = node.name;
261
- if (!nameNode || !ts.isIdentifier(nameNode)) {
262
- return;
263
- }
264
- const name = nameNode.text;
265
- const pos2 = nameNode.getStart(sourceFile);
266
- const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
267
- const scope = scopeParts.join(".");
268
- const signature = getSignature(printer, node, sourceFile);
269
- const docComment = getJsDoc(node, sourceFile);
270
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
271
- symbols.push({
272
- id: 0,
273
- lang,
274
- kind,
275
- name,
276
- file,
277
- line: line2 + 1,
278
- col: character,
279
- signature,
280
- docComment,
281
- scope,
282
- text
283
- });
284
- }
285
- }
286
- const pos = node.getStart(sourceFile);
287
- const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
288
- const lineNum = line + 1;
289
- if (ts.isCallExpression(node)) {
290
- const expr = node.expression;
291
- if (ts.isIdentifier(expr)) {
292
- refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
293
- }
294
- } else if (ts.isPropertyAccessExpression(node)) {
295
- if (ts.isIdentifier(node.expression)) {
296
- refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
297
- }
298
- } else if (ts.isTypeReferenceNode(node)) {
299
- const name = getTypeName(node.typeName);
300
- if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
301
- } else if (ts.isHeritageClause(node)) {
302
- for (const t of node.types) {
303
- const name = getTypeName(t.expression);
304
- if (name)
305
- refs.push({
306
- fromId: 0,
307
- toName: name,
308
- callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
309
- line: lineNum
310
- });
311
- }
312
- } else if (ts.isImportDeclaration(node)) {
313
- emitImportSpecifierRefs(node, refs, lineNum);
314
- } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
315
- emitExportSpecifierRefs(node, refs, lineNum);
316
- }
317
- const scopeIdx = scopeParts.length;
318
- pushScopeName(node, scopeParts);
319
- const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
320
- ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
321
- scopeParts.length = scopeIdx;
192
+ function lineAt(offsets, index) {
193
+ let low = 0;
194
+ let high = offsets.length;
195
+ while (low < high) {
196
+ const mid = low + high >>> 1;
197
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
198
+ else high = mid;
322
199
  }
323
- visit(sourceFile, 0, []);
324
- return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
200
+ return low + 1;
325
201
  }
326
- function getTypeName(name) {
327
- if (ts.isIdentifier(name)) return name.text;
328
- if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
329
- return "";
202
+ function hasImportPatterns(lang) {
203
+ return LANG_IMPORTS[lang] !== void 0;
330
204
  }
331
- function deduplicateRefs(refs) {
205
+ function extractImports(opts) {
206
+ const patterns = LANG_IMPORTS[opts.lang];
207
+ if (!patterns || !opts.content) return [];
208
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
209
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
210
+ const refs = [];
332
211
  const seen = /* @__PURE__ */ new Set();
333
- return refs.filter((r) => {
334
- const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
335
- if (seen.has(key)) return false;
336
- seen.add(key);
337
- return true;
338
- });
339
- }
340
- function getImportSpecifierName(spec) {
341
- return spec.propertyName?.text ?? spec.name.text;
342
- }
343
- function emitImportSpecifierRefs(node, refs, lineNum) {
344
- const module = moduleSpecifierOf(node.moduleSpecifier);
345
- const clause = node.importClause;
346
- if (!clause) {
347
- if (module) {
348
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
349
- }
350
- return;
351
- }
352
- if (clause.name) {
353
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
354
- }
355
- const bindings = clause.namedBindings;
356
- if (!bindings) return;
357
- if (ts.isNamedImports(bindings)) {
358
- for (const element of bindings.elements) {
212
+ const offsets = newlineOffsets(content);
213
+ for (const pattern of patterns) {
214
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
215
+ for (const match of content.matchAll(re)) {
216
+ if (refs.length >= limit) return refs;
217
+ const specifier = match[1]?.trim();
218
+ if (!specifier) continue;
219
+ const module = specifier;
220
+ const toName = pattern.name === "full" ? module : lastSegment(module);
221
+ if (!toName) continue;
222
+ const key = `${module}\0${toName}`;
223
+ if (seen.has(key)) continue;
224
+ seen.add(key);
359
225
  refs.push({
360
226
  fromId: 0,
361
- toName: getImportSpecifierName(element),
227
+ toName,
362
228
  callType: "import",
363
- line: lineNum,
229
+ line: lineAt(offsets, match.index ?? 0),
230
+ lang: opts.lang,
364
231
  module
365
232
  });
366
233
  }
367
- } else if (ts.isNamespaceImport(bindings)) {
368
- refs.push({
369
- fromId: 0,
370
- toName: bindings.name.text,
371
- callType: "import",
372
- line: lineNum,
373
- module
374
- });
375
234
  }
235
+ return refs;
376
236
  }
377
- function moduleSpecifierOf(node) {
378
- return node && ts.isStringLiteral(node) ? node.text : void 0;
379
- }
380
- function emitExportSpecifierRefs(node, refs, lineNum) {
381
- const module = moduleSpecifierOf(node.moduleSpecifier);
382
- const clause = node.exportClause;
383
- if (clause && ts.isNamespaceExport(clause)) {
384
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
385
- return;
386
- }
387
- if (clause && ts.isNamedExports(clause)) {
388
- for (const element of clause.elements) {
389
- const originalName = element.propertyName?.text ?? element.name.text;
390
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
391
- }
392
- return;
393
- }
394
- if (module) {
395
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
396
- }
397
- }
398
- var ts, tsLoad, kindMapCache;
399
- var init_ts_parser = __esm({
400
- "src/codebase-index/ts-parser.ts"() {
237
+ var IMPORT_MAX_FILE_CHARS, IMPORT_MAX_PER_FILE, DOTTED_IMPORT, LANG_IMPORTS;
238
+ var init_import_extractor = __esm({
239
+ "src/codebase-index/import-extractor.ts"() {
401
240
  "use strict";
402
- init_languages();
403
- tsLoad = null;
404
- kindMapCache = null;
241
+ IMPORT_MAX_FILE_CHARS = 512 * 1024;
242
+ IMPORT_MAX_PER_FILE = 400;
243
+ DOTTED_IMPORT = [
244
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
245
+ ];
246
+ LANG_IMPORTS = {
247
+ // Go and Python have real AST extractors; these patterns are the fallback for
248
+ // machines with no Go toolchain or Python interpreter installed, where the
249
+ // parser degrades to regex symbols and would otherwise contribute no edges.
250
+ go: [
251
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
252
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
253
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
254
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
255
+ ],
256
+ py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
257
+ rs: [
258
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
259
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
260
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
261
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
262
+ ],
263
+ java: DOTTED_IMPORT,
264
+ kotlin: DOTTED_IMPORT,
265
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
266
+ csharp: [
267
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
268
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
269
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
270
+ ],
271
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
272
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
273
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
274
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
275
+ php: [
276
+ // `use A\B\C` imports the class C, which is what the index has a symbol
277
+ // for — the namespace symbol only covers the `A\B` prefix.
278
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
279
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
280
+ ],
281
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
282
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
283
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
284
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
285
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
286
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
287
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
288
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
289
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
290
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
291
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
292
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
293
+ html: [
294
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
295
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
296
+ ],
297
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
298
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
299
+ };
405
300
  }
406
301
  });
407
302
 
@@ -487,6 +382,32 @@ function parseParserOutput(stdout, lang) {
487
382
  refs: dedupeRefs(coerceRefs(record.refs, lang))
488
383
  };
489
384
  }
385
+ function parseParserBatchOutput(stdout, lang) {
386
+ const trimmed = stdout.trim();
387
+ if (!trimmed) return [];
388
+ let parsed;
389
+ try {
390
+ parsed = JSON.parse(trimmed);
391
+ } catch {
392
+ return [];
393
+ }
394
+ if (!parsed || typeof parsed !== "object") return [];
395
+ const results = parsed.results;
396
+ if (!Array.isArray(results)) return [];
397
+ return results.flatMap((entry) => {
398
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
399
+ const candidate = entry;
400
+ if (typeof candidate.file !== "string" || !candidate.file) return [];
401
+ return [
402
+ {
403
+ file: candidate.file,
404
+ error: typeof candidate.error === "string" && candidate.error ? candidate.error : void 0,
405
+ symbols: coerceSymbols(candidate.symbols),
406
+ refs: dedupeRefs(coerceRefs(candidate.refs, lang))
407
+ }
408
+ ];
409
+ });
410
+ }
490
411
  function dedupeRefs(refs) {
491
412
  const seen = /* @__PURE__ */ new Set();
492
413
  return refs.filter((ref) => {
@@ -527,187 +448,172 @@ var init_spawn_gate = __esm({
527
448
  }
528
449
  });
529
450
 
530
- // src/codebase-index/go-parser.ts
531
- var go_parser_exports = {};
532
- __export(go_parser_exports, {
533
- detectLang: () => detectLang,
534
- parseSymbols: () => parseSymbols2
535
- });
451
+ // src/codebase-index/parser-batch.ts
536
452
  import { spawn } from "node:child_process";
453
+ import * as fsSync from "node:fs";
454
+ import * as fs4 from "node:fs/promises";
537
455
  import * as os from "node:os";
538
456
  import * as path7 from "node:path";
539
- import * as fs4 from "node:fs/promises";
540
- async function parseSymbols2(opts) {
541
- const { file, content, lang } = opts;
542
- try {
543
- const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
544
- if (parsed.symbols.length > 0) {
545
- return parsed;
457
+ function chunkBatchFiles(files) {
458
+ const chunks = [];
459
+ let current = [];
460
+ let bytes = 0;
461
+ for (const file of files) {
462
+ const size = Buffer.byteLength(file.content, "utf8");
463
+ if (current.length > 0 && (current.length >= MAX_BATCH_FILES || bytes + size > MAX_BATCH_BYTES)) {
464
+ chunks.push(current);
465
+ current = [];
466
+ bytes = 0;
467
+ }
468
+ current.push(file);
469
+ bytes += size;
470
+ }
471
+ if (current.length > 0) chunks.push(current);
472
+ return chunks;
473
+ }
474
+ function batchTimeoutMs(fileCount) {
475
+ return Math.min(12e4, 15e3 + fileCount * 1500);
476
+ }
477
+ async function ensureScriptPath(cached, prefix, fileName, script) {
478
+ if (cached) return { path: cached, wrote: false };
479
+ const dir = await fs4.mkdtemp(path7.join(os.tmpdir(), prefix));
480
+ const scriptPath = path7.join(dir, fileName);
481
+ await fs4.writeFile(scriptPath, script, { encoding: "utf8", flag: "wx" });
482
+ process.once("exit", () => {
483
+ try {
484
+ fsSync.rmSync(dir, { recursive: true, force: true });
485
+ } catch {
546
486
  }
547
- const fallback = fallbackParse(file, content, lang);
548
- return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
549
- } catch {
550
- return fallbackParse(file, content, lang);
551
- }
487
+ });
488
+ return { path: scriptPath, wrote: true };
552
489
  }
553
- function fallbackParse(filePath, content, lang) {
554
- if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
555
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
556
- }
557
- const symbols = [];
558
- const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
559
- const lines = content.split(/\r?\n/);
560
- for (const [idx, line] of lines.entries()) {
561
- const trimmed = line.trimStart();
562
- const col = line.length - trimmed.length + 1;
563
- const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
564
- if (fn?.[1]) {
565
- addFallbackSymbol(symbols, {
566
- filePath,
567
- lang,
568
- kind: trimmed.startsWith("func (") ? "method" : "function",
569
- name: fn[1],
570
- line: idx + 1,
571
- col,
572
- signature: trimmed,
573
- scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
574
- });
575
- continue;
576
- }
577
- const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
578
- if (typeDecl?.[1]) {
579
- addFallbackSymbol(symbols, {
580
- filePath,
581
- lang,
582
- kind: "type",
583
- name: typeDecl[1],
584
- line: idx + 1,
585
- col,
586
- signature: trimmed,
587
- scope: packageName
588
- });
589
- continue;
590
- }
591
- const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
592
- if (valueDecl?.[1] && valueDecl[2]) {
593
- addFallbackSymbol(symbols, {
594
- filePath,
595
- lang,
596
- kind: valueDecl[1],
597
- name: valueDecl[2],
598
- line: idx + 1,
599
- col,
600
- signature: trimmed,
601
- scope: packageName
602
- });
490
+ function runToolchainChild(binary, args, stdinPayload, timeoutMs) {
491
+ return new Promise((resolve4) => {
492
+ let settled = false;
493
+ let stdout = "";
494
+ let proc;
495
+ try {
496
+ proc = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
497
+ } catch {
498
+ resolve4(null);
499
+ return;
603
500
  }
604
- }
605
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
606
- }
607
- function addFallbackSymbol(symbols, opts) {
608
- symbols.push({
609
- id: 0,
610
- lang: opts.lang,
611
- kind: opts.kind,
612
- name: opts.name,
613
- file: opts.filePath,
614
- line: opts.line,
615
- col: opts.col,
616
- signature: opts.signature,
617
- docComment: "",
618
- scope: opts.scope,
619
- text: `${opts.name} ${opts.signature}`.trim()
501
+ const finish = (value) => {
502
+ if (settled) return;
503
+ settled = true;
504
+ clearTimeout(timer);
505
+ resolve4(value);
506
+ };
507
+ const timer = setTimeout(() => {
508
+ proc.kill("SIGKILL");
509
+ finish(null);
510
+ }, timeoutMs);
511
+ timer.unref?.();
512
+ proc.on("error", () => finish(null));
513
+ proc.stdout?.on("data", (chunk) => {
514
+ stdout += chunk.toString();
515
+ });
516
+ proc.stderr?.resume();
517
+ proc.stdin?.on("error", () => {
518
+ });
519
+ proc.stdin?.write(stdinPayload);
520
+ proc.stdin?.end();
521
+ proc.on("close", (code) => finish({ code, stdout }));
620
522
  });
621
523
  }
622
- function hasUnbalancedDelimiters(content) {
623
- const pairs = { "(": ")", "[": "]", "{": "}" };
624
- const closers = new Set(Object.values(pairs));
625
- const stack = [];
626
- for (const ch of content) {
627
- if (pairs[ch]) {
628
- stack.push(pairs[ch]);
629
- } else if (closers.has(ch) && stack.pop() !== ch) {
630
- return true;
631
- }
524
+ async function runGoBatch(files, goBinary) {
525
+ const out = /* @__PURE__ */ new Map();
526
+ if (files.length === 0) return out;
527
+ const { path: scriptPath } = await ensureScriptPath(
528
+ _goBatchScriptPath,
529
+ "ws-go-parse",
530
+ "batch.go",
531
+ GO_BATCH_SCRIPT
532
+ );
533
+ _goBatchScriptPath = scriptPath;
534
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
535
+ const result = await withSpawnGate(
536
+ () => runToolchainChild(
537
+ goBinary ?? resolveWin32Command("go"),
538
+ ["run", scriptPath],
539
+ payload,
540
+ batchTimeoutMs(files.length)
541
+ )
542
+ );
543
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
544
+ for (const entry of parseParserBatchOutput(result.stdout, "go")) {
545
+ if (entry.error !== void 0) continue;
546
+ out.set(entry.file, {
547
+ file: entry.file,
548
+ lang: "go",
549
+ symbols: entry.symbols.map((s) => ({
550
+ id: 0,
551
+ lang: "go",
552
+ kind: s.kind,
553
+ name: s.name,
554
+ file: entry.file,
555
+ line: s.line,
556
+ col: s.col,
557
+ signature: s.signature ?? "",
558
+ docComment: "",
559
+ scope: s.scope ?? "",
560
+ text: `${s.name} ${s.signature ?? ""}`.trim()
561
+ })),
562
+ refs: entry.refs,
563
+ mtimeMs: Date.now()
564
+ });
632
565
  }
633
- return stack.length > 0;
566
+ return out;
634
567
  }
635
- async function syncGoParse(filePath, content, lang) {
636
- try {
637
- let scriptPath = _cachedGoScriptPath;
638
- if (!scriptPath) {
639
- const tmpDir = await fs4.mkdtemp(path7.join(os.tmpdir(), "ws-go-parse-"));
640
- scriptPath = path7.join(tmpDir, "parse.go");
641
- await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
642
- _cachedGoScriptPath = scriptPath;
643
- }
644
- const goBinary = resolveWin32Command("go");
645
- const goResult = await new Promise(
646
- (resolve4, reject) => {
647
- let settled = false;
648
- const proc = spawn(goBinary, ["run", scriptPath], {
649
- stdio: ["pipe", "pipe", "pipe"],
650
- windowsHide: true
651
- });
652
- proc.on("error", (err) => {
653
- if (settled) return;
654
- settled = true;
655
- reject(err);
656
- });
657
- let stdout2 = "";
658
- proc.stdout?.on("data", (chunk) => {
659
- stdout2 += chunk.toString();
660
- });
661
- proc.stderr?.resume();
662
- proc.stdin?.write(content);
663
- proc.stdin?.end();
664
- const timer = setTimeout(() => {
665
- if (settled) return;
666
- settled = true;
667
- proc.kill("SIGKILL");
668
- reject(new Error("timeout"));
669
- }, 15e3);
670
- timer.unref?.();
671
- proc.on("close", (code2) => {
672
- if (settled) return;
673
- settled = true;
674
- clearTimeout(timer);
675
- resolve4({ code: code2, stdout: stdout2 });
676
- });
677
- }
678
- );
679
- const { code, stdout } = goResult;
680
- if (code !== 0 || !stdout.trim()) {
681
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
682
- }
683
- const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
684
- const symbols = rawSymbols.map((s) => ({
685
- id: 0,
686
- lang,
687
- kind: s.kind,
688
- name: s.name,
689
- file: filePath,
690
- line: s.line,
691
- col: s.col,
692
- signature: s.signature ?? "",
693
- docComment: "",
694
- scope: s.scope ?? "",
695
- text: `${s.name} ${s.signature ?? ""}`.trim()
696
- }));
697
- return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
698
- } catch {
699
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
568
+ async function runPyBatch(files, pythonBinary) {
569
+ const out = /* @__PURE__ */ new Map();
570
+ if (files.length === 0) return out;
571
+ const { path: scriptPath } = await ensureScriptPath(
572
+ _pyBatchScriptPath,
573
+ "ws-py-parse",
574
+ "batch.py",
575
+ PY_BATCH_SCRIPT
576
+ );
577
+ _pyBatchScriptPath = scriptPath;
578
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
579
+ const result = await withSpawnGate(
580
+ () => runToolchainChild(pythonBinary, [scriptPath], payload, batchTimeoutMs(files.length))
581
+ );
582
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
583
+ for (const entry of parseParserBatchOutput(result.stdout, "py")) {
584
+ if (entry.error !== void 0) continue;
585
+ out.set(entry.file, {
586
+ file: entry.file,
587
+ lang: "py",
588
+ symbols: entry.symbols.map((s) => ({
589
+ id: 0,
590
+ lang: "py",
591
+ kind: s.kind,
592
+ name: s.name,
593
+ file: entry.file,
594
+ line: s.line,
595
+ col: s.col,
596
+ signature: s.signature ?? "",
597
+ docComment: "",
598
+ scope: s.scope ?? "",
599
+ text: `${s.name} ${s.signature ?? ""}`.trim()
600
+ })),
601
+ refs: entry.refs,
602
+ mtimeMs: Date.now()
603
+ });
700
604
  }
605
+ return out;
701
606
  }
702
- var GO_PARSE_SCRIPT, _cachedGoScriptPath;
703
- var init_go_parser = __esm({
704
- "src/codebase-index/go-parser.ts"() {
607
+ var MAX_BATCH_FILES, MAX_BATCH_BYTES, GO_BATCH_SCRIPT, PY_BATCH_SCRIPT, _goBatchScriptPath, _pyBatchScriptPath;
608
+ var init_parser_batch = __esm({
609
+ "src/codebase-index/parser-batch.ts"() {
705
610
  "use strict";
706
611
  init_win32_resolve();
707
612
  init_parser_output();
708
613
  init_spawn_gate();
709
- init_languages();
710
- GO_PARSE_SCRIPT = `
614
+ MAX_BATCH_FILES = 100;
615
+ MAX_BATCH_BYTES = 8 * 1024 * 1024;
616
+ GO_BATCH_SCRIPT = `
711
617
  package main
712
618
 
713
619
  import (
@@ -731,8 +637,6 @@ type Sym struct {
731
637
  Scope string \`json:"scope"\`
732
638
  }
733
639
 
734
- // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
735
- // yields both. Module is the import path for CallType "import", else empty.
736
640
  type Ref struct {
737
641
  ToName string \`json:"toName"\`
738
642
  CallType string \`json:"callType"\`
@@ -740,57 +644,53 @@ type Ref struct {
740
644
  Module string \`json:"module"\`
741
645
  }
742
646
 
743
- type Result struct {
744
- Symbols []Sym \`json:"symbols"\`
745
- Refs []Ref \`json:"refs"\`
647
+ type FileResult struct {
648
+ File string \`json:"file"\`
649
+ Error string \`json:"error,omitempty"\`
650
+ Symbols []Sym \`json:"symbols"\`
651
+ Refs []Ref \`json:"refs"\`
746
652
  }
747
653
 
748
- func emptyResult() string {
749
- return "{\\"symbols\\":[],\\"refs\\":[]}"
654
+ type BatchResult struct {
655
+ Version int \`json:"version"\`
656
+ Results []FileResult \`json:"results"\`
750
657
  }
751
658
 
752
- func main() {
753
- src, err := io.ReadAll(os.Stdin)
754
- if err != nil {
755
- fmt.Print(emptyResult())
756
- return
757
- }
659
+ type inputFile struct {
660
+ File string \`json:"file"\`
661
+ Content string \`json:"content"\`
662
+ }
663
+
664
+ func parseOne(name string, src []byte) FileResult {
665
+ res := FileResult{File: name, Symbols: []Sym{}, Refs: []Ref{}}
758
666
  fset := token.NewFileSet()
759
667
  node, err := parser.ParseFile(fset, "src.go", src, 0)
760
668
  if err != nil {
761
- fmt.Print(emptyResult())
762
- return
669
+ res.Error = err.Error()
670
+ return res
763
671
  }
764
-
765
- var syms []Sym
766
-
767
- // Package-level scope
768
672
  pkgScope := node.Name.Name
769
-
770
- // Collect all top-level declarations
771
673
  for _, decl := range node.Decls {
772
674
  switch d := decl.(type) {
773
675
  case *ast.FuncDecl:
774
- name := d.Name.Name
676
+ symName := d.Name.Name
775
677
  kind := "function"
776
678
  scope := pkgScope
777
679
  if d.Recv != nil && len(d.Recv.List) > 0 {
778
- scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
680
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + symName
779
681
  kind = "method"
780
682
  } else {
781
- scope = pkgScope + "." + name
683
+ scope = pkgScope + "." + symName
782
684
  }
783
685
  pos := fset.Position(d.Pos())
784
- sig := formatFuncSig(d)
785
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
786
-
686
+ res.Symbols = append(res.Symbols, Sym{Name: symName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: formatFuncSig(d), Scope: scope})
787
687
  case *ast.GenDecl:
788
688
  for _, spec := range d.Specs {
789
689
  switch s := spec.(type) {
790
690
  case *ast.TypeSpec:
791
- name := s.Name.Name
691
+ typeName := s.Name.Name
792
692
  pos := fset.Position(s.Pos())
793
- sig := "type " + name
693
+ sig := "type " + typeName
794
694
  if s.TypeParams != nil {
795
695
  sig += formatTypeParams(s.TypeParams)
796
696
  }
@@ -801,64 +701,72 @@ func main() {
801
701
  } else {
802
702
  sig += " = " + formatType(s.Type)
803
703
  }
804
- syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
805
-
704
+ res.Symbols = append(res.Symbols, Sym{Name: typeName, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
806
705
  case *ast.ValueSpec:
807
706
  for _, n := range s.Names {
808
- name := n.Name
707
+ valueName := n.Name
809
708
  pos := fset.Position(n.Pos())
810
709
  kind := "var"
811
710
  if d.Tok == token.CONST {
812
711
  kind = "const"
813
712
  }
814
- sig := kind + " " + name
713
+ sig := kind + " " + valueName
815
714
  if s.Type != nil {
816
715
  sig += " " + formatType(s.Type)
817
716
  }
818
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
717
+ res.Symbols = append(res.Symbols, Sym{Name: valueName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
819
718
  }
820
719
  }
821
720
  }
822
721
  }
823
722
  }
824
-
825
- refs := []Ref{}
826
723
  ast.Inspect(node, func(n ast.Node) bool {
827
724
  switch expr := n.(type) {
828
725
  case *ast.CallExpr:
829
726
  line := fset.Position(expr.Pos()).Line
830
727
  switch fun := expr.Fun.(type) {
831
728
  case *ast.Ident:
832
- refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
729
+ res.Refs = append(res.Refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
833
730
  case *ast.SelectorExpr:
834
- // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
835
- // declared symbol name, so it resolves the same way the TypeScript
836
- // and Python extractors' call refs do.
837
- refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
731
+ res.Refs = append(res.Refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
838
732
  }
839
733
  case *ast.ImportSpec:
840
734
  if expr.Path != nil {
841
735
  if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
842
736
  line := fset.Position(expr.Pos()).Line
843
- // A Go import names a package, not a symbol; the package's
844
- // last path segment is the name it is referenced by.
845
737
  name := importPath
846
738
  if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
847
739
  name = importPath[idx+1:]
848
740
  }
849
- refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
741
+ res.Refs = append(res.Refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
850
742
  }
851
743
  }
852
744
  }
853
745
  return true
854
746
  })
747
+ return res
748
+ }
855
749
 
856
- if syms == nil {
857
- syms = []Sym{}
750
+ func main() {
751
+ raw, err := io.ReadAll(os.Stdin)
752
+ if err != nil {
753
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
754
+ fmt.Print(string(out))
755
+ return
858
756
  }
859
- data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
757
+ var inputs []inputFile
758
+ if err := json.Unmarshal(raw, &inputs); err != nil {
759
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
760
+ fmt.Print(string(out))
761
+ return
762
+ }
763
+ results := make([]FileResult, 0, len(inputs))
764
+ for _, in := range inputs {
765
+ results = append(results, parseOne(in.File, []byte(in.Content)))
766
+ }
767
+ data, err := json.Marshal(BatchResult{Version: 1, Results: results})
860
768
  if err != nil {
861
- fmt.Print(emptyResult())
769
+ fmt.Print("{\\"version\\":1,\\"results\\":[]}")
862
770
  return
863
771
  }
864
772
  fmt.Print(string(data))
@@ -978,10 +886,8 @@ func formatType(t ast.Expr) string {
978
886
  case *ast.BasicLit:
979
887
  return v.Value
980
888
  case *ast.IndexExpr:
981
- // Generic instantiation with one type arg, e.g. Logger[int].
982
889
  return formatType(v.X) + "[" + formatType(v.Index) + "]"
983
890
  case *ast.IndexListExpr:
984
- // Generic instantiation with multiple type args, e.g. Map[K, V].
985
891
  args := make([]string, len(v.Indices))
986
892
  for i, idx := range v.Indices {
987
893
  args[i] = formatType(idx)
@@ -992,7 +898,187 @@ func formatType(t ast.Expr) string {
992
898
  }
993
899
  }
994
900
  `;
995
- _cachedGoScriptPath = null;
901
+ PY_BATCH_SCRIPT = `import ast, json, sys
902
+
903
+ def get_name(node):
904
+ if isinstance(node, ast.Name):
905
+ return node.id
906
+ elif isinstance(node, ast.Attribute):
907
+ return get_name(node.value) + "." + node.attr
908
+ elif isinstance(node, ast.Subscript):
909
+ return get_name(node.value)
910
+ elif isinstance(node, ast.Call):
911
+ return get_name(node.func)
912
+ elif isinstance(node, ast.Constant):
913
+ return str(node.value)
914
+ return ""
915
+
916
+ def leaf_name(node):
917
+ if isinstance(node, ast.Attribute):
918
+ return node.attr
919
+ if isinstance(node, ast.Name):
920
+ return node.id
921
+ return get_name(node).split(".")[-1]
922
+
923
+ def is_private(name):
924
+ return name.startswith("__") and not name.endswith("__")
925
+
926
+ def parse_one(name, source, module_name):
927
+ result = {"file": name, "symbols": [], "refs": []}
928
+ try:
929
+ tree = ast.parse(source, filename=name)
930
+ except Exception as e:
931
+ result["error"] = str(e)
932
+ return result
933
+ syms = []
934
+ refs = []
935
+ scope_stack = [module_name]
936
+
937
+ def sym(d):
938
+ return {
939
+ "name": d["name"], "kind": d["kind"], "line": d["line"], "col": d["col"],
940
+ "signature": d["signature"], "scope": d["scope"],
941
+ }
942
+
943
+ class Visitor(ast.NodeVisitor):
944
+ def visit_ClassDef(self, node):
945
+ bases = [get_name(b) for b in node.bases]
946
+ sig = "class " + node.name
947
+ if bases:
948
+ sig += "(" + ", ".join(bases) + ")"
949
+ sig += ": ..."
950
+ syms.append(sym({
951
+ "name": node.name, "kind": "class", "line": node.lineno,
952
+ "col": node.col_offset, "signature": sig,
953
+ "scope": ".".join(scope_stack) + "." + node.name,
954
+ }))
955
+ scope_stack.append(node.name)
956
+ self.generic_visit(node)
957
+ scope_stack.pop()
958
+
959
+ def visit_FunctionDef(self, node):
960
+ args = ", ".join(a.arg for a in node.args.args)
961
+ returns = get_name(node.returns) if node.returns is not None else ""
962
+ is_async = isinstance(node, ast.AsyncFunctionDef)
963
+ kind = "function"
964
+ prefix = "def "
965
+ for dec in node.decorator_list:
966
+ d = get_name(dec)
967
+ if d.endswith(".staticmethod"):
968
+ kind = "staticmethod"
969
+ elif d.endswith(".classmethod"):
970
+ kind = "classmethod"
971
+ elif d == "property":
972
+ kind = "property"
973
+ if is_async:
974
+ kind = "async_" + kind
975
+ sig = f"{prefix}{node.name}({args})"
976
+ if returns:
977
+ sig += f" -> {returns}"
978
+ syms.append(sym({
979
+ "name": node.name, "kind": kind, "line": node.lineno,
980
+ "col": node.col_offset, "signature": sig,
981
+ "scope": ".".join(scope_stack) + "." + node.name,
982
+ }))
983
+
984
+ def visit_AsyncFunctionDef(self, node):
985
+ self.visit_FunctionDef(node)
986
+
987
+ def visit_Assign(self, node):
988
+ for target in node.targets:
989
+ if isinstance(target, ast.Name):
990
+ tname = target.id
991
+ if is_private(tname):
992
+ continue
993
+ kind = "const" if tname.isupper() else "var"
994
+ col = target.col_offset if hasattr(target, "col_offset") else 0
995
+ syms.append(sym({
996
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
997
+ "signature": f"{tname} = ...", "scope": ".".join(scope_stack),
998
+ }))
999
+
1000
+ def visit_AnnAssign(self, node):
1001
+ if isinstance(node.target, ast.Name):
1002
+ tname = node.target.id
1003
+ if is_private(tname):
1004
+ return
1005
+ kind = "const" if tname.isupper() else "var"
1006
+ col = node.target.col_offset if hasattr(node.target, "col_offset") else 0
1007
+ sig = f"{tname}: {get_name(node.annotation)}"
1008
+ if node.value:
1009
+ sig += " = ..."
1010
+ syms.append(sym({
1011
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
1012
+ "signature": sig, "scope": ".".join(scope_stack),
1013
+ }))
1014
+
1015
+ def visit_Import(self, node):
1016
+ # Parity with the single-file parser: imports are symbols too.
1017
+ for alias in node.names:
1018
+ name = alias.asname or alias.name
1019
+ syms.append(sym({
1020
+ "name": name, "kind": "import", "line": node.lineno,
1021
+ "col": node.col_offset, "signature": f"import {alias.name}",
1022
+ "scope": ".".join(scope_stack),
1023
+ }))
1024
+
1025
+ def visit_ImportFrom(self, node):
1026
+ module = node.module or ""
1027
+ for alias in node.names:
1028
+ name = alias.asname or alias.name
1029
+ syms.append(sym({
1030
+ "name": name, "kind": "import", "line": node.lineno,
1031
+ "col": node.col_offset, "signature": f"from {module} import {alias.name}",
1032
+ "scope": ".".join(scope_stack),
1033
+ }))
1034
+
1035
+ Visitor().visit(tree)
1036
+
1037
+ for node in ast.walk(tree):
1038
+ if isinstance(node, ast.Call):
1039
+ cname = leaf_name(node.func)
1040
+ if cname:
1041
+ refs.append({"toName": cname, "callType": "call", "line": node.lineno})
1042
+ elif isinstance(node, ast.Import):
1043
+ for alias in node.names:
1044
+ refs.append({
1045
+ "toName": alias.name.split(".")[-1], "callType": "import",
1046
+ "line": node.lineno, "module": alias.name,
1047
+ })
1048
+ elif isinstance(node, ast.ImportFrom):
1049
+ module = ("." * (node.level or 0)) + (node.module or "")
1050
+ for alias in node.names:
1051
+ refs.append({
1052
+ "toName": alias.name, "callType": "import",
1053
+ "line": node.lineno, "module": module,
1054
+ })
1055
+ elif isinstance(node, ast.ClassDef):
1056
+ for base in node.bases:
1057
+ bname = leaf_name(base)
1058
+ if bname:
1059
+ refs.append({"toName": bname, "callType": "inherit", "line": node.lineno})
1060
+
1061
+ result["symbols"] = syms
1062
+ result["refs"] = refs
1063
+ return result
1064
+
1065
+ def main():
1066
+ try:
1067
+ inputs = json.loads(sys.stdin.read())
1068
+ except Exception:
1069
+ print(json.dumps({"version": 1, "results": []}))
1070
+ return
1071
+ results = []
1072
+ for entry in inputs:
1073
+ name = entry.get("file", "")
1074
+ module_name = name.rsplit("/", 1)[-1].rsplit("\\\\", 1)[-1][:-3]
1075
+ results.append(parse_one(name, entry.get("content", ""), module_name))
1076
+ print(json.dumps({"version": 1, "results": results}))
1077
+
1078
+ main()
1079
+ `;
1080
+ _goBatchScriptPath = null;
1081
+ _pyBatchScriptPath = null;
996
1082
  }
997
1083
  });
998
1084
 
@@ -1002,7 +1088,7 @@ __export(generic_parser_exports, {
1002
1088
  GENERIC_MAX_FILE_CHARS: () => GENERIC_MAX_FILE_CHARS,
1003
1089
  GENERIC_MAX_SYMBOLS_DEFAULT: () => GENERIC_MAX_SYMBOLS_DEFAULT,
1004
1090
  parseGeneric: () => parseGeneric,
1005
- parseSymbols: () => parseSymbols3
1091
+ parseSymbols: () => parseSymbols
1006
1092
  });
1007
1093
  function patternsFor(lang) {
1008
1094
  return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
@@ -1090,7 +1176,7 @@ function parseGeneric(opts) {
1090
1176
  }
1091
1177
  return { file, lang, symbols, mtimeMs };
1092
1178
  }
1093
- async function parseSymbols3(opts) {
1179
+ async function parseSymbols(opts) {
1094
1180
  return parseGeneric(opts);
1095
1181
  }
1096
1182
  var C_LIKE, LANG_PATTERNS, KEYWORDS, GENERIC_MAX_SYMBOLS_DEFAULT, GENERIC_MAX_FILE_CHARS;
@@ -1323,13 +1409,14 @@ var init_generic_parser = __esm({
1323
1409
  var py_parser_exports = {};
1324
1410
  __export(py_parser_exports, {
1325
1411
  detectLang: () => detectLang,
1326
- parseSymbols: () => parseSymbols4
1412
+ parseSymbols: () => parseSymbols2,
1413
+ resolvePythonBinary: () => resolvePythonBinary
1327
1414
  });
1328
1415
  import { spawn as spawn2 } from "node:child_process";
1329
1416
  import * as fs5 from "node:fs/promises";
1330
1417
  import * as os2 from "node:os";
1331
1418
  import * as path8 from "node:path";
1332
- async function parseSymbols4(opts) {
1419
+ async function parseSymbols2(opts) {
1333
1420
  const { file, content, lang } = opts;
1334
1421
  try {
1335
1422
  const native = await withSpawnGate(() => syncPyParse(file, content, lang));
@@ -1403,6 +1490,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1403
1490
  });
1404
1491
  });
1405
1492
  }
1493
+ function resolvePythonBinary() {
1494
+ cachedPyBinary ??= resolvePython();
1495
+ return cachedPyBinary;
1496
+ }
1406
1497
  async function syncPyParse(filePath, content, lang) {
1407
1498
  try {
1408
1499
  if (!_cachedScriptPath) {
@@ -1693,9 +1784,713 @@ for node in ast.walk(tree):
1693
1784
  if name:
1694
1785
  refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1695
1786
 
1696
- print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1787
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1788
+ `;
1789
+ _cachedScriptPath = null;
1790
+ }
1791
+ });
1792
+
1793
+ // src/codebase-index/ts-parser.ts
1794
+ var ts_parser_exports = {};
1795
+ __export(ts_parser_exports, {
1796
+ detectLang: () => detectLang,
1797
+ parseSymbols: () => parseSymbols3
1798
+ });
1799
+ function loadTypescript() {
1800
+ tsLoad ??= import("@typescript/typescript6").then((m) => {
1801
+ ts = m.default ?? m;
1802
+ return ts;
1803
+ });
1804
+ return tsLoad;
1805
+ }
1806
+ function kindMap() {
1807
+ kindMapCache ??= {
1808
+ [ts.SyntaxKind.ClassDeclaration]: "class",
1809
+ [ts.SyntaxKind.InterfaceDeclaration]: "interface",
1810
+ [ts.SyntaxKind.EnumDeclaration]: "enum",
1811
+ [ts.SyntaxKind.TypeAliasDeclaration]: "type",
1812
+ [ts.SyntaxKind.FunctionDeclaration]: "function",
1813
+ [ts.SyntaxKind.MethodDeclaration]: "method",
1814
+ [ts.SyntaxKind.GetAccessor]: "property",
1815
+ [ts.SyntaxKind.SetAccessor]: "property",
1816
+ [ts.SyntaxKind.PropertyDeclaration]: "property",
1817
+ [ts.SyntaxKind.Parameter]: "parameter",
1818
+ [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
1819
+ };
1820
+ return kindMapCache;
1821
+ }
1822
+ function kindOf(node) {
1823
+ if (ts.isVariableDeclaration(node)) {
1824
+ const parent = node.parent;
1825
+ if (ts.isVariableDeclarationList(parent)) {
1826
+ const flags = parent.flags;
1827
+ if (flags & ts.NodeFlags.Let) return "let";
1828
+ if (flags & ts.NodeFlags.Const) return "const";
1829
+ return "var";
1830
+ }
1831
+ }
1832
+ if (ts.isModuleDeclaration(node)) return "namespace";
1833
+ return kindMap()[node.kind] ?? null;
1834
+ }
1835
+ function getSignature(printer, node, sourceFile) {
1836
+ const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1837
+ return raw.replace(/\s+/g, " ").slice(0, 500);
1838
+ }
1839
+ function getJsDoc(node, sourceFile) {
1840
+ const fullText = sourceFile.getFullText();
1841
+ const nodePos = node.getFullStart();
1842
+ const comments = ts.getLeadingCommentRanges(fullText, nodePos);
1843
+ if (!comments) return "";
1844
+ for (const range of comments) {
1845
+ const commentText = fullText.slice(range.pos, range.end);
1846
+ const trimmed = commentText.trim();
1847
+ if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
1848
+ const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
1849
+ return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
1850
+ }
1851
+ }
1852
+ return "";
1853
+ }
1854
+ function pushScopeName(node, parts) {
1855
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
1856
+ parts.push(node.name?.text ?? "Anon");
1857
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
1858
+ if (node.name && ts.isIdentifier(node.name)) {
1859
+ parts.push(node.name.text);
1860
+ }
1861
+ }
1862
+ }
1863
+ async function parseSymbols3(opts) {
1864
+ const { file, content, lang } = opts;
1865
+ await loadTypescript();
1866
+ let sourceFile;
1867
+ try {
1868
+ sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
1869
+ } catch {
1870
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1871
+ }
1872
+ const symbols = [];
1873
+ const refs = [];
1874
+ const printer = ts.createPrinter({});
1875
+ function visit(node, funcDepth, scopeParts) {
1876
+ const kind = kindOf(node);
1877
+ if (kind) {
1878
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
1879
+ } else {
1880
+ const nameNode = node.name;
1881
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
1882
+ return;
1883
+ }
1884
+ const name = nameNode.text;
1885
+ const pos2 = nameNode.getStart(sourceFile);
1886
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
1887
+ const scope = scopeParts.join(".");
1888
+ const signature = getSignature(printer, node, sourceFile);
1889
+ const docComment = getJsDoc(node, sourceFile);
1890
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
1891
+ symbols.push({
1892
+ id: 0,
1893
+ lang,
1894
+ kind,
1895
+ name,
1896
+ file,
1897
+ line: line2 + 1,
1898
+ col: character,
1899
+ signature,
1900
+ docComment,
1901
+ scope,
1902
+ text
1903
+ });
1904
+ }
1905
+ }
1906
+ const pos = node.getStart(sourceFile);
1907
+ const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1908
+ const lineNum = line + 1;
1909
+ if (ts.isCallExpression(node)) {
1910
+ const expr = node.expression;
1911
+ if (ts.isIdentifier(expr)) {
1912
+ refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
1913
+ }
1914
+ } else if (ts.isPropertyAccessExpression(node)) {
1915
+ if (ts.isIdentifier(node.expression)) {
1916
+ refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
1917
+ }
1918
+ } else if (ts.isTypeReferenceNode(node)) {
1919
+ const name = getTypeName(node.typeName);
1920
+ if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
1921
+ } else if (ts.isHeritageClause(node)) {
1922
+ for (const t of node.types) {
1923
+ const name = getTypeName(t.expression);
1924
+ if (name)
1925
+ refs.push({
1926
+ fromId: 0,
1927
+ toName: name,
1928
+ callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
1929
+ line: lineNum
1930
+ });
1931
+ }
1932
+ } else if (ts.isImportDeclaration(node)) {
1933
+ emitImportSpecifierRefs(node, refs, lineNum);
1934
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
1935
+ emitExportSpecifierRefs(node, refs, lineNum);
1936
+ }
1937
+ const scopeIdx = scopeParts.length;
1938
+ pushScopeName(node, scopeParts);
1939
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
1940
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
1941
+ scopeParts.length = scopeIdx;
1942
+ }
1943
+ visit(sourceFile, 0, []);
1944
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1945
+ }
1946
+ function getTypeName(name) {
1947
+ if (ts.isIdentifier(name)) return name.text;
1948
+ if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
1949
+ return "";
1950
+ }
1951
+ function deduplicateRefs(refs) {
1952
+ const seen = /* @__PURE__ */ new Set();
1953
+ return refs.filter((r) => {
1954
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
1955
+ if (seen.has(key)) return false;
1956
+ seen.add(key);
1957
+ return true;
1958
+ });
1959
+ }
1960
+ function getImportSpecifierName(spec) {
1961
+ return spec.propertyName?.text ?? spec.name.text;
1962
+ }
1963
+ function emitImportSpecifierRefs(node, refs, lineNum) {
1964
+ const module = moduleSpecifierOf(node.moduleSpecifier);
1965
+ const clause = node.importClause;
1966
+ if (!clause) {
1967
+ if (module) {
1968
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
1969
+ }
1970
+ return;
1971
+ }
1972
+ if (clause.name) {
1973
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
1974
+ }
1975
+ const bindings = clause.namedBindings;
1976
+ if (!bindings) return;
1977
+ if (ts.isNamedImports(bindings)) {
1978
+ for (const element of bindings.elements) {
1979
+ refs.push({
1980
+ fromId: 0,
1981
+ toName: getImportSpecifierName(element),
1982
+ callType: "import",
1983
+ line: lineNum,
1984
+ module
1985
+ });
1986
+ }
1987
+ } else if (ts.isNamespaceImport(bindings)) {
1988
+ refs.push({
1989
+ fromId: 0,
1990
+ toName: bindings.name.text,
1991
+ callType: "import",
1992
+ line: lineNum,
1993
+ module
1994
+ });
1995
+ }
1996
+ }
1997
+ function moduleSpecifierOf(node) {
1998
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
1999
+ }
2000
+ function emitExportSpecifierRefs(node, refs, lineNum) {
2001
+ const module = moduleSpecifierOf(node.moduleSpecifier);
2002
+ const clause = node.exportClause;
2003
+ if (clause && ts.isNamespaceExport(clause)) {
2004
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
2005
+ return;
2006
+ }
2007
+ if (clause && ts.isNamedExports(clause)) {
2008
+ for (const element of clause.elements) {
2009
+ const originalName = element.propertyName?.text ?? element.name.text;
2010
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
2011
+ }
2012
+ return;
2013
+ }
2014
+ if (module) {
2015
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
2016
+ }
2017
+ }
2018
+ var ts, tsLoad, kindMapCache;
2019
+ var init_ts_parser = __esm({
2020
+ "src/codebase-index/ts-parser.ts"() {
2021
+ "use strict";
2022
+ init_languages();
2023
+ tsLoad = null;
2024
+ kindMapCache = null;
2025
+ }
2026
+ });
2027
+
2028
+ // src/codebase-index/go-parser.ts
2029
+ var go_parser_exports = {};
2030
+ __export(go_parser_exports, {
2031
+ detectLang: () => detectLang,
2032
+ parseSymbols: () => parseSymbols4
2033
+ });
2034
+ import { spawn as spawn3 } from "node:child_process";
2035
+ import * as os3 from "node:os";
2036
+ import * as path9 from "node:path";
2037
+ import * as fs6 from "node:fs/promises";
2038
+ async function parseSymbols4(opts) {
2039
+ const { file, content, lang } = opts;
2040
+ try {
2041
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
2042
+ if (parsed.symbols.length > 0) {
2043
+ return parsed;
2044
+ }
2045
+ const fallback = fallbackParse(file, content, lang);
2046
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
2047
+ } catch {
2048
+ return fallbackParse(file, content, lang);
2049
+ }
2050
+ }
2051
+ function fallbackParse(filePath, content, lang) {
2052
+ if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
2053
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2054
+ }
2055
+ const symbols = [];
2056
+ const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
2057
+ const lines = content.split(/\r?\n/);
2058
+ for (const [idx, line] of lines.entries()) {
2059
+ const trimmed = line.trimStart();
2060
+ const col = line.length - trimmed.length + 1;
2061
+ const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
2062
+ if (fn?.[1]) {
2063
+ addFallbackSymbol(symbols, {
2064
+ filePath,
2065
+ lang,
2066
+ kind: trimmed.startsWith("func (") ? "method" : "function",
2067
+ name: fn[1],
2068
+ line: idx + 1,
2069
+ col,
2070
+ signature: trimmed,
2071
+ scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
2072
+ });
2073
+ continue;
2074
+ }
2075
+ const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2076
+ if (typeDecl?.[1]) {
2077
+ addFallbackSymbol(symbols, {
2078
+ filePath,
2079
+ lang,
2080
+ kind: "type",
2081
+ name: typeDecl[1],
2082
+ line: idx + 1,
2083
+ col,
2084
+ signature: trimmed,
2085
+ scope: packageName
2086
+ });
2087
+ continue;
2088
+ }
2089
+ const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2090
+ if (valueDecl?.[1] && valueDecl[2]) {
2091
+ addFallbackSymbol(symbols, {
2092
+ filePath,
2093
+ lang,
2094
+ kind: valueDecl[1],
2095
+ name: valueDecl[2],
2096
+ line: idx + 1,
2097
+ col,
2098
+ signature: trimmed,
2099
+ scope: packageName
2100
+ });
2101
+ }
2102
+ }
2103
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2104
+ }
2105
+ function addFallbackSymbol(symbols, opts) {
2106
+ symbols.push({
2107
+ id: 0,
2108
+ lang: opts.lang,
2109
+ kind: opts.kind,
2110
+ name: opts.name,
2111
+ file: opts.filePath,
2112
+ line: opts.line,
2113
+ col: opts.col,
2114
+ signature: opts.signature,
2115
+ docComment: "",
2116
+ scope: opts.scope,
2117
+ text: `${opts.name} ${opts.signature}`.trim()
2118
+ });
2119
+ }
2120
+ function hasUnbalancedDelimiters(content) {
2121
+ const pairs = { "(": ")", "[": "]", "{": "}" };
2122
+ const closers = new Set(Object.values(pairs));
2123
+ const stack = [];
2124
+ for (const ch of content) {
2125
+ if (pairs[ch]) {
2126
+ stack.push(pairs[ch]);
2127
+ } else if (closers.has(ch) && stack.pop() !== ch) {
2128
+ return true;
2129
+ }
2130
+ }
2131
+ return stack.length > 0;
2132
+ }
2133
+ async function syncGoParse(filePath, content, lang) {
2134
+ try {
2135
+ let scriptPath = _cachedGoScriptPath;
2136
+ if (!scriptPath) {
2137
+ const tmpDir = await fs6.mkdtemp(path9.join(os3.tmpdir(), "ws-go-parse-"));
2138
+ scriptPath = path9.join(tmpDir, "parse.go");
2139
+ await fs6.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2140
+ _cachedGoScriptPath = scriptPath;
2141
+ }
2142
+ const goBinary = resolveWin32Command("go");
2143
+ const goResult = await new Promise(
2144
+ (resolve4, reject) => {
2145
+ let settled = false;
2146
+ const proc = spawn3(goBinary, ["run", scriptPath], {
2147
+ stdio: ["pipe", "pipe", "pipe"],
2148
+ windowsHide: true
2149
+ });
2150
+ proc.on("error", (err) => {
2151
+ if (settled) return;
2152
+ settled = true;
2153
+ reject(err);
2154
+ });
2155
+ let stdout2 = "";
2156
+ proc.stdout?.on("data", (chunk) => {
2157
+ stdout2 += chunk.toString();
2158
+ });
2159
+ proc.stderr?.resume();
2160
+ proc.stdin?.write(content);
2161
+ proc.stdin?.end();
2162
+ const timer = setTimeout(() => {
2163
+ if (settled) return;
2164
+ settled = true;
2165
+ proc.kill("SIGKILL");
2166
+ reject(new Error("timeout"));
2167
+ }, 15e3);
2168
+ timer.unref?.();
2169
+ proc.on("close", (code2) => {
2170
+ if (settled) return;
2171
+ settled = true;
2172
+ clearTimeout(timer);
2173
+ resolve4({ code: code2, stdout: stdout2 });
2174
+ });
2175
+ }
2176
+ );
2177
+ const { code, stdout } = goResult;
2178
+ if (code !== 0 || !stdout.trim()) {
2179
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2180
+ }
2181
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
2182
+ const symbols = rawSymbols.map((s) => ({
2183
+ id: 0,
2184
+ lang,
2185
+ kind: s.kind,
2186
+ name: s.name,
2187
+ file: filePath,
2188
+ line: s.line,
2189
+ col: s.col,
2190
+ signature: s.signature ?? "",
2191
+ docComment: "",
2192
+ scope: s.scope ?? "",
2193
+ text: `${s.name} ${s.signature ?? ""}`.trim()
2194
+ }));
2195
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
2196
+ } catch {
2197
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2198
+ }
2199
+ }
2200
+ var GO_PARSE_SCRIPT, _cachedGoScriptPath;
2201
+ var init_go_parser = __esm({
2202
+ "src/codebase-index/go-parser.ts"() {
2203
+ "use strict";
2204
+ init_win32_resolve();
2205
+ init_parser_output();
2206
+ init_spawn_gate();
2207
+ init_languages();
2208
+ GO_PARSE_SCRIPT = `
2209
+ package main
2210
+
2211
+ import (
2212
+ "encoding/json"
2213
+ "fmt"
2214
+ "go/ast"
2215
+ "go/parser"
2216
+ "go/token"
2217
+ "io"
2218
+ "os"
2219
+ "strconv"
2220
+ "strings"
2221
+ )
2222
+
2223
+ type Sym struct {
2224
+ Name string \`json:"name"\`
2225
+ Kind string \`json:"kind"\`
2226
+ Line int \`json:"line"\`
2227
+ Col int \`json:"col"\`
2228
+ Signature string \`json:"signature"\`
2229
+ Scope string \`json:"scope"\`
2230
+ }
2231
+
2232
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
2233
+ // yields both. Module is the import path for CallType "import", else empty.
2234
+ type Ref struct {
2235
+ ToName string \`json:"toName"\`
2236
+ CallType string \`json:"callType"\`
2237
+ Line int \`json:"line"\`
2238
+ Module string \`json:"module"\`
2239
+ }
2240
+
2241
+ type Result struct {
2242
+ Symbols []Sym \`json:"symbols"\`
2243
+ Refs []Ref \`json:"refs"\`
2244
+ }
2245
+
2246
+ func emptyResult() string {
2247
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
2248
+ }
2249
+
2250
+ func main() {
2251
+ src, err := io.ReadAll(os.Stdin)
2252
+ if err != nil {
2253
+ fmt.Print(emptyResult())
2254
+ return
2255
+ }
2256
+ fset := token.NewFileSet()
2257
+ node, err := parser.ParseFile(fset, "src.go", src, 0)
2258
+ if err != nil {
2259
+ fmt.Print(emptyResult())
2260
+ return
2261
+ }
2262
+
2263
+ var syms []Sym
2264
+
2265
+ // Package-level scope
2266
+ pkgScope := node.Name.Name
2267
+
2268
+ // Collect all top-level declarations
2269
+ for _, decl := range node.Decls {
2270
+ switch d := decl.(type) {
2271
+ case *ast.FuncDecl:
2272
+ name := d.Name.Name
2273
+ kind := "function"
2274
+ scope := pkgScope
2275
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2276
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
2277
+ kind = "method"
2278
+ } else {
2279
+ scope = pkgScope + "." + name
2280
+ }
2281
+ pos := fset.Position(d.Pos())
2282
+ sig := formatFuncSig(d)
2283
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
2284
+
2285
+ case *ast.GenDecl:
2286
+ for _, spec := range d.Specs {
2287
+ switch s := spec.(type) {
2288
+ case *ast.TypeSpec:
2289
+ name := s.Name.Name
2290
+ pos := fset.Position(s.Pos())
2291
+ sig := "type " + name
2292
+ if s.TypeParams != nil {
2293
+ sig += formatTypeParams(s.TypeParams)
2294
+ }
2295
+ if st, ok := s.Type.(*ast.StructType); ok {
2296
+ sig += " = struct { " + formatFields(st.Fields.List) + " }"
2297
+ } else if it, ok := s.Type.(*ast.InterfaceType); ok {
2298
+ sig += " = interface { " + formatMethods(it.Methods.List) + " }"
2299
+ } else {
2300
+ sig += " = " + formatType(s.Type)
2301
+ }
2302
+ syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2303
+
2304
+ case *ast.ValueSpec:
2305
+ for _, n := range s.Names {
2306
+ name := n.Name
2307
+ pos := fset.Position(n.Pos())
2308
+ kind := "var"
2309
+ if d.Tok == token.CONST {
2310
+ kind = "const"
2311
+ }
2312
+ sig := kind + " " + name
2313
+ if s.Type != nil {
2314
+ sig += " " + formatType(s.Type)
2315
+ }
2316
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2317
+ }
2318
+ }
2319
+ }
2320
+ }
2321
+ }
2322
+
2323
+ refs := []Ref{}
2324
+ ast.Inspect(node, func(n ast.Node) bool {
2325
+ switch expr := n.(type) {
2326
+ case *ast.CallExpr:
2327
+ line := fset.Position(expr.Pos()).Line
2328
+ switch fun := expr.Fun.(type) {
2329
+ case *ast.Ident:
2330
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
2331
+ case *ast.SelectorExpr:
2332
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
2333
+ // declared symbol name, so it resolves the same way the TypeScript
2334
+ // and Python extractors' call refs do.
2335
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
2336
+ }
2337
+ case *ast.ImportSpec:
2338
+ if expr.Path != nil {
2339
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
2340
+ line := fset.Position(expr.Pos()).Line
2341
+ // A Go import names a package, not a symbol; the package's
2342
+ // last path segment is the name it is referenced by.
2343
+ name := importPath
2344
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
2345
+ name = importPath[idx+1:]
2346
+ }
2347
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
2348
+ }
2349
+ }
2350
+ }
2351
+ return true
2352
+ })
2353
+
2354
+ if syms == nil {
2355
+ syms = []Sym{}
2356
+ }
2357
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
2358
+ if err != nil {
2359
+ fmt.Print(emptyResult())
2360
+ return
2361
+ }
2362
+ fmt.Print(string(data))
2363
+ }
2364
+
2365
+ func recvTypeName(t ast.Expr) string {
2366
+ switch v := t.(type) {
2367
+ case *ast.Ident:
2368
+ return v.Name
2369
+ case *ast.StarExpr:
2370
+ return recvTypeName(v.X)
2371
+ default:
2372
+ return "?"
2373
+ }
2374
+ }
2375
+
2376
+ func formatFuncSig(d *ast.FuncDecl) string {
2377
+ scope := ""
2378
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2379
+ scope = "(" + formatFieldList(d.Recv.List) + ") "
2380
+ }
2381
+ scope += formatFuncType(d.Type)
2382
+ return "func " + scope
2383
+ }
2384
+
2385
+ func formatFuncType(f *ast.FuncType) string {
2386
+ params := formatFieldList(f.Params.List)
2387
+ results := ""
2388
+ if f.Results != nil {
2389
+ results = " -> " + formatFieldList(f.Results.List)
2390
+ }
2391
+ return params + results
2392
+ }
2393
+
2394
+ func formatFieldList(fields []*ast.Field) string {
2395
+ if len(fields) == 0 {
2396
+ return "()"
2397
+ }
2398
+ names := make([]string, 0, len(fields))
2399
+ for _, f := range fields {
2400
+ name := ""
2401
+ if len(f.Names) > 0 {
2402
+ name = f.Names[0].Name
2403
+ }
2404
+ t := formatType(f.Type)
2405
+ if name != "" {
2406
+ names = append(names, name+" "+t)
2407
+ } else {
2408
+ names = append(names, t)
2409
+ }
2410
+ }
2411
+ return "(" + strings.Join(names, ", ") + ")"
2412
+ }
2413
+
2414
+ func formatFields(fields []*ast.Field) string {
2415
+ lines := make([]string, 0)
2416
+ for _, f := range fields {
2417
+ name := ""
2418
+ if len(f.Names) > 0 {
2419
+ name = f.Names[0].Name
2420
+ }
2421
+ t := formatType(f.Type)
2422
+ if name != "" {
2423
+ lines = append(lines, name+" "+t)
2424
+ } else {
2425
+ lines = append(lines, t)
2426
+ }
2427
+ }
2428
+ return strings.Join(lines, "; ")
2429
+ }
2430
+
2431
+ func formatMethods(fields []*ast.Field) string {
2432
+ return formatFields(fields)
2433
+ }
2434
+
2435
+ func formatTypeParams(tp *ast.FieldList) string {
2436
+ if tp == nil || len(tp.List) == 0 {
2437
+ return ""
2438
+ }
2439
+ params := make([]string, len(tp.List))
2440
+ for i, p := range tp.List {
2441
+ if len(p.Names) > 0 {
2442
+ params[i] = p.Names[0].Name
2443
+ } else {
2444
+ params[i] = "T"
2445
+ }
2446
+ }
2447
+ return "[" + strings.Join(params, ", ") + "]"
2448
+ }
2449
+
2450
+ func formatType(t ast.Expr) string {
2451
+ if t == nil {
2452
+ return "?"
2453
+ }
2454
+ switch v := t.(type) {
2455
+ case *ast.Ident:
2456
+ return v.Name
2457
+ case *ast.SelectorExpr:
2458
+ return formatType(v.X) + "." + v.Sel.Name
2459
+ case *ast.StarExpr:
2460
+ return "*" + formatType(v.X)
2461
+ case *ast.ArrayType:
2462
+ if v.Len == nil {
2463
+ return "[]" + formatType(v.Elt)
2464
+ }
2465
+ return "[...]" + formatType(v.Elt)
2466
+ case *ast.MapType:
2467
+ return "map[" + formatType(v.Key) + "]" + formatType(v.Value)
2468
+ case *ast.InterfaceType:
2469
+ return "interface{}"
2470
+ case *ast.StructType:
2471
+ return "struct{}"
2472
+ case *ast.FuncType:
2473
+ return formatFuncType(v)
2474
+ case *ast.ChanType:
2475
+ return "chan " + formatType(v.Value)
2476
+ case *ast.BasicLit:
2477
+ return v.Value
2478
+ case *ast.IndexExpr:
2479
+ // Generic instantiation with one type arg, e.g. Logger[int].
2480
+ return formatType(v.X) + "[" + formatType(v.Index) + "]"
2481
+ case *ast.IndexListExpr:
2482
+ // Generic instantiation with multiple type args, e.g. Map[K, V].
2483
+ args := make([]string, len(v.Indices))
2484
+ for i, idx := range v.Indices {
2485
+ args[i] = formatType(idx)
2486
+ }
2487
+ return formatType(v.X) + "[" + strings.Join(args, ", ") + "]"
2488
+ default:
2489
+ return "?"
2490
+ }
2491
+ }
1697
2492
  `;
1698
- _cachedScriptPath = null;
2493
+ _cachedGoScriptPath = null;
1699
2494
  }
1700
2495
  });
1701
2496
 
@@ -1791,7 +2586,7 @@ __export(json_parser_exports, {
1791
2586
  parseSymbols: () => parseSymbols6
1792
2587
  });
1793
2588
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1794
- import * as path9 from "node:path";
2589
+ import * as path10 from "node:path";
1795
2590
  function parseSymbols6(opts) {
1796
2591
  const { file, content, lang } = opts;
1797
2592
  try {
@@ -1803,7 +2598,7 @@ function parseSymbols6(opts) {
1803
2598
  function regexParse2(opts) {
1804
2599
  const { file, content, lang } = opts;
1805
2600
  const symbols = [];
1806
- const basename6 = path9.basename(file).toLowerCase();
2601
+ const basename6 = path10.basename(file).toLowerCase();
1807
2602
  const isPackageJson = basename6 === "package.json";
1808
2603
  const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
1809
2604
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1829,11 +2624,11 @@ function regexParse2(opts) {
1829
2624
  const line = lineFromOffset(offset);
1830
2625
  symbols.push(
1831
2626
  makeSymbol({
1832
- name: path9.basename(file),
2627
+ name: path10.basename(file),
1833
2628
  kind: "object",
1834
2629
  line,
1835
2630
  col: 0,
1836
- signature: `"${path9.basename(file)}" = { ... }`,
2631
+ signature: `"${path10.basename(file)}" = { ... }`,
1837
2632
  file,
1838
2633
  lang
1839
2634
  })
@@ -2161,6 +2956,106 @@ var init_yaml_parser = __esm({
2161
2956
  });
2162
2957
 
2163
2958
  // src/codebase-index/tree-sitter/queries.ts
2959
+ function parseGroupedUse(text) {
2960
+ const open = text.indexOf("{");
2961
+ const close = text.lastIndexOf("}");
2962
+ if (open < 0 || close <= open) return null;
2963
+ const prefix = text.slice(0, open).replace(/[\\/]+$/, "");
2964
+ const out = [];
2965
+ for (const rawMember of text.slice(open + 1, close).split(",")) {
2966
+ let member = rawMember.trim();
2967
+ if (!member) continue;
2968
+ member = member.replace(
2969
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2970
+ ""
2971
+ ).trim();
2972
+ if (!member) continue;
2973
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(member);
2974
+ if (aliasMatch) member = member.slice(0, aliasMatch.index).trim();
2975
+ if (!member) continue;
2976
+ const module = prefix ? `${prefix}\\${member}` : member;
2977
+ const toName = member.split(/[\\/]/).filter(Boolean).pop();
2978
+ if (toName) out.push({ toName, callType: "import", module });
2979
+ }
2980
+ return out.length ? out : null;
2981
+ }
2982
+ function importFromText(prefixes) {
2983
+ return (node) => {
2984
+ let text = node.text.replace(/\s+/g, " ").trim();
2985
+ for (const prefix of prefixes) {
2986
+ if (text.startsWith(prefix)) text = text.slice(prefix.length).trim();
2987
+ }
2988
+ text = text.replace(
2989
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2990
+ ""
2991
+ ).trim();
2992
+ if (text.includes("{")) return parseGroupedUse(text);
2993
+ if (text.includes(",") && !text.includes("<") && !text.includes("=")) {
2994
+ const out = [];
2995
+ for (const clause of text.split(",")) {
2996
+ const one = oneImportClause(clause.trim());
2997
+ if (one) out.push(one);
2998
+ }
2999
+ return out.length ? out : null;
3000
+ }
3001
+ const single = oneImportClause(text);
3002
+ return single ? [single] : null;
3003
+ };
3004
+ }
3005
+ function oneImportClause(rawClause) {
3006
+ let text = rawClause;
3007
+ text = text.replace(
3008
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
3009
+ ""
3010
+ ).trim();
3011
+ text = text.replace(/[;}]+$/g, "").trim();
3012
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(text);
3013
+ if (aliasMatch) text = text.slice(0, aliasMatch.index).trim();
3014
+ const eqMatch = /^([A-Za-z_]\w*)\s*=\s*(.+)$/.exec(text);
3015
+ if (eqMatch) text = eqMatch[2].trim();
3016
+ if (!text) return null;
3017
+ if (text.endsWith("*")) text = text.slice(0, -1).replace(/[.]$/, "");
3018
+ if (!text) return null;
3019
+ const module = text;
3020
+ const toName = module.split(/[.\\/]/).filter(Boolean).pop()?.replace(/<.*>$/s, "");
3021
+ if (!toName) return null;
3022
+ return { toName, callType: "import", module };
3023
+ }
3024
+ function heritageLeaf(node, depth) {
3025
+ if (depth > 6) return null;
3026
+ const named = node.childForFieldName("name");
3027
+ if (named) {
3028
+ if (named.type === "scoped_type_identifier" || named.type === "qualified_name" || named.type === "scope_resolution" || named.type === "user_type") {
3029
+ return heritageLeaf(named, depth + 1);
3030
+ }
3031
+ return named.text;
3032
+ }
3033
+ const children = [];
3034
+ for (let i = 0; i < node.namedChildCount; i++) {
3035
+ const c = node.namedChild(i);
3036
+ if (c) children.push(c);
3037
+ }
3038
+ for (let i = children.length - 1; i >= 0; i--) {
3039
+ const c = children[i];
3040
+ if (c.type === "type_arguments" || c.type === "type_argument_list" || // cpp: (template_type arguments: (template_argument_list …)) — the
3041
+ // descriptor's type_identifier inside it is never the declared base.
3042
+ c.type === "template_argument_list" || c.type === "type_parameter_list" || c.type === "type_projection" || c.type === "value_arguments") {
3043
+ continue;
3044
+ }
3045
+ if (c.type === "type_identifier" || c.type === "identifier" || c.type === "constant" || c.type === "name") {
3046
+ return c.text;
3047
+ }
3048
+ if (c.type === "scoped_type_identifier" || c.type === "qualified_name" || c.type === "scope_resolution" || c.type === "user_type") {
3049
+ return heritageLeaf(c, depth + 1);
3050
+ }
3051
+ }
3052
+ return leafSegment(
3053
+ node.text.replace(/\\/g, ".").replace(/::/g, ".").replace(/<[^<>]*>$/, "")
3054
+ );
3055
+ }
3056
+ function leafSegment(text) {
3057
+ return text.split(".").filter(Boolean).pop() ?? text;
3058
+ }
2164
3059
  function getQueries(lang) {
2165
3060
  return LANG_QUERIES[lang] ?? DEFAULT_QUERIES;
2166
3061
  }
@@ -2172,10 +3067,127 @@ function readFirstString(node) {
2172
3067
  const child = node.namedChild(0);
2173
3068
  return child ? readFirstString(child) : null;
2174
3069
  }
2175
- var DEFAULT_QUERIES, LANG_QUERIES;
3070
+ var heritageExtractor, cCallExtractor, rubyCallExtractor, firstIdentifierCallExtractor, cIncludeExtractor, phpConstructorExtractor, DEFAULT_QUERIES, LANG_QUERIES;
2176
3071
  var init_queries = __esm({
2177
3072
  "src/codebase-index/tree-sitter/queries.ts"() {
2178
3073
  "use strict";
3074
+ heritageExtractor = (node) => {
3075
+ const out = [];
3076
+ const SKIP_SUBTREES = /* @__PURE__ */ new Set([
3077
+ "type_arguments",
3078
+ "type_argument_list",
3079
+ // tree-sitter-cpp names its argument subtree template_argument_list —
3080
+ // verified AST: (base_class_clause (template_type name:
3081
+ // (type_identifier) arguments: (template_argument_list
3082
+ // (type_descriptor type: (type_identifier))))). Without this entry
3083
+ // `class D : Base<Foo>` recurses into the descriptor and emits Foo as a
3084
+ // phantom inherit ref.
3085
+ "template_argument_list",
3086
+ "type_parameter_list",
3087
+ "type_projection",
3088
+ "value_arguments"
3089
+ ]);
3090
+ const collect = (current, depth) => {
3091
+ if (depth > 4) return;
3092
+ for (let i = 0; i < current.namedChildCount; i++) {
3093
+ const child = current.namedChild(i);
3094
+ if (!child) continue;
3095
+ if (SKIP_SUBTREES.has(child.type)) continue;
3096
+ if (child.type === "type_identifier" || child.type === "identifier" || child.type === "named_type" || child.type === "type" || // PHP heritage carries `name`; Ruby a `constant`.
3097
+ child.type === "constant" || child.type === "name") {
3098
+ const name = child.type === "named_type" ? leafSegment(child.text) : child.text;
3099
+ if (name) out.push({ toName: name });
3100
+ continue;
3101
+ }
3102
+ if (child.type === "generic_type" || child.type === "generic_name") {
3103
+ for (let j = 0; j < child.namedChildCount; j++) {
3104
+ const inner = child.namedChild(j);
3105
+ if (inner && !SKIP_SUBTREES.has(inner.type) && (inner.type === "type_identifier" || inner.type === "identifier" || inner.type === "name")) {
3106
+ out.push({ toName: inner.text });
3107
+ break;
3108
+ }
3109
+ }
3110
+ continue;
3111
+ }
3112
+ if (child.type === "qualified_name" || child.type === "scoped_type_identifier" || child.type === "user_type" || child.type === "scope_resolution") {
3113
+ const leaf = heritageLeaf(child, 0);
3114
+ if (leaf) out.push({ toName: leaf });
3115
+ continue;
3116
+ }
3117
+ collect(child, depth + 1);
3118
+ }
3119
+ };
3120
+ collect(node, 0);
3121
+ return out;
3122
+ };
3123
+ cCallExtractor = (node) => {
3124
+ const fn = node.childForFieldName("function");
3125
+ if (!fn) return null;
3126
+ if (fn.type === "field_expression") {
3127
+ const field = fn.childForFieldName("field");
3128
+ if (field) return [{ toName: field.text, callType: "call" }];
3129
+ const seg = fn.text.split("->").filter(Boolean).pop();
3130
+ if (seg) return [{ toName: leafSegment(seg.split(".")[0] ?? seg), callType: "call" }];
3131
+ return null;
3132
+ }
3133
+ if (fn.type === "qualified_identifier") {
3134
+ const name = fn.childForFieldName("name");
3135
+ if (name) return [{ toName: name.text, callType: "call" }];
3136
+ const seg = fn.text.split("::").filter(Boolean).pop();
3137
+ if (seg) return [{ toName: seg.split(/[<(]/)[0].trim(), callType: "call" }];
3138
+ return null;
3139
+ }
3140
+ return [{ toName: fn.text.split(/[<(]/)[0].trim(), callType: "call" }];
3141
+ };
3142
+ rubyCallExtractor = (node) => {
3143
+ const emissions = [];
3144
+ const method = node.childForFieldName("method");
3145
+ if (method) {
3146
+ const name = method.text;
3147
+ if (name && !name.includes(" ")) emissions.push({ toName: name, callType: "call" });
3148
+ if (name === "require" || name === "require_relative") {
3149
+ const args = node.childForFieldName("arguments");
3150
+ const first = args?.namedChild(0);
3151
+ if (first) {
3152
+ const raw = first.text.replace(/^['"]|['"]$/g, "");
3153
+ const toName = raw.split("/").filter(Boolean).pop();
3154
+ if (toName) emissions.push({ toName, callType: "import", module: raw });
3155
+ }
3156
+ }
3157
+ }
3158
+ return emissions;
3159
+ };
3160
+ firstIdentifierCallExtractor = (node) => {
3161
+ for (let i = 0; i < node.namedChildCount; i++) {
3162
+ const child = node.namedChild(i);
3163
+ if (child && (child.type === "simple_identifier" || child.type === "identifier")) {
3164
+ return [{ toName: child.text, callType: "call" }];
3165
+ }
3166
+ }
3167
+ const first = node.namedChild(0);
3168
+ if (!first) return null;
3169
+ const leaf = leafSegment(first.text.split(/[<(]/)[0] ?? first.text);
3170
+ if (!leaf) return null;
3171
+ return [{ toName: leaf, callType: "call" }];
3172
+ };
3173
+ cIncludeExtractor = (node) => {
3174
+ const raw = node.text.replace(/^#\s*include\s*/i, "").trim();
3175
+ const module = raw.replace(/^["'<]|["'>]$/g, "");
3176
+ if (!module) return null;
3177
+ const toName = module.split("/").pop()?.replace(/\.h$/, "");
3178
+ if (!toName) return null;
3179
+ return [{ toName, callType: "import", module }];
3180
+ };
3181
+ phpConstructorExtractor = (node) => {
3182
+ for (let i = 0; i < node.namedChildCount; i++) {
3183
+ const child = node.namedChild(i);
3184
+ if (child && (child.type === "qualified_name" || child.type === "name")) {
3185
+ const leaf = child.text.split(/[\\]/).filter(Boolean).pop();
3186
+ if (leaf) return [{ toName: leaf, callType: "call" }];
3187
+ }
3188
+ }
3189
+ return null;
3190
+ };
2179
3191
  DEFAULT_QUERIES = {
2180
3192
  declKinds: {}
2181
3193
  };
@@ -2208,7 +3220,13 @@ var init_queries = __esm({
2208
3220
  "struct_specifier",
2209
3221
  "union_specifier",
2210
3222
  "enum_specifier"
2211
- ])
3223
+ ]),
3224
+ refRules: {
3225
+ // `obj->run()` and `Cls::stat()` carry structured function fields —
3226
+ // cCallExtractor handles all three AST shapes.
3227
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3228
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor }
3229
+ }
2212
3230
  },
2213
3231
  cpp: {
2214
3232
  declKinds: {
@@ -2239,7 +3257,13 @@ var init_queries = __esm({
2239
3257
  "union_specifier",
2240
3258
  "enum_specifier",
2241
3259
  "namespace_definition"
2242
- ])
3260
+ ]),
3261
+ refRules: {
3262
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3263
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor },
3264
+ // `class Foo : public Bar, private Baz` — the base-class clause.
3265
+ base_class_clause: { callType: "inherit", nameExtractor: heritageExtractor }
3266
+ }
2243
3267
  },
2244
3268
  java: {
2245
3269
  declKinds: {
@@ -2274,7 +3298,22 @@ var init_queries = __esm({
2274
3298
  "interface_declaration",
2275
3299
  "enum_declaration",
2276
3300
  "record_declaration"
2277
- ])
3301
+ ]),
3302
+ refRules: {
3303
+ method_invocation: { callType: "call", field: "name" },
3304
+ object_creation_expression: { callType: "call", field: "type" },
3305
+ // Verified AST: `superclass: (superclass (type_identifier))` and
3306
+ // `interfaces: (super_interfaces (type_list ...))` — no underscores.
3307
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor },
3308
+ super_interfaces: {
3309
+ callType: "implement",
3310
+ nameExtractor: heritageExtractor
3311
+ },
3312
+ import_declaration: {
3313
+ callType: "import",
3314
+ nameExtractor: importFromText(["import "])
3315
+ }
3316
+ }
2278
3317
  },
2279
3318
  csharp: {
2280
3319
  // C# 10+ `namespace Foo.Bar;` produces this node type. The legacy block
@@ -2312,7 +3351,18 @@ var init_queries = __esm({
2312
3351
  "struct_declaration",
2313
3352
  "enum_declaration",
2314
3353
  "record_declaration"
2315
- ])
3354
+ ]),
3355
+ refRules: {
3356
+ // Verified AST: `invocation_expression function: (identifier)` — the
3357
+ // callee field is `function` (C-style), not `name`.
3358
+ invocation_expression: { callType: "call", field: "function" },
3359
+ object_creation_expression: { callType: "call", field: "type" },
3360
+ base_list: { callType: "inherit", nameExtractor: heritageExtractor },
3361
+ using_directive: {
3362
+ callType: "import",
3363
+ nameExtractor: importFromText(["using "])
3364
+ }
3365
+ }
2316
3366
  },
2317
3367
  php: {
2318
3368
  declKinds: {
@@ -2331,7 +3381,7 @@ var init_queries = __esm({
2331
3381
  interface_declaration: "name",
2332
3382
  trait_declaration: "name",
2333
3383
  enum_declaration: "name",
2334
- namespace_declaration: "name"
3384
+ namespace_definition: "name"
2335
3385
  },
2336
3386
  scopeNodes: /* @__PURE__ */ new Set([
2337
3387
  "program",
@@ -2340,7 +3390,24 @@ var init_queries = __esm({
2340
3390
  "interface_declaration",
2341
3391
  "trait_declaration",
2342
3392
  "enum_declaration"
2343
- ])
3393
+ ]),
3394
+ refRules: {
3395
+ function_call_expression: { callType: "call", field: "function" },
3396
+ // Verified AST: `new App\Model\User()` carries a BARE qualified_name
3397
+ // child (no `name:` field), so the field default never fires.
3398
+ object_creation_expression: { callType: "call", nameExtractor: phpConstructorExtractor },
3399
+ base_clause: { callType: "inherit", nameExtractor: heritageExtractor },
3400
+ class_interface_clause: {
3401
+ callType: "implement",
3402
+ nameExtractor: heritageExtractor
3403
+ },
3404
+ // Verified AST: `namespace_use_declaration (namespace_use_clause
3405
+ // (qualified_name ...))` — not `use_declaration`.
3406
+ namespace_use_declaration: {
3407
+ callType: "import",
3408
+ nameExtractor: importFromText(["use "])
3409
+ }
3410
+ }
2344
3411
  },
2345
3412
  // ─── Scripting / mobile ────────────────────────────────────────────────────
2346
3413
  ruby: {
@@ -2358,7 +3425,13 @@ var init_queries = __esm({
2358
3425
  module: "name",
2359
3426
  constant: "name"
2360
3427
  },
2361
- scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"])
3428
+ scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"]),
3429
+ refRules: {
3430
+ // `call` covers both `foo(...)` and `obj.foo(...)` — the extractor
3431
+ // records the method leaf, plus `require`/`require_relative` imports.
3432
+ call: { callType: "call", nameExtractor: rubyCallExtractor },
3433
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor }
3434
+ }
2362
3435
  },
2363
3436
  swift: {
2364
3437
  declKinds: {
@@ -2391,7 +3464,18 @@ var init_queries = __esm({
2391
3464
  "protocol_declaration",
2392
3465
  "actor_declaration",
2393
3466
  "extension_declaration"
2394
- ])
3467
+ ]),
3468
+ refRules: {
3469
+ // Verified AST: `call_expression (simple_identifier) (call_suffix …)` —
3470
+ // the callee is a bare first child, no field name.
3471
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3472
+ // Verified AST: `inheritance_specifier inherits_from: (user_type …)`.
3473
+ inheritance_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3474
+ import_declaration: {
3475
+ callType: "import",
3476
+ nameExtractor: importFromText(["import ", "import type ", "@testable import "])
3477
+ }
3478
+ }
2395
3479
  },
2396
3480
  kotlin: {
2397
3481
  declKinds: {
@@ -2416,7 +3500,17 @@ var init_queries = __esm({
2416
3500
  "object_declaration",
2417
3501
  "interface_declaration",
2418
3502
  "function_declaration"
2419
- ])
3503
+ ]),
3504
+ refRules: {
3505
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3506
+ // Verified AST: `delegation_specifier (user_type (type_identifier))` —
3507
+ // the `: Handler` / `: Base()` clause.
3508
+ delegation_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3509
+ import_header: {
3510
+ callType: "import",
3511
+ nameExtractor: importFromText(["import "])
3512
+ }
3513
+ }
2420
3514
  },
2421
3515
  elixir: {
2422
3516
  declKinds: {
@@ -2488,7 +3582,38 @@ function visitTree(tree, content, file, lang, queries) {
2488
3582
  const boundedContent = content.length > TREE_SITTER_MAX_FILE_CHARS ? content.slice(0, TREE_SITTER_MAX_FILE_CHARS) : content;
2489
3583
  const nlOffsets = newlineOffsets3(boundedContent);
2490
3584
  const symbols = [];
3585
+ const refs = [];
3586
+ const seenRefs = /* @__PURE__ */ new Set();
2491
3587
  const scopeStack = [];
3588
+ function emitRefsForNode(node, rule) {
3589
+ const emissions = rule.nameExtractor?.(node) ?? defaultRefTarget(node, rule);
3590
+ if (!emissions) return;
3591
+ const { line } = lineColAt2(nlOffsets, node.startIndex);
3592
+ for (const emission of emissions) {
3593
+ if (!emission.toName) continue;
3594
+ const callType = emission.callType ?? rule.callType;
3595
+ const key = `${emission.toName}:${callType}:${line}:${emission.module ?? ""}:${node.startIndex}`;
3596
+ if (seenRefs.has(key)) continue;
3597
+ seenRefs.add(key);
3598
+ refs.push({
3599
+ fromId: 0,
3600
+ // assignRefsToSymbols attaches owners after insertion
3601
+ toName: emission.toName.slice(0, 200),
3602
+ callType,
3603
+ line,
3604
+ lang,
3605
+ module: emission.module
3606
+ });
3607
+ }
3608
+ }
3609
+ function defaultRefTarget(node, rule) {
3610
+ if (!rule.field) return null;
3611
+ const field = node.childForFieldName(rule.field);
3612
+ if (!field) return null;
3613
+ const leaf = field.text.split(/[.:\\]/).filter(Boolean).pop()?.split(/[<(]/)[0];
3614
+ if (!leaf) return null;
3615
+ return [{ toName: leaf.trim() }];
3616
+ }
2492
3617
  function visit(node, depth) {
2493
3618
  if (symbols.length >= TREE_SITTER_MAX_SYMBOLS) return;
2494
3619
  if (node.isMissing || node.isError) {
@@ -2507,6 +3632,8 @@ function visitTree(tree, content, file, lang, queries) {
2507
3632
  );
2508
3633
  if (emitted) symbols.push(emitted);
2509
3634
  }
3635
+ const refRule = queries.refRules?.[node.type];
3636
+ if (refRule) emitRefsForNode(node, refRule);
2510
3637
  }
2511
3638
  const pushesScope = queries.scopeNodes?.has(node.type) ?? false;
2512
3639
  const pushIdx = pushesScope ? pushScope(scopeStack, node, queries) : -1;
@@ -2524,7 +3651,7 @@ function visitTree(tree, content, file, lang, queries) {
2524
3651
  if (pushIdx !== -1) scopeStack.pop();
2525
3652
  }
2526
3653
  visit(tree.rootNode, 0);
2527
- return { symbols };
3654
+ return { symbols, refs };
2528
3655
  }
2529
3656
  function pushScope(scopeStack, node, queries) {
2530
3657
  const name = extractName(node, queries);
@@ -2611,7 +3738,7 @@ __export(tree_sitter_parser_exports, {
2611
3738
  parseSymbols: () => parseSymbols8,
2612
3739
  parseTreeSitterAst: () => parseTreeSitterAst
2613
3740
  });
2614
- import * as path10 from "node:path";
3741
+ import * as path11 from "node:path";
2615
3742
  import { fileURLToPath } from "node:url";
2616
3743
  function optInEnabled(env) {
2617
3744
  return process.env[env] === "1" || process.env[env] === "true";
@@ -2634,7 +3761,7 @@ async function loadLanguage(lang) {
2634
3761
  if (!grammarName) {
2635
3762
  throw new Error(`tree-sitter: no grammar registered for lang "${lang}"`);
2636
3763
  }
2637
- const wasmPath = path10.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
3764
+ const wasmPath = path11.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
2638
3765
  const { Language, init } = await getRuntime();
2639
3766
  await init();
2640
3767
  const languageObj = await Language.load(wasmPath);
@@ -2655,7 +3782,7 @@ function isTreeSitterSupported(lang) {
2655
3782
  function getGrammarWasmPath(lang) {
2656
3783
  const name = resolveGrammarName(lang);
2657
3784
  if (!name) return void 0;
2658
- return path10.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
3785
+ return path11.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
2659
3786
  }
2660
3787
  async function parseSymbols8(opts) {
2661
3788
  const { file, content, lang } = opts;
@@ -2671,89 +3798,229 @@ async function parseSymbols8(opts) {
2671
3798
  if (!tree) {
2672
3799
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2673
3800
  }
2674
- const { symbols } = visitTree(tree, content, file, lang, getQueries(lang));
2675
- parser.delete();
2676
- tree.delete();
2677
- return { file, lang, symbols, refs: [], mtimeMs: Date.now() };
2678
- } catch {
2679
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2680
- }
2681
- }
2682
- async function loadTreeSitterLanguage(lang) {
2683
- const cached = await loadLanguage(lang);
2684
- return cached.Language;
2685
- }
2686
- async function __smokeRootType(opts) {
2687
- if (!isTreeSitterSupported(opts.lang)) {
2688
- throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
2689
- }
2690
- const { Parser } = await getRuntime();
2691
- const cached = await loadLanguage(opts.lang);
2692
- const parser = new Parser();
2693
- parser.setLanguage(cached.Language);
2694
- let tree = null;
2695
- try {
2696
- tree = parser.parse(opts.content);
2697
- if (!tree) throw new Error("tree-sitter: parser.parse returned null");
2698
- return tree.rootNode.type;
2699
- } finally {
2700
- tree?.delete();
2701
- parser.delete();
3801
+ const { symbols, refs } = visitTree(tree, content, file, lang, getQueries(lang));
3802
+ parser.delete();
3803
+ tree.delete();
3804
+ return { file, lang, symbols, refs, mtimeMs: Date.now() };
3805
+ } catch {
3806
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
3807
+ }
3808
+ }
3809
+ async function loadTreeSitterLanguage(lang) {
3810
+ const cached = await loadLanguage(lang);
3811
+ return cached.Language;
3812
+ }
3813
+ async function __smokeRootType(opts) {
3814
+ if (!isTreeSitterSupported(opts.lang)) {
3815
+ throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
3816
+ }
3817
+ const { Parser } = await getRuntime();
3818
+ const cached = await loadLanguage(opts.lang);
3819
+ const parser = new Parser();
3820
+ parser.setLanguage(cached.Language);
3821
+ let tree = null;
3822
+ try {
3823
+ tree = parser.parse(opts.content);
3824
+ if (!tree) throw new Error("tree-sitter: parser.parse returned null");
3825
+ return tree.rootNode.type;
3826
+ } finally {
3827
+ tree?.delete();
3828
+ parser.delete();
3829
+ }
3830
+ }
3831
+ async function parseTreeSitterAst(opts) {
3832
+ const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
3833
+ if (!grammar) return null;
3834
+ try {
3835
+ const { Parser, Language, init } = await getRuntime();
3836
+ await init();
3837
+ const wasmPath = path11.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
3838
+ const languageObj = await Language.load(wasmPath);
3839
+ const parser = new Parser();
3840
+ parser.setLanguage(languageObj);
3841
+ const tree = parser.parse(opts.content);
3842
+ if (!tree) {
3843
+ parser.delete();
3844
+ return null;
3845
+ }
3846
+ return { tree, parser };
3847
+ } catch {
3848
+ return null;
3849
+ }
3850
+ }
3851
+ var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
3852
+ var init_tree_sitter_parser = __esm({
3853
+ "src/codebase-index/tree-sitter-parser.ts"() {
3854
+ "use strict";
3855
+ init_queries();
3856
+ init_visitor();
3857
+ WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
3858
+ RUNTIME_WASM = path11.join(WASM_DIR, "tree-sitter-runtime.wasm");
3859
+ LANG_TO_GRAMMAR = {
3860
+ c: "c",
3861
+ cpp: "cpp",
3862
+ java: "java",
3863
+ csharp: "c_sharp",
3864
+ // tree-sitter directory uses underscore
3865
+ php: "php",
3866
+ ruby: "ruby",
3867
+ swift: "swift",
3868
+ kotlin: "kotlin",
3869
+ shell: "bash",
3870
+ // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
3871
+ // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
3872
+ elixir: "elixir"
3873
+ };
3874
+ GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
3875
+ PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
3876
+ RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
3877
+ runtimePromise = null;
3878
+ languageCache = /* @__PURE__ */ new Map();
3879
+ }
3880
+ });
3881
+
3882
+ // src/codebase-index/parser-dispatch.ts
3883
+ var parser_dispatch_exports = {};
3884
+ __export(parser_dispatch_exports, {
3885
+ parseFileContent: () => parseFileContent,
3886
+ parseFilesContent: () => parseFilesContent
3887
+ });
3888
+ async function parseFileContent(file, content, lang) {
3889
+ const parsed = await dispatch(file, content, lang);
3890
+ return withRelations(parsed, content, lang);
3891
+ }
3892
+ async function parseFilesContent(files) {
3893
+ if (files.length === 0) return [];
3894
+ const slots = files.map(() => ({ result: null }));
3895
+ const batchingEnabled = process.env["WRONGSTACK_TOOLCHAIN_BATCH"] !== "0";
3896
+ if (batchingEnabled) {
3897
+ const goFiles = [];
3898
+ const pyFiles = [];
3899
+ files.forEach((f, index) => {
3900
+ if (f.lang === "go") goFiles.push({ ...f, index });
3901
+ else if (f.lang === "py") pyFiles.push({ ...f, index });
3902
+ });
3903
+ if (goFiles.length > 0) {
3904
+ await applyBatchResults(slots, goFiles, (chunks) => runGoBatch(chunks), "go");
3905
+ }
3906
+ if (pyFiles.length > 0) {
3907
+ const pyBinary = await resolvePythonBinary();
3908
+ if (pyBinary) {
3909
+ await applyBatchResults(slots, pyFiles, (chunks) => runPyBatch(chunks, pyBinary), "py");
3910
+ }
3911
+ }
3912
+ }
3913
+ const jobs = [];
3914
+ for (let i = 0; i < files.length; i++) {
3915
+ if (slots[i].result !== null) continue;
3916
+ const { file, content, lang } = files[i];
3917
+ const slot = slots[i];
3918
+ jobs.push(
3919
+ (async () => {
3920
+ try {
3921
+ slot.result = await parseFileContent(file, content, lang);
3922
+ } catch (err) {
3923
+ slot.error = err instanceof Error ? err.message : String(err);
3924
+ }
3925
+ })()
3926
+ );
3927
+ }
3928
+ await Promise.all(jobs);
3929
+ return slots;
3930
+ }
3931
+ async function applyBatchResults(slots, batchFiles, runBatch, lang) {
3932
+ for (const chunk of chunkBatchFiles(batchFiles)) {
3933
+ let byFile = null;
3934
+ try {
3935
+ byFile = await runBatch(chunk);
3936
+ } catch {
3937
+ byFile = null;
3938
+ }
3939
+ if (!byFile) continue;
3940
+ for (const item of chunk) {
3941
+ const parsed = byFile.get(item.file);
3942
+ if (!parsed) continue;
3943
+ slots[item.index] = { result: withRelations(parsed, item.content, lang) };
3944
+ }
3945
+ }
3946
+ }
3947
+ async function dispatch(file, content, lang) {
3948
+ switch (lang) {
3949
+ case "ts":
3950
+ case "tsx":
3951
+ case "js":
3952
+ case "jsx": {
3953
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3954
+ return parseSymbols9({ file, content, lang });
3955
+ }
3956
+ case "go": {
3957
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3958
+ return parseSymbols9({ file, content, lang: "go" });
3959
+ }
3960
+ case "py": {
3961
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3962
+ return parseSymbols9({ file, content, lang: "py" });
3963
+ }
3964
+ case "rs": {
3965
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3966
+ return parseSymbols9({ file, content, lang: "rs" });
3967
+ }
3968
+ case "json": {
3969
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3970
+ return parseSymbols9({ file, content, lang: "json" });
3971
+ }
3972
+ case "yaml": {
3973
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3974
+ return parseSymbols9({ file, content, lang: "yaml" });
3975
+ }
3976
+ // Phase 1: ten languages now route through the Tree-Sitter WASM
3977
+ // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3978
+ // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3979
+ // or the parser returns zero symbols — preserving the indexable-file
3980
+ // contract that "missing a parser must never mean skipping the file".
3981
+ case "c":
3982
+ case "cpp":
3983
+ case "java":
3984
+ case "csharp":
3985
+ case "php":
3986
+ case "ruby":
3987
+ case "swift":
3988
+ case "kotlin":
3989
+ case "shell":
3990
+ case "elixir": {
3991
+ try {
3992
+ const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
3993
+ const parsed = await parseSymbols10({ file, content, lang });
3994
+ if (parsed.symbols.length > 0) return parsed;
3995
+ } catch {
3996
+ }
3997
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
3998
+ return parseSymbols9({ file, content, lang });
3999
+ }
4000
+ default: {
4001
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4002
+ return parseSymbols9({ file, content, lang });
4003
+ }
2702
4004
  }
2703
4005
  }
2704
- async function parseTreeSitterAst(opts) {
2705
- const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
2706
- if (!grammar) return null;
2707
- try {
2708
- const { Parser, Language, init } = await getRuntime();
2709
- await init();
2710
- const wasmPath = path10.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
2711
- const languageObj = await Language.load(wasmPath);
2712
- const parser = new Parser();
2713
- parser.setLanguage(languageObj);
2714
- const tree = parser.parse(opts.content);
2715
- if (!tree) {
2716
- parser.delete();
2717
- return null;
2718
- }
2719
- return { tree, parser };
2720
- } catch {
2721
- return null;
4006
+ function withRelations(parsed, content, lang) {
4007
+ let refs = parsed.refs ?? [];
4008
+ if (refs.length === 0 && hasImportPatterns(lang)) {
4009
+ refs = extractImports({ content, lang });
2722
4010
  }
4011
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
2723
4012
  }
2724
- var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
2725
- var init_tree_sitter_parser = __esm({
2726
- "src/codebase-index/tree-sitter-parser.ts"() {
4013
+ var init_parser_dispatch = __esm({
4014
+ "src/codebase-index/parser-dispatch.ts"() {
2727
4015
  "use strict";
2728
- init_queries();
2729
- init_visitor();
2730
- WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
2731
- RUNTIME_WASM = path10.join(WASM_DIR, "tree-sitter-runtime.wasm");
2732
- LANG_TO_GRAMMAR = {
2733
- c: "c",
2734
- cpp: "cpp",
2735
- java: "java",
2736
- csharp: "c_sharp",
2737
- // tree-sitter directory uses underscore
2738
- php: "php",
2739
- ruby: "ruby",
2740
- swift: "swift",
2741
- kotlin: "kotlin",
2742
- shell: "bash",
2743
- // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
2744
- // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
2745
- elixir: "elixir"
2746
- };
2747
- GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
2748
- PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
2749
- RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
2750
- runtimePromise = null;
2751
- languageCache = /* @__PURE__ */ new Map();
4016
+ init_import_extractor();
4017
+ init_parser_batch();
4018
+ init_py_parser();
2752
4019
  }
2753
4020
  });
2754
4021
 
2755
4022
  // src/read.ts
2756
- import * as fs14 from "node:fs/promises";
4023
+ import * as fs15 from "node:fs/promises";
2757
4024
  import { FsError, ToolValidationError } from "@wrongstack/core/types";
2758
4025
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
2759
4026
 
@@ -2829,7 +4096,7 @@ function isBinaryBuffer(buf) {
2829
4096
  }
2830
4097
 
2831
4098
  // src/codebase-index/background-indexer.ts
2832
- import * as fs13 from "node:fs";
4099
+ import * as fs14 from "node:fs";
2833
4100
  import { fileURLToPath as fileURLToPath6 } from "node:url";
2834
4101
  import { Worker as Worker2 } from "node:worker_threads";
2835
4102
 
@@ -2914,9 +4181,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
2914
4181
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
2915
4182
  import { execFile } from "node:child_process";
2916
4183
  import { createHash as createHash2 } from "node:crypto";
2917
- import * as fs9 from "node:fs/promises";
4184
+ import * as fs10 from "node:fs/promises";
2918
4185
  import { availableParallelism } from "node:os";
2919
- import * as path13 from "node:path";
4186
+ import * as path14 from "node:path";
2920
4187
  import {
2921
4188
  DEFAULT_WALK_IGNORE_DIRS,
2922
4189
  indexParallelBatchSize,
@@ -3591,276 +4858,89 @@ var ModuleResolver = class {
3591
4858
  else if (segment !== "self") break;
3592
4859
  }
3593
4860
  const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
3594
- return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
3595
- }
3596
- const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
3597
- const crate = head === "crate" ? owningCrate : this.structure.roots.find(
3598
- (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
3599
- );
3600
- if (!crate) {
3601
- return this.lookupWithExtensions(
3602
- path5.posix.join(path5.posix.dirname(fromFile), ...segments),
3603
- "rs"
3604
- );
3605
- }
3606
- const rest = segments.slice(1);
3607
- for (const base of crate.sourceRoots) {
3608
- const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
3609
- const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
3610
- const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
3611
- if (hit) return hit;
3612
- }
3613
- return void 0;
3614
- }
3615
- /** `com.example.Thing` and `com.example.*` against JVM source roots. */
3616
- resolveJvm(spec) {
3617
- const segments = spec.split(".").filter(Boolean);
3618
- if (segments.length === 0) return void 0;
3619
- const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
3620
- const wildcard = segments[segments.length - 1] === "*";
3621
- const parts = wildcard ? segments.slice(0, -1) : segments;
3622
- for (const base of [...sourceRoots, this.structure.projectRoot]) {
3623
- const target = path5.posix.join(base, ...parts);
3624
- const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
3625
- if (hit) return hit;
3626
- }
3627
- return void 0;
3628
- }
3629
- /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
3630
- resolveInclude(fromFile, spec) {
3631
- const relative3 = this.lookupWithExtensions(
3632
- path5.posix.join(path5.posix.dirname(fromFile), spec),
3633
- "c"
3634
- );
3635
- if (relative3) return relative3;
3636
- for (const base of [
3637
- path5.posix.join(this.structure.projectRoot, "include"),
3638
- this.structure.projectRoot
3639
- ]) {
3640
- const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
3641
- if (hit) return hit;
3642
- }
3643
- return void 0;
3644
- }
3645
- /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
3646
- resolveRuby(fromFile, spec) {
3647
- const relative3 = this.lookupWithExtensions(
3648
- path5.posix.join(path5.posix.dirname(fromFile), spec),
3649
- "ruby"
3650
- );
3651
- if (relative3) return relative3;
3652
- for (const base of [
3653
- path5.posix.join(this.structure.projectRoot, "lib"),
3654
- this.structure.projectRoot
3655
- ]) {
3656
- const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
3657
- if (hit) return hit;
3658
- }
3659
- return void 0;
3660
- }
3661
- };
3662
-
3663
- // src/codebase-index/import-extractor.ts
3664
- var IMPORT_MAX_FILE_CHARS = 512 * 1024;
3665
- var IMPORT_MAX_PER_FILE = 400;
3666
- var DOTTED_IMPORT = [
3667
- { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
3668
- ];
3669
- var LANG_IMPORTS = {
3670
- // Go and Python have real AST extractors; these patterns are the fallback for
3671
- // machines with no Go toolchain or Python interpreter installed, where the
3672
- // parser degrades to regex symbols and would otherwise contribute no edges.
3673
- go: [
3674
- { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
3675
- // Grouped form: inside `import ( … )` each line is an optional alias plus a
3676
- // quoted path. A stray match elsewhere resolves to no file and is dropped.
3677
- { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
3678
- ],
3679
- py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
3680
- rs: [
3681
- // use a::b::C; | use a::b::{C, D}; → the path before any brace
3682
- { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
3683
- // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
3684
- { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
3685
- ],
3686
- java: DOTTED_IMPORT,
3687
- kotlin: DOTTED_IMPORT,
3688
- scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
3689
- csharp: [
3690
- // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
3691
- { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
3692
- { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
3693
- ],
3694
- // Quoted includes only: <stdio.h> is a system header with no indexed file.
3695
- c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3696
- cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3697
- ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
3698
- php: [
3699
- // `use A\B\C` imports the class C, which is what the index has a symbol
3700
- // for — the namespace symbol only covers the `A\B` prefix.
3701
- { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
3702
- { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
3703
- ],
3704
- swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
3705
- dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
3706
- lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
3707
- elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
3708
- haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
3709
- zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
3710
- proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
3711
- // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
3712
- css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
3713
- // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
3714
- vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3715
- svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3716
- html: [
3717
- { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
3718
- { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
3719
- ],
3720
- shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
3721
- r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
3722
- };
3723
- function lastSegment(specifier) {
3724
- const pathLike = /[/\\]|::/.test(specifier);
3725
- const segments = specifier.split(/[/\\]|::/).filter(Boolean);
3726
- let last = segments[segments.length - 1] ?? specifier;
3727
- if (last === "*" || last === "_") {
3728
- last = segments[segments.length - 2] ?? specifier;
3729
- }
3730
- if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
3731
- const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
3732
- return dotted[dotted.length - 1] ?? last;
3733
- }
3734
- function newlineOffsets(content) {
3735
- const offsets = [];
3736
- for (let i = 0; i < content.length; i++) {
3737
- if (content.charCodeAt(i) === 10) offsets.push(i);
3738
- }
3739
- return offsets;
3740
- }
3741
- function lineAt(offsets, index) {
3742
- let low = 0;
3743
- let high = offsets.length;
3744
- while (low < high) {
3745
- const mid = low + high >>> 1;
3746
- if ((offsets[mid] ?? 0) < index) low = mid + 1;
3747
- else high = mid;
3748
- }
3749
- return low + 1;
3750
- }
3751
- function hasImportPatterns(lang) {
3752
- return LANG_IMPORTS[lang] !== void 0;
3753
- }
3754
- function extractImports(opts) {
3755
- const patterns = LANG_IMPORTS[opts.lang];
3756
- if (!patterns || !opts.content) return [];
3757
- const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
3758
- const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
3759
- const refs = [];
3760
- const seen = /* @__PURE__ */ new Set();
3761
- const offsets = newlineOffsets(content);
3762
- for (const pattern of patterns) {
3763
- const re = new RegExp(pattern.re.source, pattern.re.flags);
3764
- for (const match of content.matchAll(re)) {
3765
- if (refs.length >= limit) return refs;
3766
- const specifier = match[1]?.trim();
3767
- if (!specifier) continue;
3768
- const module = specifier;
3769
- const toName = pattern.name === "full" ? module : lastSegment(module);
3770
- if (!toName) continue;
3771
- const key = `${module}\0${toName}`;
3772
- if (seen.has(key)) continue;
3773
- seen.add(key);
3774
- refs.push({
3775
- fromId: 0,
3776
- toName,
3777
- callType: "import",
3778
- line: lineAt(offsets, match.index ?? 0),
3779
- lang: opts.lang,
3780
- module
3781
- });
3782
- }
3783
- }
3784
- return refs;
3785
- }
3786
-
3787
- // src/codebase-index/parser-dispatch.ts
3788
- async function parseFileContent(file, content, lang) {
3789
- const parsed = await dispatch(file, content, lang);
3790
- return withRelations(parsed, content, lang);
3791
- }
3792
- async function dispatch(file, content, lang) {
3793
- switch (lang) {
3794
- case "ts":
3795
- case "tsx":
3796
- case "js":
3797
- case "jsx": {
3798
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3799
- return parseSymbols9({ file, content, lang });
3800
- }
3801
- case "go": {
3802
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3803
- return parseSymbols9({ file, content, lang: "go" });
3804
- }
3805
- case "py": {
3806
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3807
- return parseSymbols9({ file, content, lang: "py" });
3808
- }
3809
- case "rs": {
3810
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3811
- return parseSymbols9({ file, content, lang: "rs" });
3812
- }
3813
- case "json": {
3814
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3815
- return parseSymbols9({ file, content, lang: "json" });
4861
+ return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
3816
4862
  }
3817
- case "yaml": {
3818
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3819
- return parseSymbols9({ file, content, lang: "yaml" });
4863
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
4864
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
4865
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
4866
+ );
4867
+ if (!crate) {
4868
+ return this.lookupWithExtensions(
4869
+ path5.posix.join(path5.posix.dirname(fromFile), ...segments),
4870
+ "rs"
4871
+ );
3820
4872
  }
3821
- // Phase 1: ten languages now route through the Tree-Sitter WASM
3822
- // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3823
- // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3824
- // or the parser returns zero symbols — preserving the indexable-file
3825
- // contract that "missing a parser must never mean skipping the file".
3826
- case "c":
3827
- case "cpp":
3828
- case "java":
3829
- case "csharp":
3830
- case "php":
3831
- case "ruby":
3832
- case "swift":
3833
- case "kotlin":
3834
- case "shell":
3835
- case "elixir": {
3836
- try {
3837
- const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
3838
- const parsed = await parseSymbols10({ file, content, lang });
3839
- if (parsed.symbols.length > 0) return parsed;
3840
- } catch {
3841
- }
3842
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
3843
- return parseSymbols9({ file, content, lang });
4873
+ const rest = segments.slice(1);
4874
+ for (const base of crate.sourceRoots) {
4875
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
4876
+ const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
4877
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
4878
+ if (hit) return hit;
3844
4879
  }
3845
- default: {
3846
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
3847
- return parseSymbols9({ file, content, lang });
4880
+ return void 0;
4881
+ }
4882
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
4883
+ resolveJvm(spec) {
4884
+ const segments = spec.split(".").filter(Boolean);
4885
+ if (segments.length === 0) return void 0;
4886
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
4887
+ const wildcard = segments[segments.length - 1] === "*";
4888
+ const parts = wildcard ? segments.slice(0, -1) : segments;
4889
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
4890
+ const target = path5.posix.join(base, ...parts);
4891
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
4892
+ if (hit) return hit;
3848
4893
  }
4894
+ return void 0;
3849
4895
  }
3850
- }
3851
- function withRelations(parsed, content, lang) {
3852
- let refs = parsed.refs ?? [];
3853
- if (refs.length === 0 && hasImportPatterns(lang)) {
3854
- refs = extractImports({ content, lang });
4896
+ /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
4897
+ resolveInclude(fromFile, spec) {
4898
+ const relative3 = this.lookupWithExtensions(
4899
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
4900
+ "c"
4901
+ );
4902
+ if (relative3) return relative3;
4903
+ for (const base of [
4904
+ path5.posix.join(this.structure.projectRoot, "include"),
4905
+ this.structure.projectRoot
4906
+ ]) {
4907
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
4908
+ if (hit) return hit;
4909
+ }
4910
+ return void 0;
3855
4911
  }
3856
- return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
3857
- }
4912
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
4913
+ resolveRuby(fromFile, spec) {
4914
+ const relative3 = this.lookupWithExtensions(
4915
+ path5.posix.join(path5.posix.dirname(fromFile), spec),
4916
+ "ruby"
4917
+ );
4918
+ if (relative3) return relative3;
4919
+ for (const base of [
4920
+ path5.posix.join(this.structure.projectRoot, "lib"),
4921
+ this.structure.projectRoot
4922
+ ]) {
4923
+ const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
4924
+ if (hit) return hit;
4925
+ }
4926
+ return void 0;
4927
+ }
4928
+ };
4929
+
4930
+ // src/codebase-index/indexer.ts
4931
+ init_parser_dispatch();
3858
4932
 
3859
4933
  // src/codebase-index/parser-worker-pool.ts
4934
+ import * as fs7 from "node:fs";
4935
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
3860
4936
  import { Worker } from "node:worker_threads";
3861
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3862
- import * as fs6 from "node:fs";
3863
4937
  var WORKER_POOL_THRESHOLD = 500;
4938
+ function resolveWorkerPoolThreshold() {
4939
+ const raw = process.env["WRONGSTACK_INDEX_WORKER_THRESHOLD"];
4940
+ if (raw === void 0) return WORKER_POOL_THRESHOLD;
4941
+ if (!/^\d+$/.test(raw)) return WORKER_POOL_THRESHOLD;
4942
+ return Number.parseInt(raw, 10);
4943
+ }
3864
4944
  var ParserWorkerPool = class {
3865
4945
  constructor(maxWorkers = defaultWorkerCount()) {
3866
4946
  this.maxWorkers = maxWorkers;
@@ -3904,7 +4984,8 @@ var ParserWorkerPool = class {
3904
4984
  w.unref();
3905
4985
  w.on("message", (msg) => this.handleMessage(msg));
3906
4986
  w.on("error", (err) => this.handleError(err, w));
3907
- this.workers.push({ worker: w, busy: false });
4987
+ w.on("exit", () => this.retireByReference(w));
4988
+ this.workers.push({ worker: w, workerId: w.threadId, busy: false });
3908
4989
  } catch {
3909
4990
  if (this.workers.length === 0) {
3910
4991
  this.unavailable = true;
@@ -3920,7 +5001,7 @@ var ParserWorkerPool = class {
3920
5001
  }
3921
5002
  /**
3922
5003
  * Parse files in parallel across the worker pool. Returns a flat
3923
- * `FileSymbols[]` in completion order (caller sorts if needed).
5004
+ * `FileSymbols[]` in completion order (caller matches by file path).
3924
5005
  *
3925
5006
  * Content is pre-read by the main thread (for the content-hash check)
3926
5007
  * and passed to workers to avoid a second disk read. Files are
@@ -3941,21 +5022,19 @@ var ParserWorkerPool = class {
3941
5022
  chunks[i % workerCount].push(files[i]);
3942
5023
  }
3943
5024
  return new Promise((resolve4, reject) => {
5025
+ const pendingChunks = /* @__PURE__ */ new Map();
3944
5026
  this.pending.set(batchId, {
3945
5027
  resolve: resolve4,
3946
5028
  reject,
3947
5029
  accumulated: [],
3948
- expectedWorkers: workerCount,
3949
- completedWorkers: 0
5030
+ pendingChunks,
5031
+ settled: false
3950
5032
  });
3951
5033
  for (let i = 0; i < workerCount; i++) {
3952
5034
  const pw = this.workers[i];
3953
5035
  pw.busy = true;
3954
- pw.worker.postMessage({
3955
- type: "parse",
3956
- id: batchId,
3957
- files: chunks[i]
3958
- });
5036
+ pendingChunks.set(pw.workerId, chunks[i]);
5037
+ pw.worker.postMessage({ type: "parse", id: batchId, files: chunks[i] });
3959
5038
  }
3960
5039
  });
3961
5040
  }
@@ -3964,6 +5043,12 @@ var ParserWorkerPool = class {
3964
5043
  const workers = this.workers.map((w) => w.worker);
3965
5044
  this.workers = [];
3966
5045
  this.unavailable = false;
5046
+ for (const [, p] of this.pending) {
5047
+ if (p.settled) continue;
5048
+ p.settled = true;
5049
+ p.reject(new Error("ParserWorkerPool shut down"));
5050
+ }
5051
+ this.pending.clear();
3967
5052
  for (const w of workers) {
3968
5053
  try {
3969
5054
  w.postMessage({ type: "shutdown" });
@@ -3984,39 +5069,117 @@ var ParserWorkerPool = class {
3984
5069
  })
3985
5070
  )
3986
5071
  );
3987
- for (const [, p] of this.pending) p.reject(new Error("ParserWorkerPool shut down"));
3988
- this.pending.clear();
3989
5072
  }
3990
5073
  handleMessage(msg) {
3991
5074
  const batch = this.pending.get(msg.id);
3992
5075
  if (!batch) return;
5076
+ const worker2 = this.workers.find((w) => w.workerId === msg.workerId);
5077
+ if (worker2) worker2.busy = false;
5078
+ batch.pendingChunks.delete(msg.workerId);
3993
5079
  batch.accumulated.push(...msg.results);
3994
- batch.completedWorkers++;
3995
- const freeWorker = this.workers.find((w) => w.busy);
3996
- if (freeWorker) freeWorker.busy = false;
3997
- if (batch.completedWorkers >= batch.expectedWorkers) {
5080
+ if (batch.pendingChunks.size === 0) {
5081
+ if (batch.settled) return;
5082
+ batch.settled = true;
3998
5083
  this.pending.delete(msg.id);
3999
5084
  batch.resolve(batch.accumulated);
4000
5085
  }
4001
5086
  }
4002
- handleError(err, source) {
4003
- this.workers = this.workers.filter((w) => w.worker !== source);
4004
- if (this.workers.length === 0) {
4005
- for (const [, p] of this.pending) p.reject(err);
4006
- this.pending.clear();
4007
- this.unavailable = true;
5087
+ /**
5088
+ * Remove a worker from the pool and salvage any chunk it still owed.
5089
+ *
5090
+ * Idempotent by workerId `error` and `exit` can both fire for one
5091
+ * death, and a worker may die while no batch references it. When the
5092
+ * dead worker owed files to an in-flight batch and other workers remain,
5093
+ * those files are re-parsed inline on this thread (one fewer worker
5094
+ * should cost latency, not correctness). When it was the last worker,
5095
+ * every remaining batch rejects so the indexer's existing inline
5096
+ * fallback takes over the whole batch.
5097
+ */
5098
+ retireWorker(workerId) {
5099
+ const entry = this.workers.find((w) => w.workerId === workerId);
5100
+ if (!entry) return;
5101
+ this.workers = this.workers.filter((w) => w.workerId !== workerId);
5102
+ for (const [batchId, batch] of [...this.pending]) {
5103
+ const orphaned = batch.pendingChunks.get(workerId);
5104
+ if (!orphaned) continue;
5105
+ if (this.workers.length === 0) {
5106
+ this.pending.delete(batchId);
5107
+ this.unavailable = true;
5108
+ if (!batch.settled) {
5109
+ batch.settled = true;
5110
+ batch.reject(new Error("ParserWorkerPool: all workers died mid-batch"));
5111
+ }
5112
+ continue;
5113
+ }
5114
+ void this.reparseInline(batch, orphaned, workerId);
5115
+ }
5116
+ }
5117
+ /**
5118
+ * Salvage path: re-parse an orphaned chunk on this thread. Files that
5119
+ * fail here stay absent from the results — same contract as a per-file
5120
+ * error inside a live worker (see handleMessage).
5121
+ */
5122
+ async reparseInline(batch, orphaned, workerId) {
5123
+ try {
5124
+ const { parseFileContent: parseFileContent2 } = await Promise.resolve().then(() => (init_parser_dispatch(), parser_dispatch_exports));
5125
+ for (const item of orphaned) {
5126
+ if (batch.settled) return;
5127
+ try {
5128
+ const parsed = await parseFileContent2(item.file, item.content, item.lang);
5129
+ batch.accumulated.push(parsed);
5130
+ } catch {
5131
+ }
5132
+ await new Promise((resolve4) => setImmediate(resolve4));
5133
+ }
5134
+ } finally {
5135
+ this.finishSalvage(batch, workerId);
5136
+ }
5137
+ }
5138
+ /**
5139
+ * Terminal tail of a salvage — runs on every exit path. Kept free of
5140
+ * control flow inside a `finally` (noUnsafeFinally): releases the
5141
+ * pending-marker and resolves the batch if this was its last chunk.
5142
+ */
5143
+ finishSalvage(batch, workerId) {
5144
+ batch.pendingChunks.delete(workerId);
5145
+ if (batch.pendingChunks.size === 0) {
5146
+ for (const [batchId, tracked] of this.pending) {
5147
+ if (tracked === batch) {
5148
+ if (batch.settled) return;
5149
+ batch.settled = true;
5150
+ this.pending.delete(batchId);
5151
+ batch.resolve(batch.accumulated);
5152
+ return;
5153
+ }
5154
+ }
4008
5155
  }
4009
5156
  }
5157
+ /**
5158
+ * Retire by worker object rather than threadId. `threadId` is -1 before
5159
+ * the worker emits `online`, so a death during script load would make a
5160
+ * threadId-keyed lookup silently no-op and leak the entry (with its
5161
+ * pending chunk) — reference identity is correct in every case.
5162
+ */
5163
+ retireByReference(source) {
5164
+ const entry = this.workers.find((w) => w.worker === source);
5165
+ if (entry) this.retireWorker(entry.workerId);
5166
+ }
5167
+ handleError(err, source) {
5168
+ void err;
5169
+ this.retireByReference(source);
5170
+ }
4010
5171
  };
4011
5172
  function defaultWorkerCount() {
4012
5173
  const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
4013
5174
  return Math.max(1, Math.min(4, cores - 1));
4014
5175
  }
4015
5176
  function resolveWorkerScriptUrl() {
5177
+ const override = process.env["WRONGSTACK_PARSER_WORKER_SCRIPT"];
5178
+ if (override) return pathToFileURL(override);
4016
5179
  for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
4017
5180
  try {
4018
5181
  const url = new URL(rel, import.meta.url);
4019
- if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
5182
+ if (url.protocol === "file:" && fs7.existsSync(fileURLToPath2(url))) return url;
4020
5183
  } catch {
4021
5184
  }
4022
5185
  }
@@ -4029,8 +5192,8 @@ function getParserPool() {
4029
5192
  }
4030
5193
 
4031
5194
  // src/codebase-index/writer.ts
4032
- import * as fs8 from "node:fs";
4033
- import * as path12 from "node:path";
5195
+ import * as fs9 from "node:fs";
5196
+ import * as path13 from "node:path";
4034
5197
 
4035
5198
  // src/codebase-index/bm25.ts
4036
5199
  var K1 = 1.5;
@@ -4125,11 +5288,11 @@ var Bm25Index = class {
4125
5288
  init_languages();
4126
5289
 
4127
5290
  // src/codebase-index/schema.ts
4128
- var SCHEMA_VERSION = 4;
5291
+ var SCHEMA_VERSION = 5;
4129
5292
 
4130
5293
  // src/codebase-index/sqlite-runtime.ts
4131
- import { createRequire } from "node:module";
4132
5294
  import { toErrorMessage } from "@wrongstack/core/utils";
5295
+ import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
4133
5296
  var warningSilenced = false;
4134
5297
  function silenceSqliteExperimentalWarning() {
4135
5298
  if (warningSilenced) return;
@@ -4147,11 +5310,10 @@ function loadDatabaseSync() {
4147
5310
  if (DatabaseSyncCtor) return DatabaseSyncCtor;
4148
5311
  silenceSqliteExperimentalWarning();
4149
5312
  try {
4150
- const req = createRequire(import.meta.url);
4151
- DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
5313
+ DatabaseSyncCtor = loadRuntimeDatabaseSync();
4152
5314
  } catch (err) {
4153
5315
  throw new Error(
4154
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
5316
+ `The codebase index needs node:sqlite (Node >= 22.5) or bun:sqlite. This runtime doesn't provide it: ${toErrorMessage(err)}`
4155
5317
  );
4156
5318
  }
4157
5319
  return DatabaseSyncCtor;
@@ -4194,9 +5356,87 @@ function runSqliteWithRetry(fn) {
4194
5356
  throw lastError;
4195
5357
  }
4196
5358
 
5359
+ // src/codebase-index/vector-search.ts
5360
+ var RRF_K = 60;
5361
+ function vectorEmbeddingEnabled() {
5362
+ return process.env["WRONGSTACK_INDEX_VECTORS"] === "1";
5363
+ }
5364
+ var VECTOR_DIMENSIONS = 384;
5365
+ var NGRAM_SIZE = 3;
5366
+ function embedText(text) {
5367
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
5368
+ const normalized = text.toLowerCase().trim();
5369
+ if (normalized.length < NGRAM_SIZE) {
5370
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5371
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5372
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
5373
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5374
+ vec[bucket] += 1;
5375
+ }
5376
+ } else {
5377
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5378
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
5379
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5380
+ vec[bucket] += 1;
5381
+ }
5382
+ }
5383
+ let norm = 0;
5384
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5385
+ norm += vec[i] * vec[i];
5386
+ }
5387
+ norm = Math.sqrt(norm);
5388
+ if (norm > 0) {
5389
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5390
+ vec[i] /= norm;
5391
+ }
5392
+ }
5393
+ return vec;
5394
+ }
5395
+ function hashNgram(str) {
5396
+ let hash = 2166136261;
5397
+ for (let i = 0; i < str.length; i++) {
5398
+ hash ^= str.charCodeAt(i);
5399
+ hash = Math.imul(hash, 16777619);
5400
+ }
5401
+ return hash >>> 0;
5402
+ }
5403
+ function cosineSimilarity(a, b) {
5404
+ let dot = 0;
5405
+ const len = Math.min(a.length, b.length);
5406
+ for (let i = 0; i < len; i++) {
5407
+ dot += a[i] * b[i];
5408
+ }
5409
+ return dot;
5410
+ }
5411
+ function encodeVector(vec) {
5412
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5413
+ }
5414
+ function decodeVector(buf) {
5415
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5416
+ const copy = new Float32Array(buf.byteLength / 4);
5417
+ for (let i = 0; i < copy.length; i++) {
5418
+ copy[i] = view.getFloat32(i * 4, true);
5419
+ }
5420
+ return copy;
5421
+ }
5422
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5423
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5424
+ const scored = [];
5425
+ for (const id of allIds) {
5426
+ const bm25Rank = bm25Ranks.get(id);
5427
+ const vecRank = vectorRanks.get(id);
5428
+ let score = 0;
5429
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5430
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5431
+ scored.push([id, score]);
5432
+ }
5433
+ scored.sort((a, b) => b[1] - a[1]);
5434
+ return scored;
5435
+ }
5436
+
4197
5437
  // src/codebase-index/writer-admin.ts
4198
- import * as fs7 from "node:fs";
4199
- import * as path11 from "node:path";
5438
+ import * as fs8 from "node:fs";
5439
+ import * as path12 from "node:path";
4200
5440
  var DB_FILE = "index.db";
4201
5441
  function getAllIndexableWithStatement(stmt) {
4202
5442
  return stmt("SELECT id, text FROM symbols").all().map(
@@ -4232,6 +5472,14 @@ function getMetadataWithStatement(stmt, key) {
4232
5472
  const rows = stmt("SELECT value FROM metadata WHERE key = ?").all(key);
4233
5473
  return rows[0]?.value;
4234
5474
  }
5475
+ function getIndexSummaryWithStatement(stmt) {
5476
+ const fileRows = stmt("SELECT COUNT(*) AS n FROM files").all();
5477
+ const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
5478
+ return {
5479
+ totalFiles: fileRows[0] ? Number(fileRows[0].n) : 0,
5480
+ lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null
5481
+ };
5482
+ }
4235
5483
  function getFileMetaWithStatement(stmt, file) {
4236
5484
  const rows = stmt(
4237
5485
  "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
@@ -4261,22 +5509,106 @@ function getAllFileMetasWithStatement(stmt) {
4261
5509
  }
4262
5510
  function getIndexDbSizeBytes(indexDir) {
4263
5511
  try {
4264
- return fs7.statSync(path11.join(indexDir, DB_FILE)).size;
5512
+ return fs8.statSync(path12.join(indexDir, DB_FILE)).size;
4265
5513
  } catch {
4266
5514
  return 0;
4267
5515
  }
4268
5516
  }
4269
5517
 
5518
+ // src/codebase-index/writer-helpers.ts
5519
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
5520
+ function escapeLike(value) {
5521
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
5522
+ }
5523
+ function ladderChunkSizes(total, max) {
5524
+ if (total <= 0) return [];
5525
+ const sizes = [];
5526
+ let remaining = total;
5527
+ while (remaining > 0) {
5528
+ const cap = Math.min(remaining, max);
5529
+ const pow = cap >= 1 ? 2 ** Math.floor(Math.log2(cap)) : 1;
5530
+ const take = Math.max(1, Math.min(pow, remaining));
5531
+ sizes.push(take);
5532
+ remaining -= take;
5533
+ }
5534
+ return sizes;
5535
+ }
5536
+ function nextPow2(count) {
5537
+ return count <= 1 ? 1 : 2 ** Math.ceil(Math.log2(count));
5538
+ }
5539
+ function padToInBucket(values) {
5540
+ if (values.length <= 1) return values.slice();
5541
+ const target = nextPow2(values.length);
5542
+ const padded = values.slice();
5543
+ while (padded.length < target) padded.push(padded[0]);
5544
+ return padded;
5545
+ }
5546
+ function placeholders(count) {
5547
+ return Array.from({ length: count }, () => "?").join(",");
5548
+ }
5549
+ function inListChunks(total, max) {
5550
+ if (total <= 0) return [];
5551
+ if (nextPow2(total) <= max) return [total];
5552
+ const powMax = Math.max(1, 2 ** Math.floor(Math.log2(max)));
5553
+ return ladderChunkSizes(total, powMax);
5554
+ }
5555
+ function posixIndexPath(file) {
5556
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
5557
+ }
5558
+ function indexedFileMatchSql(column = "file") {
5559
+ return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
5560
+ }
5561
+ function indexedFileMatchArgs(file) {
5562
+ const posix4 = posixIndexPath(file.trim());
5563
+ return [file, posix4, `%/${escapeLike(posix4)}`];
5564
+ }
5565
+ function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
5566
+ if (packageLabel === filter) return true;
5567
+ const posixFile = posixIndexPath(storedFile);
5568
+ const posixFilter = posixIndexPath(filter.trim());
5569
+ if (!posixFilter) return false;
5570
+ return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
5571
+ }
5572
+ function assignRefsToSymbols(refs, symbols) {
5573
+ if (refs.length === 0 || symbols.length === 0) return [];
5574
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
5575
+ const seen = /* @__PURE__ */ new Set();
5576
+ const assigned = [];
5577
+ for (const ref of refs) {
5578
+ let owner;
5579
+ for (const symbol of ordered) {
5580
+ if (symbol.line > ref.line) break;
5581
+ owner = symbol;
5582
+ }
5583
+ if (!owner && ref.callType === "import") owner = ordered[0];
5584
+ if (!owner || owner.id <= 0) continue;
5585
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5586
+ if (seen.has(key)) continue;
5587
+ seen.add(key);
5588
+ assigned.push({ ...ref, fromId: owner.id });
5589
+ }
5590
+ return assigned;
5591
+ }
5592
+ function resolveIndexDir(projectRoot, override) {
5593
+ return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
5594
+ }
5595
+ function codebaseIndexDirOverride(ctx) {
5596
+ const v = ctx.meta?.["codebaseIndexDir"];
5597
+ return typeof v === "string" ? v : void 0;
5598
+ }
5599
+
4270
5600
  // src/codebase-index/writer-bulk-insert.ts
4271
5601
  function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4272
5602
  if (rows.length === 0) return;
4273
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 12));
4274
- for (let i = 0; i < rows.length; i += chunkSize) {
4275
- const chunk = rows.slice(i, i + chunkSize);
4276
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5603
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 12)));
5604
+ let cursor = 0;
5605
+ for (const take of ladder) {
5606
+ const chunk = rows.slice(cursor, cursor + take);
5607
+ cursor += take;
5608
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4277
5609
  const insert = stmt(
4278
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
4279
- VALUES ${placeholders}`
5610
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text)
5611
+ VALUES ${placeholders2}`
4280
5612
  );
4281
5613
  const binds = [];
4282
5614
  for (const r of chunk) {
@@ -4291,8 +5623,7 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4291
5623
  r.signature,
4292
5624
  r.docComment,
4293
5625
  r.scope,
4294
- r.text,
4295
- r.file
5626
+ r.text
4296
5627
  );
4297
5628
  }
4298
5629
  insert.run(...binds);
@@ -4300,11 +5631,13 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4300
5631
  }
4301
5632
  function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4302
5633
  if (!ftsAvailable || rows.length === 0) return;
4303
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4304
- for (let i = 0; i < rows.length; i += chunkSize) {
4305
- const chunk = rows.slice(i, i + chunkSize);
4306
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4307
- const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
5634
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5635
+ let cursor = 0;
5636
+ for (const take of ladder) {
5637
+ const chunk = rows.slice(cursor, cursor + take);
5638
+ cursor += take;
5639
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5640
+ const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders2}`);
4308
5641
  const binds = [];
4309
5642
  for (const r of chunk) binds.push(r.id, r.text);
4310
5643
  insert.run(...binds);
@@ -4312,11 +5645,13 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4312
5645
  }
4313
5646
  function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4314
5647
  if (rows.length === 0) return;
4315
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4316
- for (let i = 0; i < rows.length; i += chunkSize) {
4317
- const chunk = rows.slice(i, i + chunkSize);
4318
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4319
- const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders}`);
5648
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5649
+ let cursor = 0;
5650
+ for (const take of ladder) {
5651
+ const chunk = rows.slice(cursor, cursor + take);
5652
+ cursor += take;
5653
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5654
+ const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders2}`);
4320
5655
  const binds = [];
4321
5656
  for (const r of chunk) binds.push(r.id, r.vector);
4322
5657
  insert.run(...binds);
@@ -4324,13 +5659,15 @@ function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4324
5659
  }
4325
5660
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
4326
5661
  if (refs.length === 0) return;
4327
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
4328
- for (let i = 0; i < refs.length; i += chunkSize) {
4329
- const chunk = refs.slice(i, i + chunkSize);
4330
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5662
+ const ladder = ladderChunkSizes(refs.length, Math.max(1, Math.floor(maxSqlVars / 8)));
5663
+ let cursor = 0;
5664
+ for (const take of ladder) {
5665
+ const chunk = refs.slice(cursor, cursor + take);
5666
+ cursor += take;
5667
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4331
5668
  const insert = stmt(
4332
5669
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
4333
- VALUES ${placeholders}`
5670
+ VALUES ${placeholders2}`
4334
5671
  );
4335
5672
  const binds = [];
4336
5673
  for (const ref of chunk) {
@@ -4484,56 +5821,6 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
4484
5821
  return edges;
4485
5822
  }
4486
5823
 
4487
- // src/codebase-index/writer-helpers.ts
4488
- import { resolveWstackPaths } from "@wrongstack/core/utils";
4489
- function escapeLike(value) {
4490
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
4491
- }
4492
- function posixIndexPath(file) {
4493
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
4494
- }
4495
- function indexedFileMatchSql(column = "file") {
4496
- return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
4497
- }
4498
- function indexedFileMatchArgs(file) {
4499
- const posix4 = posixIndexPath(file.trim());
4500
- return [file, posix4, `%/${escapeLike(posix4)}`];
4501
- }
4502
- function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
4503
- if (packageLabel === filter) return true;
4504
- const posixFile = posixIndexPath(storedFile);
4505
- const posixFilter = posixIndexPath(filter.trim());
4506
- if (!posixFilter) return false;
4507
- return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
4508
- }
4509
- function assignRefsToSymbols(refs, symbols) {
4510
- if (refs.length === 0 || symbols.length === 0) return [];
4511
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4512
- const seen = /* @__PURE__ */ new Set();
4513
- const assigned = [];
4514
- for (const ref of refs) {
4515
- let owner;
4516
- for (const symbol of ordered) {
4517
- if (symbol.line > ref.line) break;
4518
- owner = symbol;
4519
- }
4520
- if (!owner && ref.callType === "import") owner = ordered[0];
4521
- if (!owner || owner.id <= 0) continue;
4522
- const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4523
- if (seen.has(key)) continue;
4524
- seen.add(key);
4525
- assigned.push({ ...ref, fromId: owner.id });
4526
- }
4527
- return assigned;
4528
- }
4529
- function resolveIndexDir(projectRoot, override) {
4530
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4531
- }
4532
- function codebaseIndexDirOverride(ctx) {
4533
- const v = ctx.meta?.["codebaseIndexDir"];
4534
- return typeof v === "string" ? v : void 0;
4535
- }
4536
-
4537
5824
  // src/codebase-index/writer-ref-mapper.ts
4538
5825
  function mapWriterRefRow(row) {
4539
5826
  return {
@@ -4555,20 +5842,20 @@ function mapWriterRefRow(row) {
4555
5842
  var MAX_SQL_VARS = 900;
4556
5843
  function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
4557
5844
  const results = [];
4558
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4559
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4560
- const placeholders = chunk.map(() => "?").join(",");
4561
- const sql = buildSql(placeholders);
5845
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5846
+ const chunk = padToInBucket(ids.slice(0, take));
5847
+ ids = ids.slice(take);
5848
+ const sql = buildSql(placeholders(chunk.length));
4562
5849
  results.push(...stmt(sql).all(...chunk, ...extraArgs));
4563
5850
  }
4564
5851
  return results;
4565
5852
  }
4566
5853
  function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
4567
5854
  let total = 0;
4568
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4569
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4570
- const placeholders = chunk.map(() => "?").join(",");
4571
- const sql = buildSql(placeholders);
5855
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5856
+ const chunk = padToInBucket(ids.slice(0, take));
5857
+ ids = ids.slice(take);
5858
+ const sql = buildSql(placeholders(chunk.length));
4572
5859
  const rows = stmt(sql).all(...chunk, ...extraArgs);
4573
5860
  total += rows[0]?.n ?? 0;
4574
5861
  }
@@ -4597,16 +5884,24 @@ function resolveIndexedFiles(stmt, file) {
4597
5884
  }
4598
5885
  function resolveSymbolIds(stmt, symbolName, file) {
4599
5886
  if (!file) {
4600
- const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
4601
- return rows2.map((r) => r.id);
5887
+ const rows = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(
5888
+ symbolName
5889
+ );
5890
+ return rows.map((r) => r.id);
4602
5891
  }
4603
5892
  const indexedFiles = resolveIndexedFiles(stmt, file);
4604
5893
  if (indexedFiles.length === 0) return [];
4605
- const placeholders = indexedFiles.map(() => "?").join(",");
4606
- const rows = stmt(
4607
- `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
4608
- ).all(symbolName, ...indexedFiles);
4609
- return rows.map((r) => r.id);
5894
+ const ids = [];
5895
+ let cursor = 0;
5896
+ for (const take of inListChunks(indexedFiles.length, MAX_SQL_VARS)) {
5897
+ const files = padToInBucket(indexedFiles.slice(cursor, cursor + take));
5898
+ cursor += take;
5899
+ const rows = stmt(
5900
+ `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders(files.length)}) ORDER BY id`
5901
+ ).all(symbolName, ...files);
5902
+ ids.push(...rows.map((r) => r.id));
5903
+ }
5904
+ return ids;
4610
5905
  }
4611
5906
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4612
5907
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
@@ -5040,9 +6335,9 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5040
6335
  const loadedIds = new Set(syms.map((s) => s.id));
5041
6336
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
5042
6337
  if (missingIds.length > 0) {
5043
- const placeholders = missingIds.map(() => "?").join(",");
6338
+ const placeholders2 = missingIds.map(() => "?").join(",");
5044
6339
  const extras = stmt(
5045
- `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
6340
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders2})`
5046
6341
  ).all(...missingIds);
5047
6342
  for (const s of extras) symById.set(s.id, s);
5048
6343
  }
@@ -5055,81 +6350,6 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5055
6350
  return { nodes, edges };
5056
6351
  }
5057
6352
 
5058
- // src/codebase-index/vector-search.ts
5059
- var RRF_K = 60;
5060
- var VECTOR_DIMENSIONS = 384;
5061
- var NGRAM_SIZE = 3;
5062
- function embedText(text) {
5063
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5064
- const normalized = text.toLowerCase().trim();
5065
- if (normalized.length < NGRAM_SIZE) {
5066
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5067
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5068
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5069
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5070
- vec[bucket] += 1;
5071
- }
5072
- } else {
5073
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5074
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5075
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5076
- vec[bucket] += 1;
5077
- }
5078
- }
5079
- let norm = 0;
5080
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5081
- norm += vec[i] * vec[i];
5082
- }
5083
- norm = Math.sqrt(norm);
5084
- if (norm > 0) {
5085
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5086
- vec[i] /= norm;
5087
- }
5088
- }
5089
- return vec;
5090
- }
5091
- function hashNgram(str) {
5092
- let hash = 2166136261;
5093
- for (let i = 0; i < str.length; i++) {
5094
- hash ^= str.charCodeAt(i);
5095
- hash = Math.imul(hash, 16777619);
5096
- }
5097
- return hash >>> 0;
5098
- }
5099
- function cosineSimilarity(a, b) {
5100
- let dot = 0;
5101
- const len = Math.min(a.length, b.length);
5102
- for (let i = 0; i < len; i++) {
5103
- dot += a[i] * b[i];
5104
- }
5105
- return dot;
5106
- }
5107
- function encodeVector(vec) {
5108
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5109
- }
5110
- function decodeVector(buf) {
5111
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5112
- const copy = new Float32Array(buf.byteLength / 4);
5113
- for (let i = 0; i < copy.length; i++) {
5114
- copy[i] = view.getFloat32(i * 4, true);
5115
- }
5116
- return copy;
5117
- }
5118
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5119
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5120
- const scored = [];
5121
- for (const id of allIds) {
5122
- const bm25Rank = bm25Ranks.get(id);
5123
- const vecRank = vectorRanks.get(id);
5124
- let score = 0;
5125
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5126
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5127
- scored.push([id, score]);
5128
- }
5129
- scored.sort((a, b) => b[1] - a[1]);
5130
- return scored;
5131
- }
5132
-
5133
6353
  // src/codebase-index/writer-mutations.ts
5134
6354
  function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5135
6355
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
@@ -5141,24 +6361,29 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5141
6361
  for (const ref of entry.refs) affectedNames.add(ref.toName);
5142
6362
  }
5143
6363
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5144
- const placeholders = options.deleteForFiles.map(() => "?").join(",");
5145
6364
  for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5146
6365
  affectedNames.add(name);
5147
6366
  }
5148
- if (ftsAvailable) {
5149
- stmtFn(
5150
- `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5151
- ).run(...options.deleteForFiles);
5152
- }
5153
- if (vectorsAvailable) {
6367
+ let cursor = 0;
6368
+ for (const take of inListChunks(options.deleteForFiles.length, Math.floor(maxSqlVars / 4))) {
6369
+ const bucket = padToInBucket(options.deleteForFiles.slice(cursor, cursor + take));
6370
+ cursor += take;
6371
+ const ph = placeholders(bucket.length);
6372
+ if (ftsAvailable) {
6373
+ stmtFn(
6374
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6375
+ ).run(...bucket);
6376
+ }
6377
+ if (vectorsAvailable) {
6378
+ stmtFn(
6379
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6380
+ ).run(...bucket);
6381
+ }
5154
6382
  stmtFn(
5155
- `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5156
- ).run(...options.deleteForFiles);
6383
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6384
+ ).run(...bucket);
6385
+ stmtFn(`DELETE FROM symbols WHERE file IN (${ph})`).run(...bucket);
5157
6386
  }
5158
- stmtFn(
5159
- `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5160
- ).run(...options.deleteForFiles);
5161
- stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5162
6387
  }
5163
6388
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5164
6389
  let nextId = allocateSymbolIds(totalSymbols);
@@ -5190,12 +6415,14 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5190
6415
  text: buildIndexableText(s.name, s.signature, s.docComment)
5191
6416
  });
5192
6417
  }
5193
- vectorRows.push({
5194
- id,
5195
- vector: encodeVector(
5196
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5197
- )
5198
- });
6418
+ if (vectorsAvailable) {
6419
+ vectorRows.push({
6420
+ id,
6421
+ vector: encodeVector(
6422
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6423
+ )
6424
+ });
6425
+ }
5199
6426
  const inserted = { ...s, id };
5200
6427
  allInserted.push(inserted);
5201
6428
  insertedForEntry.push(inserted);
@@ -5287,9 +6514,8 @@ var CORE_TABLES_SQL = `
5287
6514
  signature TEXT NOT NULL DEFAULT '',
5288
6515
  doc_comment TEXT NOT NULL DEFAULT '',
5289
6516
  scope TEXT NOT NULL DEFAULT '',
5290
- text TEXT NOT NULL DEFAULT '',
5291
- file_fk TEXT NOT NULL
5292
- );
6517
+ text TEXT NOT NULL DEFAULT ''
6518
+ );
5293
6519
  `;
5294
6520
  var FILE_INDEX_SQL = [
5295
6521
  "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
@@ -5300,7 +6526,6 @@ var SYMBOL_INDEX_SQL = [
5300
6526
  "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
5301
6527
  "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
5302
6528
  "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
5303
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
5304
6529
  "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
5305
6530
  ];
5306
6531
  var REFS_TABLE_SQL = `
@@ -5375,11 +6600,12 @@ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5375
6600
  return stmtFn(base).all();
5376
6601
  }
5377
6602
  const out = [];
5378
- for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5379
- const chunk = onlyFiles.slice(i, i + maxSqlVars);
5380
- const placeholders = chunk.map(() => "?").join(",");
6603
+ let cursor = 0;
6604
+ for (const take of inListChunks(onlyFiles.length, maxSqlVars)) {
6605
+ const chunk = padToInBucket(onlyFiles.slice(cursor, cursor + take));
6606
+ cursor += take;
5381
6607
  out.push(
5382
- ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
6608
+ ...stmtFn(`${base} AND s.file IN (${placeholders(chunk.length)})`).all(...chunk)
5383
6609
  );
5384
6610
  }
5385
6611
  return out;
@@ -5450,16 +6676,18 @@ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVar
5450
6676
  )`
5451
6677
  );
5452
6678
  const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5453
- for (let i = 0; i < resolutions.length; i += chunkSize) {
5454
- const chunk = resolutions.slice(i, i + chunkSize);
5455
- const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
6679
+ let cursor = 0;
6680
+ for (const take of ladderChunkSizes(resolutions.length, chunkSize)) {
6681
+ const chunk = resolutions.slice(cursor, cursor + take);
6682
+ cursor += take;
6683
+ const valuesPh = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5456
6684
  const binds = [];
5457
6685
  for (const entry of chunk) {
5458
6686
  binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5459
6687
  }
5460
6688
  stmtFn(
5461
6689
  `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5462
- VALUES ${placeholders}`
6690
+ VALUES ${valuesPh}`
5463
6691
  ).run(...binds);
5464
6692
  }
5465
6693
  db.exec(
@@ -5496,9 +6724,11 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5496
6724
  const list = [...names].filter((name) => name.length > 0);
5497
6725
  if (list.length === 0) return 0;
5498
6726
  let total = 0;
5499
- for (let i = 0; i < list.length; i += maxSqlVars) {
5500
- const chunk = list.slice(i, i + maxSqlVars);
5501
- const placeholders = chunk.map(() => "?").join(",");
6727
+ let cursor = 0;
6728
+ for (const take of inListChunks(list.length, maxSqlVars)) {
6729
+ const chunk = padToInBucket(list.slice(cursor, cursor + take));
6730
+ cursor += take;
6731
+ const ph = placeholders(chunk.length);
5502
6732
  try {
5503
6733
  const result = stmtFn(
5504
6734
  `UPDATE refs
@@ -5507,16 +6737,16 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5507
6737
  SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5508
6738
  FROM symbols sym
5509
6739
  JOIN lang_family lf ON lf.lang = sym.lang
5510
- WHERE sym.name IN (${placeholders})
6740
+ WHERE sym.name IN (${ph})
5511
6741
  GROUP BY sym.name, lf.family
5512
6742
  UNION ALL
5513
6743
  SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5514
6744
  FROM symbols sym
5515
- WHERE sym.name IN (${placeholders})
6745
+ WHERE sym.name IN (${ph})
5516
6746
  GROUP BY sym.name
5517
6747
  ) AS s,
5518
6748
  lang_family AS rf
5519
- WHERE refs.to_name IN (${placeholders})
6749
+ WHERE refs.to_name IN (${ph})
5520
6750
  AND rf.lang = refs.lang
5521
6751
  AND s.name = refs.to_name
5522
6752
  AND s.family = rf.family`
@@ -5528,7 +6758,7 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5528
6758
  SELECT sym.id FROM symbols sym
5529
6759
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5530
6760
  ORDER BY sym.id LIMIT 1
5531
- ) WHERE refs.to_name IN (${placeholders})
6761
+ ) WHERE refs.to_name IN (${ph})
5532
6762
  AND EXISTS (
5533
6763
  SELECT 1 FROM symbols sym
5534
6764
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
@@ -5585,7 +6815,7 @@ function buildWriterSearchWhere(query, filter) {
5585
6815
  const conditions = [];
5586
6816
  const values = [];
5587
6817
  let effectiveKind = filter?.kind;
5588
- if (filter?.lspKind !== void 0) {
6818
+ if (filter?.lspKind != null) {
5589
6819
  const mapped = lspKindToInternalKind(filter.lspKind);
5590
6820
  if (mapped !== null) {
5591
6821
  effectiveKind = mapped;
@@ -5664,7 +6894,7 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5664
6894
  );
5665
6895
  }
5666
6896
  let effectiveKind = filter?.kind;
5667
- if (filter?.lspKind !== void 0) {
6897
+ if (filter?.lspKind != null) {
5668
6898
  const mapped = lspKindToInternalKind(filter.lspKind);
5669
6899
  if (mapped === null) return { results: [], total: 0 };
5670
6900
  effectiveKind = mapped;
@@ -5701,15 +6931,14 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5701
6931
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5702
6932
  }
5703
6933
  const where = conditions.join(" AND ");
5704
- const countRows = stmtFn(
5705
- `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5706
- ).all(...values);
5707
- const total = countRows[0] ? Number(countRows[0].n) : 0;
5708
- if (total === 0) return { results: [], total: 0 };
5709
6934
  const bm25Rows = stmtFn(
5710
6935
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5711
6936
  -bm25(symbols_fts) AS score,
5712
- snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
6937
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet,
6938
+ -- Keep this uncorrelated: referencing outer columns turns it into
6939
+ -- a per-row subquery and defeats the one-count-per-statement win.
6940
+ (SELECT COUNT(*) FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
6941
+ WHERE ${where}) AS total_count
5713
6942
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5714
6943
  WHERE ${where}
5715
6944
  ORDER BY
@@ -5718,13 +6947,15 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5718
6947
  ELSE 2 END,
5719
6948
  bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5720
6949
  LIMIT ?`
5721
- ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5722
- if (vectorsAvailable && bm25Rows.length > 0) {
6950
+ ).all(...values, ...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
6951
+ if (bm25Rows.length === 0) return { results: [], total: 0 };
6952
+ const total = Number(bm25Rows[0]?.total_count ?? 0);
6953
+ if (vectorsAvailable) {
5723
6954
  const queryVec = embedText(query);
5724
6955
  const candidateIds = bm25Rows.map((r) => r.id);
5725
- const placeholders = candidateIds.map(() => "?").join(",");
6956
+ const placeholders2 = candidateIds.map(() => "?").join(",");
5726
6957
  const vecRows = stmtFn(
5727
- `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
6958
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders2})`
5728
6959
  ).all(...candidateIds);
5729
6960
  const vecScores = vecRows.map((r) => ({
5730
6961
  id: r.symbol_id,
@@ -5912,9 +7143,9 @@ var IndexStore = class _IndexStore {
5912
7143
  }
5913
7144
  constructor(projectRoot, opts = {}) {
5914
7145
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
5915
- fs8.mkdirSync(this.indexDir, { recursive: true });
7146
+ fs9.mkdirSync(this.indexDir, { recursive: true });
5916
7147
  const Database = loadDatabaseSync();
5917
- this.db = new Database(path12.join(this.indexDir, DB_FILE2));
7148
+ this.db = new Database(path13.join(this.indexDir, DB_FILE2));
5918
7149
  applyIndexStorePragmas(this.db);
5919
7150
  this.initSchema();
5920
7151
  }
@@ -6043,7 +7274,11 @@ var IndexStore = class _IndexStore {
6043
7274
  );
6044
7275
  if (symbolCount !== ftsCount) {
6045
7276
  this.db.exec("DELETE FROM symbols_fts");
6046
- if (this.vectorsAvailable) this.db.exec("DELETE FROM symbol_vectors");
7277
+ if (vectorEmbeddingEnabled() && this.stmt(
7278
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbol_vectors'"
7279
+ ).get() !== void 0) {
7280
+ this.db.exec("DELETE FROM symbol_vectors");
7281
+ }
6047
7282
  const rows = this.stmt(
6048
7283
  "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
6049
7284
  ).all();
@@ -6062,8 +7297,13 @@ var IndexStore = class _IndexStore {
6062
7297
  this.ftsAvailable = false;
6063
7298
  }
6064
7299
  try {
6065
- this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
6066
- this.vectorsAvailable = true;
7300
+ if (vectorEmbeddingEnabled()) {
7301
+ this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
7302
+ this.vectorsAvailable = true;
7303
+ } else {
7304
+ this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
7305
+ this.vectorsAvailable = false;
7306
+ }
6067
7307
  } catch {
6068
7308
  this.vectorsAvailable = false;
6069
7309
  }
@@ -6098,14 +7338,22 @@ var IndexStore = class _IndexStore {
6098
7338
  }
6099
7339
  invalidateIncomingRefsForFiles(files) {
6100
7340
  if (files.length === 0) return /* @__PURE__ */ new Set();
6101
- const placeholders = files.map(() => "?").join(",");
6102
- const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
6103
- ...files
6104
- ).map((row) => row.name);
6105
- this.stmt(
6106
- `UPDATE refs SET to_id = NULL
6107
- WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6108
- ).run(...files);
7341
+ const names = [];
7342
+ let cursor = 0;
7343
+ for (const take of inListChunks(files.length, _IndexStore.MAX_SQL_VARS)) {
7344
+ const bucket = padToInBucket(files.slice(cursor, cursor + take));
7345
+ cursor += take;
7346
+ const ph = placeholders(bucket.length);
7347
+ for (const row of this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${ph})`).all(
7348
+ ...bucket
7349
+ )) {
7350
+ names.push(row.name);
7351
+ }
7352
+ this.stmt(
7353
+ `UPDATE refs SET to_id = NULL
7354
+ WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
7355
+ ).run(...bucket);
7356
+ }
6109
7357
  return new Set(names);
6110
7358
  }
6111
7359
  resolveRefsForNamesUnsafe(names) {
@@ -6139,6 +7387,14 @@ var IndexStore = class _IndexStore {
6139
7387
  if (this.ftsAvailable) {
6140
7388
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
6141
7389
  }
7390
+ if (this.vectorsAvailable) {
7391
+ vectorRows.push({
7392
+ id,
7393
+ vector: encodeVector(
7394
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
7395
+ )
7396
+ });
7397
+ }
6142
7398
  result.push({ ...s, id });
6143
7399
  }
6144
7400
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -6171,15 +7427,15 @@ var IndexStore = class _IndexStore {
6171
7427
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6172
7428
  if (this.ftsAvailable) {
6173
7429
  this.stmt(
6174
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7430
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6175
7431
  ).run(file);
6176
7432
  }
6177
7433
  if (this.vectorsAvailable) {
6178
7434
  this.stmt(
6179
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7435
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6180
7436
  ).run(file);
6181
7437
  }
6182
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7438
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6183
7439
  this.resolveRefsForNamesUnsafe(affectedNames);
6184
7440
  this.commitWriteTransaction(ownsTransaction);
6185
7441
  } catch (error) {
@@ -6196,18 +7452,18 @@ var IndexStore = class _IndexStore {
6196
7452
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6197
7453
  if (this.ftsAvailable) {
6198
7454
  this.stmt(
6199
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7455
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6200
7456
  ).run(file);
6201
7457
  }
6202
7458
  if (this.vectorsAvailable) {
6203
7459
  this.stmt(
6204
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7460
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6205
7461
  ).run(file);
6206
7462
  }
6207
- this.stmt(
6208
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6209
- ).run(file);
6210
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7463
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7464
+ file
7465
+ );
7466
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6211
7467
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
6212
7468
  this.resolveRefsForNamesUnsafe(affectedNames);
6213
7469
  this.commitWriteTransaction(ownsTransaction);
@@ -6311,6 +7567,10 @@ var IndexStore = class _IndexStore {
6311
7567
  getStats() {
6312
7568
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6313
7569
  }
7570
+ /** P2.5: minimal summary for search-response piggyback (see writer-admin). */
7571
+ getIndexSummary() {
7572
+ return getIndexSummaryWithStatement((sql) => this.stmt(sql));
7573
+ }
6314
7574
  setLastIndexed(ts2) {
6315
7575
  this.runWithRetry(() => {
6316
7576
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
@@ -6412,18 +7672,18 @@ var IndexStore = class _IndexStore {
6412
7672
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6413
7673
  if (this.ftsAvailable) {
6414
7674
  this.stmt(
6415
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7675
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6416
7676
  ).run(meta.file);
6417
7677
  }
6418
7678
  if (this.vectorsAvailable) {
6419
7679
  this.stmt(
6420
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7680
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6421
7681
  ).run(meta.file);
6422
7682
  }
6423
- this.stmt(
6424
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6425
- ).run(meta.file);
6426
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
7683
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7684
+ meta.file
7685
+ );
7686
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(meta.file);
6427
7687
  this.stmt(
6428
7688
  `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
6429
7689
  VALUES (?, ?, ?, ?, ?, ?)
@@ -6455,6 +7715,27 @@ var IndexStore = class _IndexStore {
6455
7715
  } catch {
6456
7716
  }
6457
7717
  }
7718
+ /**
7719
+ * P4.14: best-effort WAL checkpoint for idle-time maintenance.
7720
+ *
7721
+ * `wal_autocheckpoint` is PASSIVE and only attempts work after a COMMIT —
7722
+ * once writes stop, nothing fires again, so the WAL keeps whatever frames
7723
+ * the last burst left. This probes with PASSIVE first (never blocks; busy=1
7724
+ * means readers still hold WAL snapshots) and only issues the TRUNCATE —
7725
+ * which resets index.db-wal to zero bytes — when the checkpointer can
7726
+ * proceed immediately. Callers run this on the daemon's single thread, so
7727
+ * never wait on readers here: busy means "retry at the next idle window".
7728
+ */
7729
+ checkpointWal() {
7730
+ try {
7731
+ const probe = this.db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get();
7732
+ if (Number(probe?.busy ?? 1) !== 0) return false;
7733
+ const done = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
7734
+ return Number(done?.busy ?? 1) === 0;
7735
+ } catch {
7736
+ return false;
7737
+ }
7738
+ }
6458
7739
  compactIfNeeded(options = {}) {
6459
7740
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6460
7741
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6540,7 +7821,9 @@ function resolveParallelBatch() {
6540
7821
  return indexParallelBatchSize(availableParallelism());
6541
7822
  }
6542
7823
  function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
6543
- return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
7824
+ const threshold = resolveWorkerPoolThreshold();
7825
+ if (threshold === 0) return false;
7826
+ return !isFrugalPerf() && candidateFileCount >= threshold && parseBatchCount > 1;
6544
7827
  }
6545
7828
  function yieldEventLoop() {
6546
7829
  return new Promise((resolve4) => setImmediate(resolve4));
@@ -6563,15 +7846,15 @@ var IndexSourceChangedError = class extends Error {
6563
7846
  name = "IndexSourceChangedError";
6564
7847
  };
6565
7848
  function isWithinProject(projectRoot, file) {
6566
- const rel = path13.relative(projectRoot, file);
6567
- return rel !== "" && !rel.startsWith(`..${path13.sep}`) && rel !== ".." && !path13.isAbsolute(rel);
7849
+ const rel = path14.relative(projectRoot, file);
7850
+ return rel !== "" && !rel.startsWith(`..${path14.sep}`) && rel !== ".." && !path14.isAbsolute(rel);
6568
7851
  }
6569
7852
  function isMissingPathError(err) {
6570
7853
  const code = err?.code;
6571
7854
  return code === "ENOENT" || code === "ENOTDIR";
6572
7855
  }
6573
7856
  function normalizeComparablePath(value) {
6574
- const resolved = path13.resolve(value);
7857
+ const resolved = path14.resolve(value);
6575
7858
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6576
7859
  }
6577
7860
  function gitOutput(projectRoot, args) {
@@ -6617,24 +7900,24 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6617
7900
  const record = statusRecords[i];
6618
7901
  if (!record) continue;
6619
7902
  const status = record.slice(0, 2);
6620
- const changedPath = path13.resolve(projectRoot, record.slice(3));
7903
+ const changedPath = path14.resolve(projectRoot, record.slice(3));
6621
7904
  dirty.add(changedPath);
6622
7905
  if (status.includes("D")) deleted.add(changedPath);
6623
7906
  if (status.includes("R") || status.includes("C")) {
6624
7907
  const source = statusRecords[++i];
6625
- if (source) dirty.add(path13.resolve(projectRoot, source));
7908
+ if (source) dirty.add(path14.resolve(projectRoot, source));
6626
7909
  }
6627
7910
  }
6628
7911
  const files = [];
6629
7912
  for (const relative3 of output.toString("utf8").split("\0")) {
6630
7913
  if (!relative3) continue;
6631
7914
  const portable = relative3.replace(/\\/g, "/");
6632
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path13.posix.basename(portable))) {
7915
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path14.posix.basename(portable))) {
6633
7916
  continue;
6634
7917
  }
6635
- const full = path13.resolve(projectRoot, relative3);
7918
+ const full = path14.resolve(projectRoot, relative3);
6636
7919
  if (deleted.has(full)) continue;
6637
- const ext = path13.extname(relative3).toLowerCase();
7920
+ const ext = path14.extname(relative3).toLowerCase();
6638
7921
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6639
7922
  }
6640
7923
  const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
@@ -6642,7 +7925,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6642
7925
  for (const dirtyFile of [...dirty].sort()) {
6643
7926
  if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6644
7927
  snapshot.update("\0").update(dirtyFile).update("\0");
6645
- snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
7928
+ snapshot.update(xxhash64String(await fs10.readFile(dirtyFile, "utf8")));
6646
7929
  }
6647
7930
  return {
6648
7931
  files,
@@ -6678,7 +7961,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6678
7961
  }
6679
7962
  let entries;
6680
7963
  try {
6681
- entries = await fs9.readdir(dir, { withFileTypes: true });
7964
+ entries = await fs10.readdir(dir, { withFileTypes: true });
6682
7965
  } catch (err) {
6683
7966
  complete = false;
6684
7967
  errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6687,14 +7970,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6687
7970
  dirCount++;
6688
7971
  for (const e of entries) {
6689
7972
  if (ignoreSet.has(e.name)) continue;
6690
- const full = path13.join(dir, e.name);
6691
- const rel = path13.relative(projectRoot, full).replace(/\\/g, "/");
7973
+ const full = path14.join(dir, e.name);
7974
+ const rel = path14.relative(projectRoot, full).replace(/\\/g, "/");
6692
7975
  if (e.isDirectory()) {
6693
7976
  if (isGitIgnored(rel, true)) continue;
6694
7977
  await walk(full);
6695
7978
  } else if (e.isFile()) {
6696
7979
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
6697
- const ext = path13.extname(e.name).toLowerCase();
7980
+ const ext = path14.extname(e.name).toLowerCase();
6698
7981
  if (indexableExts.has(ext) || detectLang(full) !== null) {
6699
7982
  results.push(full);
6700
7983
  }
@@ -6780,10 +8063,10 @@ async function runIndexerAtomic(store, opts) {
6780
8063
  let trustedUnchanged;
6781
8064
  let discoverySnapshotKey;
6782
8065
  if (opts.files && opts.files.length > 0) {
6783
- files = opts.files.map((f) => path13.resolve(projectRoot, f)).filter((f) => {
8066
+ files = opts.files.map((f) => path14.resolve(projectRoot, f)).filter((f) => {
6784
8067
  if (!isWithinProject(projectRoot, f)) return false;
6785
- const rel = path13.relative(projectRoot, f).replace(/\\/g, "/");
6786
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path13.basename(f)) && !isGitIgnored(rel, false);
8068
+ const rel = path14.relative(projectRoot, f).replace(/\\/g, "/");
8069
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path14.basename(f)) && !isGitIgnored(rel, false);
6787
8070
  });
6788
8071
  } else {
6789
8072
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -6816,7 +8099,6 @@ async function runIndexerAtomic(store, opts) {
6816
8099
  if (!meta || !trustedUnchanged.has(file)) return true;
6817
8100
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
6818
8101
  symbolsIndexed += meta.symbolCount;
6819
- filesIndexed++;
6820
8102
  filesSkipped++;
6821
8103
  filesPreSkipped++;
6822
8104
  return false;
@@ -6845,7 +8127,7 @@ async function runIndexerAtomic(store, opts) {
6845
8127
  async (file) => {
6846
8128
  let stat3;
6847
8129
  try {
6848
- stat3 = await fs9.stat(file, statOpts);
8130
+ stat3 = await fs10.stat(file, statOpts);
6849
8131
  } catch (e) {
6850
8132
  if (isAbortError(e)) throw e;
6851
8133
  return {
@@ -6872,7 +8154,7 @@ async function runIndexerAtomic(store, opts) {
6872
8154
  const meta = existingMeta.get(file);
6873
8155
  let content;
6874
8156
  try {
6875
- content = await fs9.readFile(file, { encoding: "utf8", signal });
8157
+ content = await fs10.readFile(file, { encoding: "utf8", signal });
6876
8158
  } catch (e) {
6877
8159
  if (isAbortError(e)) throw e;
6878
8160
  return {
@@ -6937,22 +8219,19 @@ async function runIndexerAtomic(store, opts) {
6937
8219
  }
6938
8220
  }
6939
8221
  if (!pool) {
6940
- await Promise.all(
6941
- toParse.map(async (item) => {
6942
- try {
6943
- const parsed = await parseFileContent(item.file, item.content, item.lang);
6944
- const settled = statReadParse[item.index];
6945
- if (settled.status === "fulfilled") {
6946
- settled.value.parsed = parsed;
6947
- }
6948
- } catch (e) {
6949
- const settled = statReadParse[item.index];
6950
- if (settled.status === "fulfilled") {
6951
- settled.value.error = `parse error: ${e instanceof Error ? e.message : String(e)}`;
6952
- }
6953
- }
6954
- })
8222
+ const parsedAll = await parseFilesContent(
8223
+ toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
6955
8224
  );
8225
+ for (let pi2 = 0; pi2 < parsedAll.length && pi2 < toParse.length; pi2++) {
8226
+ const settled = statReadParse[toParse[pi2].index];
8227
+ if (settled.status !== "fulfilled") continue;
8228
+ const slot = parsedAll[pi2];
8229
+ if (slot.result) {
8230
+ settled.value.parsed = slot.result;
8231
+ } else {
8232
+ settled.value.error = `parse error: ${slot.error ?? `no result for ${toParse[pi2].file}`}`;
8233
+ }
8234
+ }
6956
8235
  }
6957
8236
  }
6958
8237
  const batchEntries = [];
@@ -6978,7 +8257,6 @@ async function runIndexerAtomic(store, opts) {
6978
8257
  if (result.skippedMeta) {
6979
8258
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
6980
8259
  symbolsIndexed += result.skippedMeta.symbolCount;
6981
- filesIndexed++;
6982
8260
  filesSkipped++;
6983
8261
  const stored = existingMeta.get(file);
6984
8262
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
@@ -7003,7 +8281,6 @@ async function runIndexerAtomic(store, opts) {
7003
8281
  lastIndexed: Date.now(),
7004
8282
  contentHash: result.contentHash ?? ""
7005
8283
  });
7006
- filesIndexed++;
7007
8284
  filesEmpty++;
7008
8285
  }
7009
8286
  continue;
@@ -7017,7 +8294,6 @@ async function runIndexerAtomic(store, opts) {
7017
8294
  lastIndexed: Date.now(),
7018
8295
  contentHash: result.contentHash ?? ""
7019
8296
  });
7020
- filesIndexed++;
7021
8297
  filesEmpty++;
7022
8298
  continue;
7023
8299
  }
@@ -7147,7 +8423,7 @@ async function indexService(args, hooks = {}) {
7147
8423
  function searchService(args) {
7148
8424
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
7149
8425
  try {
7150
- return store.searchRanked(
8426
+ const result = store.searchRanked(
7151
8427
  args.query,
7152
8428
  {
7153
8429
  kind: args.kind,
@@ -7157,6 +8433,10 @@ function searchService(args) {
7157
8433
  },
7158
8434
  args.limit
7159
8435
  );
8436
+ if (result.total === 0) {
8437
+ return { ...result, indexSummary: store.getIndexSummary() };
8438
+ }
8439
+ return result;
7160
8440
  } finally {
7161
8441
  indexStorePool.release(store);
7162
8442
  }
@@ -7217,34 +8497,71 @@ function outgoingCallsService(args) {
7217
8497
  }
7218
8498
 
7219
8499
  // src/codebase-index/project-server-client.ts
7220
- import { spawn as spawn3 } from "node:child_process";
7221
- import * as fs12 from "node:fs";
8500
+ import { spawn as spawn4 } from "node:child_process";
8501
+ import * as fs13 from "node:fs";
7222
8502
  import * as net from "node:net";
7223
- import { StringDecoder } from "node:string_decoder";
7224
8503
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7225
8504
 
7226
8505
  // src/codebase-index/binary-frame.ts
7227
8506
  import { decode, encode } from "@msgpack/msgpack";
7228
8507
  var BINARY_FRAME_MAGIC = 87;
8508
+ var MAX_BINARY_FRAME_BYTES = 256 * 1024 * 1024;
8509
+ var MAX_INBOUND_BINARY_FRAME_BYTES = 64 * 1024 * 1024;
7229
8510
  function isBinaryFrame(firstByte) {
7230
8511
  return firstByte === BINARY_FRAME_MAGIC;
7231
8512
  }
7232
8513
  function encodeBinaryFrame(message) {
7233
- const payload = encode(message);
8514
+ const payload = encode(normalizeUndefined(message));
7234
8515
  const header = Buffer.allocUnsafe(5);
7235
8516
  header[0] = BINARY_FRAME_MAGIC;
7236
8517
  header.writeUInt32BE(payload.length, 1);
7237
8518
  return Buffer.concat([header, payload], 5 + payload.length);
7238
8519
  }
8520
+ function normalizeUndefined(value) {
8521
+ if (value instanceof Date) {
8522
+ const time = value.getTime();
8523
+ return Number.isNaN(time) ? null : value.toISOString();
8524
+ }
8525
+ if (value instanceof Map) return normalizeUndefined(Object.fromEntries(value));
8526
+ if (value instanceof Set) return normalizeUndefined([...value]);
8527
+ if (value instanceof Error) {
8528
+ return normalizeUndefined({ name: value.name, message: value.message, stack: value.stack });
8529
+ }
8530
+ if (value instanceof RegExp) return String(value);
8531
+ if (value instanceof URL) return value.toJSON();
8532
+ if (Buffer.isBuffer(value)) return { type: "Buffer", data: [...value] };
8533
+ if (Array.isArray(value)) return value.map((entry) => normalizeUndefined(entry));
8534
+ if (!isPlainObject(value)) return value;
8535
+ const out = {};
8536
+ for (const [key, entry] of Object.entries(value)) {
8537
+ if (entry === void 0) continue;
8538
+ out[key] = normalizeUndefined(entry);
8539
+ }
8540
+ return out;
8541
+ }
8542
+ function isPlainObject(value) {
8543
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
8544
+ const proto = Object.getPrototypeOf(value);
8545
+ return proto === Object.prototype || proto === null;
8546
+ }
7239
8547
  function decodeBinaryFrame(payload) {
7240
8548
  return decode(payload);
7241
8549
  }
8550
+ function encodeJsonFrame(message) {
8551
+ return `${JSON.stringify(normalizeUndefined(message))}
8552
+ `;
8553
+ }
8554
+
8555
+ // src/codebase-index/project-server-client-state.ts
8556
+ import * as fs12 from "node:fs";
8557
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8558
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
7242
8559
 
7243
8560
  // src/codebase-index/project-server-endpoint.ts
7244
8561
  import { createHash as createHash3 } from "node:crypto";
7245
- import * as fs10 from "node:fs";
7246
- import * as os3 from "node:os";
7247
- import * as path14 from "node:path";
8562
+ import * as fs11 from "node:fs";
8563
+ import * as os4 from "node:os";
8564
+ import * as path15 from "node:path";
7248
8565
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7249
8566
  var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
7250
8567
  var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
@@ -7253,21 +8570,21 @@ var buildIdCache;
7253
8570
  function projectIndexServerBuildId(entrypoint) {
7254
8571
  const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
7255
8572
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
7256
- const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path14.resolve(cleanHref);
8573
+ const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path15.resolve(cleanHref);
7257
8574
  try {
7258
- const stat3 = fs10.statSync(file);
8575
+ const stat3 = fs11.statSync(file);
7259
8576
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7260
8577
  return buildIdCache.buildId;
7261
8578
  }
7262
- const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
8579
+ const buildId = createHash3("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
7263
8580
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7264
8581
  return buildId;
7265
8582
  } catch {
7266
- return `unreadable:${path14.basename(file)}`;
8583
+ return `unreadable:${path15.basename(file)}`;
7267
8584
  }
7268
8585
  }
7269
8586
  function normalizeLocalPath(value) {
7270
- const resolved = path14.resolve(value);
8587
+ const resolved = path15.resolve(value);
7271
8588
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7272
8589
  }
7273
8590
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -7279,26 +8596,16 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
7279
8596
  if (process.platform === "win32") {
7280
8597
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
7281
8598
  }
7282
- return path14.join(os3.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
8599
+ return path15.join(os4.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
7283
8600
  }
7284
8601
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
7285
- return path14.join(
7286
- path14.resolve(resolveIndexDir(projectRoot, indexDir)),
8602
+ return path15.join(
8603
+ path15.resolve(resolveIndexDir(projectRoot, indexDir)),
7287
8604
  PROJECT_INDEX_SERVER_METADATA_FILE
7288
8605
  );
7289
8606
  }
7290
8607
 
7291
- // src/codebase-index/project-server-protocol.ts
7292
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
7293
- function encodeProjectServerMessage(message) {
7294
- return `${JSON.stringify(message)}
7295
- `;
7296
- }
7297
-
7298
8608
  // src/codebase-index/project-server-client-state.ts
7299
- import * as fs11 from "node:fs";
7300
- import { fileURLToPath as fileURLToPath4 } from "node:url";
7301
- import { checkUnixSocketPath } from "@wrongstack/core/utils";
7302
8609
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
7303
8610
  var SERVER_START_TIMEOUT_MS = 1e4;
7304
8611
  var SERVER_CONTROL_TIMEOUT_MS = 5e3;
@@ -7329,7 +8636,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
7329
8636
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
7330
8637
  try {
7331
8638
  const url = new URL(rel, import.meta.url);
7332
- if (url.protocol === "file:" && fs11.existsSync(fileURLToPath4(url))) {
8639
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath4(url))) {
7333
8640
  builtUrl = url;
7334
8641
  break;
7335
8642
  }
@@ -7417,6 +8724,12 @@ function cancellationError(signal) {
7417
8724
  return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
7418
8725
  }
7419
8726
 
8727
+ // src/codebase-index/project-server-protocol.ts
8728
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
8729
+ function encodeProjectServerMessage(message) {
8730
+ return encodeJsonFrame(message);
8731
+ }
8732
+
7420
8733
  // src/codebase-index/project-server-client.ts
7421
8734
  var ProjectServerConnection = class {
7422
8735
  constructor(projectRoot, indexDir, endpoint) {
@@ -7429,11 +8742,13 @@ var ProjectServerConnection = class {
7429
8742
  indexDir;
7430
8743
  endpoint;
7431
8744
  socket = null;
7432
- buffer = "";
7433
- /** P6: binary frame buffer accumulates raw bytes when in binary mode. */
7434
- binaryBuffer = [];
7435
- /** P6: StringDecoder for safe UTF-8 multibyte handling in JSON mode. */
7436
- textDecoder = null;
8745
+ /**
8746
+ * Raw inbound bytes for the unified per-frame reader. Frames are sniffed
8747
+ * individually — JSON text (newline-terminated) or binary (magic 0x57) —
8748
+ * instead of latching a read mode, so a JSON broadcast between binary
8749
+ * frames cannot desynchronize the reader.
8750
+ */
8751
+ readBuffer = Buffer.alloc(0);
7437
8752
  /** P6: true once the server advertises binary support and client accepts. */
7438
8753
  useBinary = false;
7439
8754
  info = null;
@@ -7575,7 +8890,7 @@ var ProjectServerConnection = class {
7575
8890
  this.activity = null;
7576
8891
  this.health = null;
7577
8892
  this.useBinary = false;
7578
- this.binaryBuffer = [];
8893
+ this.readBuffer = Buffer.alloc(0);
7579
8894
  this.connectReject?.(new Error("codebase-index client disconnected"));
7580
8895
  this.connectResolve = null;
7581
8896
  this.connectReject = null;
@@ -7596,7 +8911,7 @@ var ProjectServerConnection = class {
7596
8911
  currentAuthToken() {
7597
8912
  if (this.authToken === void 0) {
7598
8913
  try {
7599
- const raw = fs12.readFileSync(
8914
+ const raw = fs13.readFileSync(
7600
8915
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
7601
8916
  "utf8"
7602
8917
  );
@@ -7703,10 +9018,8 @@ var ProjectServerConnection = class {
7703
9018
  this.info = null;
7704
9019
  this.activity = null;
7705
9020
  this.health = null;
7706
- this.buffer = "";
7707
- this.binaryBuffer = [];
9021
+ this.readBuffer = Buffer.alloc(0);
7708
9022
  this.useBinary = false;
7709
- this.textDecoder = null;
7710
9023
  return new Promise((resolve4, reject) => {
7711
9024
  const socket = net.createConnection(this.endpoint);
7712
9025
  this.socket = socket;
@@ -7736,18 +9049,47 @@ var ProjectServerConnection = class {
7736
9049
  socket.on("close", () => this.onClose(socket));
7737
9050
  });
7738
9051
  }
9052
+ /**
9053
+ * Unified per-frame reader. Each frame is sniffed by its first byte:
9054
+ * `0x57` ('W') → length-prefixed MessagePack binary, anything else →
9055
+ * newline-delimited JSON text. Sniffing per frame (instead of latching a
9056
+ * mode) is what makes mixed streams work: the server may interleave a JSON
9057
+ * `index-state` broadcast between binary responses, and an old JSON-only
9058
+ * server stays readable while `useBinary` is armed.
9059
+ *
9060
+ * Multibyte UTF-8 in JSON frames is safe: raw `0x0a` only occurs as the
9061
+ * JSON delimiter (inside JSON strings `\n` is escaped), so a complete line
9062
+ * is always complete UTF-8.
9063
+ */
7739
9064
  onData(socket, chunk) {
7740
9065
  if (socket !== this.socket) return;
7741
- if (this.useBinary) {
7742
- this.onBinaryData(socket, chunk);
7743
- return;
7744
- }
7745
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
7746
- this.buffer += this.textDecoder.write(chunk);
9066
+ this.readBuffer = this.readBuffer.length === 0 ? chunk : Buffer.concat([this.readBuffer, chunk]);
7747
9067
  while (true) {
7748
- const newline = this.buffer.indexOf("\n");
9068
+ if (this.readBuffer.length === 0) return;
9069
+ if (this.useBinary && isBinaryFrame(this.readBuffer[0])) {
9070
+ if (this.readBuffer.length < 5) return;
9071
+ const frameLen = this.readBuffer.readUInt32BE(1);
9072
+ if (frameLen > MAX_BINARY_FRAME_BYTES) {
9073
+ socket.destroy();
9074
+ this.transition("offline", { error: "binary frame length exceeds the IPC limit" });
9075
+ return;
9076
+ }
9077
+ if (this.readBuffer.length < 5 + frameLen) return;
9078
+ const payload = this.readBuffer.subarray(5, 5 + frameLen);
9079
+ this.readBuffer = this.readBuffer.subarray(5 + frameLen);
9080
+ let message2;
9081
+ try {
9082
+ message2 = decodeBinaryFrame(payload);
9083
+ } catch {
9084
+ socket.destroy(new Error("invalid binary codebase-index server response"));
9085
+ return;
9086
+ }
9087
+ this.onMessage(message2);
9088
+ continue;
9089
+ }
9090
+ const newline = this.readBuffer.indexOf(10);
7749
9091
  if (newline < 0) {
7750
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
9092
+ if (this.readBuffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
7751
9093
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7752
9094
  }
7753
9095
  return;
@@ -7756,8 +9098,8 @@ var ProjectServerConnection = class {
7756
9098
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7757
9099
  return;
7758
9100
  }
7759
- const line = this.buffer.slice(0, newline);
7760
- this.buffer = this.buffer.slice(newline + 1);
9101
+ const line = this.readBuffer.subarray(0, newline).toString("utf8");
9102
+ this.readBuffer = this.readBuffer.subarray(newline + 1);
7761
9103
  if (!line) continue;
7762
9104
  let message;
7763
9105
  try {
@@ -7769,44 +9111,6 @@ var ProjectServerConnection = class {
7769
9111
  this.onMessage(message);
7770
9112
  }
7771
9113
  }
7772
- /**
7773
- * P6: Parse binary frames from the raw buffer.
7774
- *
7775
- * Each frame is: [1 byte magic 0x57] [4 bytes uint32 BE length] [payload].
7776
- * The magic byte distinguishes binary from JSON — a JSON frame's first byte
7777
- * is `{` (0x7B), so there is no ambiguity even in a mixed-mode buffer.
7778
- */
7779
- onBinaryData(socket, chunk) {
7780
- this.binaryBuffer.push(chunk);
7781
- const all = Buffer.concat(this.binaryBuffer);
7782
- let offset = 0;
7783
- while (offset + 5 <= all.length) {
7784
- if (!isBinaryFrame(all[offset])) {
7785
- this.useBinary = false;
7786
- this.buffer += all.subarray(offset).toString("utf8");
7787
- this.binaryBuffer = [];
7788
- return;
7789
- }
7790
- const frameLen = all.readUInt32BE(offset + 1);
7791
- if (frameLen > 256 * 1024 * 1024) {
7792
- socket.destroy();
7793
- this.transition("offline", { error: "binary frame length exceeds 256MB limit" });
7794
- return;
7795
- }
7796
- const totalLen = 5 + frameLen;
7797
- if (offset + totalLen > all.length) break;
7798
- const payload = all.subarray(offset + 5, offset + 5 + frameLen);
7799
- try {
7800
- const message = decodeBinaryFrame(payload);
7801
- this.onMessage(message);
7802
- } catch {
7803
- socket.destroy(new Error("invalid binary codebase-index server response"));
7804
- return;
7805
- }
7806
- offset += totalLen;
7807
- }
7808
- this.binaryBuffer = offset < all.length ? [all.subarray(offset)] : [];
7809
- }
7810
9114
  onMessage(message) {
7811
9115
  if (message.type === "hello") {
7812
9116
  if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
@@ -7826,7 +9130,7 @@ var ProjectServerConnection = class {
7826
9130
  }
7827
9131
  this.info = message;
7828
9132
  this.markResponsive();
7829
- if (message.binarySupported) this.useBinary = true;
9133
+ if (message.binarySupported && binaryFramingEnabled()) this.useBinary = true;
7830
9134
  this.transition("connected", { pid: message.pid });
7831
9135
  ensureHeartbeatLoop();
7832
9136
  this.connectResolve?.();
@@ -7912,13 +9216,13 @@ var ProjectServerConnection = class {
7912
9216
  if (!url) throw new Error("built codebase-index project server is unavailable");
7913
9217
  if (process.platform !== "win32") {
7914
9218
  try {
7915
- fs12.rmSync(this.endpoint, { force: true });
9219
+ fs13.rmSync(this.endpoint, { force: true });
7916
9220
  } catch {
7917
9221
  }
7918
9222
  }
7919
9223
  const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
7920
9224
  if (this.indexDir) args.push("--index-dir", this.indexDir);
7921
- const child = spawn3(process.execPath, args, {
9225
+ const child = spawn4(process.execPath, args, {
7922
9226
  detached: true,
7923
9227
  stdio: "ignore",
7924
9228
  windowsHide: true,
@@ -7936,8 +9240,8 @@ var ProjectServerConnection = class {
7936
9240
  process.kill(pid);
7937
9241
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
7938
9242
  try {
7939
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
7940
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
9243
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
9244
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
7941
9245
  } catch {
7942
9246
  }
7943
9247
  return true;
@@ -7949,6 +9253,10 @@ var ProjectServerConnection = class {
7949
9253
  var connections = /* @__PURE__ */ new Map();
7950
9254
  var MAX_CACHED_CONNECTIONS = 8;
7951
9255
  var heartbeatTimer;
9256
+ function binaryFramingEnabled() {
9257
+ const flag = process.env["WRONGSTACK_INDEX_BINARY"];
9258
+ return flag === "1" || flag === "true";
9259
+ }
7952
9260
  function forgetConnection(endpoint, connection) {
7953
9261
  if (connections.get(endpoint) === connection) connections.delete(endpoint);
7954
9262
  connection.close();
@@ -8038,7 +9346,7 @@ function resolveWorkerUrl() {
8038
9346
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8039
9347
  try {
8040
9348
  const url = new URL(rel, import.meta.url);
8041
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
9349
+ if (url.protocol === "file:" && fs14.existsSync(fileURLToPath6(url))) return url;
8042
9350
  } catch {
8043
9351
  }
8044
9352
  }
@@ -8291,7 +9599,7 @@ var readTool = {
8291
9599
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
8292
9600
  let stat3;
8293
9601
  try {
8294
- stat3 = await fs14.stat(absPath);
9602
+ stat3 = await fs15.stat(absPath);
8295
9603
  } catch (err) {
8296
9604
  const code = err.code;
8297
9605
  if (code === "ENOENT") {
@@ -8343,7 +9651,7 @@ var readTool = {
8343
9651
  ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
8344
9652
  };
8345
9653
  }
8346
- const buf = await fs14.readFile(absPath);
9654
+ const buf = await fs15.readFile(absPath);
8347
9655
  if (isBinaryBuffer(buf)) {
8348
9656
  throw new FsError({
8349
9657
  message: `read: "${input.path}" appears to be binary`,