@wrongstack/tools 0.309.0 → 0.310.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/_regex.d.ts +6 -34
  2. package/dist/audit.js +3 -3
  3. package/dist/bash.js +4 -4
  4. package/dist/builtin.js +3020 -1680
  5. package/dist/codebase-index/binary-frame.d.ts +57 -8
  6. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +6 -0
  7. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +6 -0
  8. package/dist/codebase-index/codebase-search-tool.d.ts +15 -5
  9. package/dist/codebase-index/index-service.d.ts +3 -19
  10. package/dist/codebase-index/index.js +2784 -1464
  11. package/dist/codebase-index/indexer.d.ts +3 -0
  12. package/dist/codebase-index/parser-batch.d.ts +53 -0
  13. package/dist/codebase-index/parser-dispatch.d.ts +32 -0
  14. package/dist/codebase-index/parser-output.d.ts +14 -0
  15. package/dist/codebase-index/parser-worker-pool.d.ts +57 -4
  16. package/dist/codebase-index/parser-worker-script.d.ts +5 -2
  17. package/dist/codebase-index/parser-worker-script.js +4042 -0
  18. package/dist/codebase-index/project-server-cache.d.ts +16 -0
  19. package/dist/codebase-index/project-server-client.d.ts +2 -2
  20. package/dist/codebase-index/project-server-query-cache.d.ts +88 -0
  21. package/dist/codebase-index/project-server.js +2846 -1308
  22. package/dist/codebase-index/py-parser.d.ts +5 -0
  23. package/dist/codebase-index/schema.d.ts +14 -1
  24. package/dist/codebase-index/sqlite-runtime.d.ts +2 -2
  25. package/dist/codebase-index/tree-sitter/queries.d.ts +30 -3
  26. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  27. package/dist/codebase-index/vector-search.d.ts +12 -0
  28. package/dist/codebase-index/wal-maintenance.d.ts +58 -0
  29. package/dist/codebase-index/worker-protocol/contracts.d.ts +44 -0
  30. package/dist/codebase-index/worker-protocol.d.ts +17 -1
  31. package/dist/codebase-index/worker.js +2300 -1020
  32. package/dist/codebase-index/writer-admin.d.ts +11 -0
  33. package/dist/codebase-index/writer-helpers.d.ts +31 -1
  34. package/dist/codebase-index/writer-mutations.d.ts +0 -6
  35. package/dist/codebase-index/writer-schema.d.ts +2 -2
  36. package/dist/codebase-index/writer.d.ts +15 -0
  37. package/dist/edit.js +2511 -1203
  38. package/dist/exec.js +8 -6
  39. package/dist/format.js +3 -3
  40. package/dist/git.js +6 -0
  41. package/dist/grep.js +5 -124
  42. package/dist/index.js +3016 -1739
  43. package/dist/install.js +3 -3
  44. package/dist/json.js +5 -124
  45. package/dist/kanban.js +130 -0
  46. package/dist/languages/index.js +3 -3
  47. package/dist/lint.js +3 -3
  48. package/dist/logs.js +5 -121
  49. package/dist/outdated.js +3 -3
  50. package/dist/pack.js +3020 -1680
  51. package/dist/patch.js +2524 -1216
  52. package/dist/plan.js +106 -0
  53. package/dist/process-registry.js +1 -1
  54. package/dist/read.js +2506 -1198
  55. package/dist/replace.js +2487 -1295
  56. package/dist/search.js +6 -2
  57. package/dist/session-kanban.js +24 -16
  58. package/dist/task.js +106 -0
  59. package/dist/test.js +3 -3
  60. package/dist/todo.js +106 -0
  61. package/dist/tool-tier.d.ts +11 -0
  62. package/dist/tool-tier.js +3028 -1680
  63. package/dist/tree.js +14 -3
  64. package/dist/typecheck.js +3 -3
  65. package/dist/win32.js +5 -5
  66. package/dist/write.js +2513 -1205
  67. package/package.json +5 -4
package/dist/write.js CHANGED
@@ -173,238 +173,133 @@ var init_languages = __esm({
173
173
  }
174
174
  });
175
175
 
176
- // src/codebase-index/ts-parser.ts
177
- var ts_parser_exports = {};
178
- __export(ts_parser_exports, {
179
- detectLang: () => detectLang,
180
- parseSymbols: () => parseSymbols
181
- });
182
- function loadTypescript2() {
183
- tsLoad2 ??= import("@typescript/typescript6").then((m) => {
184
- ts = m.default ?? m;
185
- return ts;
186
- });
187
- return tsLoad2;
188
- }
189
- function kindMap() {
190
- kindMapCache ??= {
191
- [ts.SyntaxKind.ClassDeclaration]: "class",
192
- [ts.SyntaxKind.InterfaceDeclaration]: "interface",
193
- [ts.SyntaxKind.EnumDeclaration]: "enum",
194
- [ts.SyntaxKind.TypeAliasDeclaration]: "type",
195
- [ts.SyntaxKind.FunctionDeclaration]: "function",
196
- [ts.SyntaxKind.MethodDeclaration]: "method",
197
- [ts.SyntaxKind.GetAccessor]: "property",
198
- [ts.SyntaxKind.SetAccessor]: "property",
199
- [ts.SyntaxKind.PropertyDeclaration]: "property",
200
- [ts.SyntaxKind.Parameter]: "parameter",
201
- [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
202
- };
203
- return kindMapCache;
204
- }
205
- function kindOf(node) {
206
- if (ts.isVariableDeclaration(node)) {
207
- const parent = node.parent;
208
- if (ts.isVariableDeclarationList(parent)) {
209
- const flags = parent.flags;
210
- if (flags & ts.NodeFlags.Let) return "let";
211
- if (flags & ts.NodeFlags.Const) return "const";
212
- return "var";
213
- }
214
- }
215
- if (ts.isModuleDeclaration(node)) return "namespace";
216
- return kindMap()[node.kind] ?? null;
217
- }
218
- function getSignature(printer, node, sourceFile) {
219
- const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
220
- return raw.replace(/\s+/g, " ").slice(0, 500);
221
- }
222
- function getJsDoc(node, sourceFile) {
223
- const fullText = sourceFile.getFullText();
224
- const nodePos = node.getFullStart();
225
- const comments = ts.getLeadingCommentRanges(fullText, nodePos);
226
- if (!comments) return "";
227
- for (const range of comments) {
228
- const commentText = fullText.slice(range.pos, range.end);
229
- const trimmed = commentText.trim();
230
- if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
231
- const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
232
- return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
233
- }
176
+ // src/codebase-index/import-extractor.ts
177
+ function lastSegment(specifier) {
178
+ const pathLike = /[/\\]|::/.test(specifier);
179
+ const segments = specifier.split(/[/\\]|::/).filter(Boolean);
180
+ let last = segments[segments.length - 1] ?? specifier;
181
+ if (last === "*" || last === "_") {
182
+ last = segments[segments.length - 2] ?? specifier;
234
183
  }
235
- return "";
184
+ if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
185
+ const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
186
+ return dotted[dotted.length - 1] ?? last;
236
187
  }
237
- function pushScopeName(node, parts) {
238
- if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
239
- parts.push(node.name?.text ?? "Anon");
240
- } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
241
- if (node.name && ts.isIdentifier(node.name)) {
242
- parts.push(node.name.text);
243
- }
188
+ function newlineOffsets(content) {
189
+ const offsets = [];
190
+ for (let i = 0; i < content.length; i++) {
191
+ if (content.charCodeAt(i) === 10) offsets.push(i);
244
192
  }
193
+ return offsets;
245
194
  }
246
- async function parseSymbols(opts) {
247
- const { file, content, lang } = opts;
248
- await loadTypescript2();
249
- let sourceFile;
250
- try {
251
- sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
252
- } catch {
253
- return { file, lang, symbols: [], mtimeMs: Date.now() };
254
- }
255
- const symbols = [];
256
- const refs = [];
257
- const printer = ts.createPrinter({});
258
- function visit(node, funcDepth, scopeParts) {
259
- const kind = kindOf(node);
260
- if (kind) {
261
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
262
- } else {
263
- const nameNode = node.name;
264
- if (!nameNode || !ts.isIdentifier(nameNode)) {
265
- return;
266
- }
267
- const name = nameNode.text;
268
- const pos2 = nameNode.getStart(sourceFile);
269
- const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
270
- const scope = scopeParts.join(".");
271
- const signature = getSignature(printer, node, sourceFile);
272
- const docComment = getJsDoc(node, sourceFile);
273
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
274
- symbols.push({
275
- id: 0,
276
- lang,
277
- kind,
278
- name,
279
- file,
280
- line: line2 + 1,
281
- col: character,
282
- signature,
283
- docComment,
284
- scope,
285
- text
286
- });
287
- }
288
- }
289
- const pos = node.getStart(sourceFile);
290
- const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
291
- const lineNum = line + 1;
292
- if (ts.isCallExpression(node)) {
293
- const expr = node.expression;
294
- if (ts.isIdentifier(expr)) {
295
- refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
296
- }
297
- } else if (ts.isPropertyAccessExpression(node)) {
298
- if (ts.isIdentifier(node.expression)) {
299
- refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
300
- }
301
- } else if (ts.isTypeReferenceNode(node)) {
302
- const name = getTypeName(node.typeName);
303
- if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
304
- } else if (ts.isHeritageClause(node)) {
305
- for (const t of node.types) {
306
- const name = getTypeName(t.expression);
307
- if (name)
308
- refs.push({
309
- fromId: 0,
310
- toName: name,
311
- callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
312
- line: lineNum
313
- });
314
- }
315
- } else if (ts.isImportDeclaration(node)) {
316
- emitImportSpecifierRefs(node, refs, lineNum);
317
- } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
318
- emitExportSpecifierRefs(node, refs, lineNum);
319
- }
320
- const scopeIdx = scopeParts.length;
321
- pushScopeName(node, scopeParts);
322
- const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
323
- ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
324
- scopeParts.length = scopeIdx;
195
+ function lineAt(offsets, index) {
196
+ let low = 0;
197
+ let high = offsets.length;
198
+ while (low < high) {
199
+ const mid = low + high >>> 1;
200
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
201
+ else high = mid;
325
202
  }
326
- visit(sourceFile, 0, []);
327
- return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
203
+ return low + 1;
328
204
  }
329
- function getTypeName(name) {
330
- if (ts.isIdentifier(name)) return name.text;
331
- if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
332
- return "";
205
+ function hasImportPatterns(lang) {
206
+ return LANG_IMPORTS[lang] !== void 0;
333
207
  }
334
- function deduplicateRefs(refs) {
208
+ function extractImports(opts) {
209
+ const patterns = LANG_IMPORTS[opts.lang];
210
+ if (!patterns || !opts.content) return [];
211
+ const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
212
+ const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
213
+ const refs = [];
335
214
  const seen = /* @__PURE__ */ new Set();
336
- return refs.filter((r) => {
337
- const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
338
- if (seen.has(key)) return false;
339
- seen.add(key);
340
- return true;
341
- });
342
- }
343
- function getImportSpecifierName(spec) {
344
- return spec.propertyName?.text ?? spec.name.text;
345
- }
346
- function emitImportSpecifierRefs(node, refs, lineNum) {
347
- const module = moduleSpecifierOf(node.moduleSpecifier);
348
- const clause = node.importClause;
349
- if (!clause) {
350
- if (module) {
351
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
352
- }
353
- return;
354
- }
355
- if (clause.name) {
356
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
357
- }
358
- const bindings = clause.namedBindings;
359
- if (!bindings) return;
360
- if (ts.isNamedImports(bindings)) {
361
- for (const element of bindings.elements) {
215
+ const offsets = newlineOffsets(content);
216
+ for (const pattern of patterns) {
217
+ const re = new RegExp(pattern.re.source, pattern.re.flags);
218
+ for (const match of content.matchAll(re)) {
219
+ if (refs.length >= limit) return refs;
220
+ const specifier = match[1]?.trim();
221
+ if (!specifier) continue;
222
+ const module = specifier;
223
+ const toName = pattern.name === "full" ? module : lastSegment(module);
224
+ if (!toName) continue;
225
+ const key = `${module}\0${toName}`;
226
+ if (seen.has(key)) continue;
227
+ seen.add(key);
362
228
  refs.push({
363
229
  fromId: 0,
364
- toName: getImportSpecifierName(element),
230
+ toName,
365
231
  callType: "import",
366
- line: lineNum,
232
+ line: lineAt(offsets, match.index ?? 0),
233
+ lang: opts.lang,
367
234
  module
368
235
  });
369
236
  }
370
- } else if (ts.isNamespaceImport(bindings)) {
371
- refs.push({
372
- fromId: 0,
373
- toName: bindings.name.text,
374
- callType: "import",
375
- line: lineNum,
376
- module
377
- });
378
237
  }
238
+ return refs;
379
239
  }
380
- function moduleSpecifierOf(node) {
381
- return node && ts.isStringLiteral(node) ? node.text : void 0;
382
- }
383
- function emitExportSpecifierRefs(node, refs, lineNum) {
384
- const module = moduleSpecifierOf(node.moduleSpecifier);
385
- const clause = node.exportClause;
386
- if (clause && ts.isNamespaceExport(clause)) {
387
- refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
388
- return;
389
- }
390
- if (clause && ts.isNamedExports(clause)) {
391
- for (const element of clause.elements) {
392
- const originalName = element.propertyName?.text ?? element.name.text;
393
- refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
394
- }
395
- return;
396
- }
397
- if (module) {
398
- refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
399
- }
400
- }
401
- var ts, tsLoad2, kindMapCache;
402
- var init_ts_parser = __esm({
403
- "src/codebase-index/ts-parser.ts"() {
240
+ var IMPORT_MAX_FILE_CHARS, IMPORT_MAX_PER_FILE, DOTTED_IMPORT, LANG_IMPORTS;
241
+ var init_import_extractor = __esm({
242
+ "src/codebase-index/import-extractor.ts"() {
404
243
  "use strict";
405
- init_languages();
406
- tsLoad2 = null;
407
- kindMapCache = null;
244
+ IMPORT_MAX_FILE_CHARS = 512 * 1024;
245
+ IMPORT_MAX_PER_FILE = 400;
246
+ DOTTED_IMPORT = [
247
+ { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
248
+ ];
249
+ LANG_IMPORTS = {
250
+ // Go and Python have real AST extractors; these patterns are the fallback for
251
+ // machines with no Go toolchain or Python interpreter installed, where the
252
+ // parser degrades to regex symbols and would otherwise contribute no edges.
253
+ go: [
254
+ { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
255
+ // Grouped form: inside `import ( … )` each line is an optional alias plus a
256
+ // quoted path. A stray match elsewhere resolves to no file and is dropped.
257
+ { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
258
+ ],
259
+ py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
260
+ rs: [
261
+ // use a::b::C; | use a::b::{C, D}; → the path before any brace
262
+ { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
263
+ // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
264
+ { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
265
+ ],
266
+ java: DOTTED_IMPORT,
267
+ kotlin: DOTTED_IMPORT,
268
+ scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
269
+ csharp: [
270
+ // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
271
+ { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
272
+ { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
273
+ ],
274
+ // Quoted includes only: <stdio.h> is a system header with no indexed file.
275
+ c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
276
+ cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
277
+ ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
278
+ php: [
279
+ // `use A\B\C` imports the class C, which is what the index has a symbol
280
+ // for — the namespace symbol only covers the `A\B` prefix.
281
+ { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
282
+ { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
283
+ ],
284
+ swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
285
+ dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
286
+ lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
287
+ elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
288
+ haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
289
+ zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
290
+ proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
291
+ // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
292
+ css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
293
+ // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
294
+ vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
295
+ svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
296
+ html: [
297
+ { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
298
+ { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
299
+ ],
300
+ shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
301
+ r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
302
+ };
408
303
  }
409
304
  });
410
305
 
@@ -490,6 +385,32 @@ function parseParserOutput(stdout, lang) {
490
385
  refs: dedupeRefs(coerceRefs(record.refs, lang))
491
386
  };
492
387
  }
388
+ function parseParserBatchOutput(stdout, lang) {
389
+ const trimmed = stdout.trim();
390
+ if (!trimmed) return [];
391
+ let parsed;
392
+ try {
393
+ parsed = JSON.parse(trimmed);
394
+ } catch {
395
+ return [];
396
+ }
397
+ if (!parsed || typeof parsed !== "object") return [];
398
+ const results = parsed.results;
399
+ if (!Array.isArray(results)) return [];
400
+ return results.flatMap((entry) => {
401
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
402
+ const candidate = entry;
403
+ if (typeof candidate.file !== "string" || !candidate.file) return [];
404
+ return [
405
+ {
406
+ file: candidate.file,
407
+ error: typeof candidate.error === "string" && candidate.error ? candidate.error : void 0,
408
+ symbols: coerceSymbols(candidate.symbols),
409
+ refs: dedupeRefs(coerceRefs(candidate.refs, lang))
410
+ }
411
+ ];
412
+ });
413
+ }
493
414
  function dedupeRefs(refs) {
494
415
  const seen = /* @__PURE__ */ new Set();
495
416
  return refs.filter((ref) => {
@@ -530,187 +451,172 @@ var init_spawn_gate = __esm({
530
451
  }
531
452
  });
532
453
 
533
- // src/codebase-index/go-parser.ts
534
- var go_parser_exports = {};
535
- __export(go_parser_exports, {
536
- detectLang: () => detectLang,
537
- parseSymbols: () => parseSymbols2
538
- });
454
+ // src/codebase-index/parser-batch.ts
539
455
  import { spawn } from "node:child_process";
456
+ import * as fsSync from "node:fs";
457
+ import * as fs4 from "node:fs/promises";
540
458
  import * as os from "node:os";
541
459
  import * as path8 from "node:path";
542
- import * as fs4 from "node:fs/promises";
543
- async function parseSymbols2(opts) {
544
- const { file, content, lang } = opts;
545
- try {
546
- const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
547
- if (parsed.symbols.length > 0) {
548
- return parsed;
460
+ function chunkBatchFiles(files) {
461
+ const chunks = [];
462
+ let current = [];
463
+ let bytes = 0;
464
+ for (const file of files) {
465
+ const size = Buffer.byteLength(file.content, "utf8");
466
+ if (current.length > 0 && (current.length >= MAX_BATCH_FILES || bytes + size > MAX_BATCH_BYTES)) {
467
+ chunks.push(current);
468
+ current = [];
469
+ bytes = 0;
470
+ }
471
+ current.push(file);
472
+ bytes += size;
473
+ }
474
+ if (current.length > 0) chunks.push(current);
475
+ return chunks;
476
+ }
477
+ function batchTimeoutMs(fileCount) {
478
+ return Math.min(12e4, 15e3 + fileCount * 1500);
479
+ }
480
+ async function ensureScriptPath(cached, prefix, fileName, script) {
481
+ if (cached) return { path: cached, wrote: false };
482
+ const dir = await fs4.mkdtemp(path8.join(os.tmpdir(), prefix));
483
+ const scriptPath = path8.join(dir, fileName);
484
+ await fs4.writeFile(scriptPath, script, { encoding: "utf8", flag: "wx" });
485
+ process.once("exit", () => {
486
+ try {
487
+ fsSync.rmSync(dir, { recursive: true, force: true });
488
+ } catch {
549
489
  }
550
- const fallback = fallbackParse(file, content, lang);
551
- return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
552
- } catch {
553
- return fallbackParse(file, content, lang);
554
- }
490
+ });
491
+ return { path: scriptPath, wrote: true };
555
492
  }
556
- function fallbackParse(filePath, content, lang) {
557
- if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
558
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
559
- }
560
- const symbols = [];
561
- const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
562
- const lines = content.split(/\r?\n/);
563
- for (const [idx, line] of lines.entries()) {
564
- const trimmed = line.trimStart();
565
- const col = line.length - trimmed.length + 1;
566
- const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
567
- if (fn?.[1]) {
568
- addFallbackSymbol(symbols, {
569
- filePath,
570
- lang,
571
- kind: trimmed.startsWith("func (") ? "method" : "function",
572
- name: fn[1],
573
- line: idx + 1,
574
- col,
575
- signature: trimmed,
576
- scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
577
- });
578
- continue;
579
- }
580
- const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
581
- if (typeDecl?.[1]) {
582
- addFallbackSymbol(symbols, {
583
- filePath,
584
- lang,
585
- kind: "type",
586
- name: typeDecl[1],
587
- line: idx + 1,
588
- col,
589
- signature: trimmed,
590
- scope: packageName
591
- });
592
- continue;
593
- }
594
- const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
595
- if (valueDecl?.[1] && valueDecl[2]) {
596
- addFallbackSymbol(symbols, {
597
- filePath,
598
- lang,
599
- kind: valueDecl[1],
600
- name: valueDecl[2],
601
- line: idx + 1,
602
- col,
603
- signature: trimmed,
604
- scope: packageName
605
- });
493
+ function runToolchainChild(binary, args, stdinPayload, timeoutMs) {
494
+ return new Promise((resolve4) => {
495
+ let settled = false;
496
+ let stdout = "";
497
+ let proc;
498
+ try {
499
+ proc = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
500
+ } catch {
501
+ resolve4(null);
502
+ return;
606
503
  }
607
- }
608
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
609
- }
610
- function addFallbackSymbol(symbols, opts) {
611
- symbols.push({
612
- id: 0,
613
- lang: opts.lang,
614
- kind: opts.kind,
615
- name: opts.name,
616
- file: opts.filePath,
617
- line: opts.line,
618
- col: opts.col,
619
- signature: opts.signature,
620
- docComment: "",
621
- scope: opts.scope,
622
- text: `${opts.name} ${opts.signature}`.trim()
504
+ const finish = (value) => {
505
+ if (settled) return;
506
+ settled = true;
507
+ clearTimeout(timer);
508
+ resolve4(value);
509
+ };
510
+ const timer = setTimeout(() => {
511
+ proc.kill("SIGKILL");
512
+ finish(null);
513
+ }, timeoutMs);
514
+ timer.unref?.();
515
+ proc.on("error", () => finish(null));
516
+ proc.stdout?.on("data", (chunk) => {
517
+ stdout += chunk.toString();
518
+ });
519
+ proc.stderr?.resume();
520
+ proc.stdin?.on("error", () => {
521
+ });
522
+ proc.stdin?.write(stdinPayload);
523
+ proc.stdin?.end();
524
+ proc.on("close", (code) => finish({ code, stdout }));
623
525
  });
624
526
  }
625
- function hasUnbalancedDelimiters(content) {
626
- const pairs = { "(": ")", "[": "]", "{": "}" };
627
- const closers = new Set(Object.values(pairs));
628
- const stack = [];
629
- for (const ch of content) {
630
- if (pairs[ch]) {
631
- stack.push(pairs[ch]);
632
- } else if (closers.has(ch) && stack.pop() !== ch) {
633
- return true;
634
- }
527
+ async function runGoBatch(files, goBinary) {
528
+ const out = /* @__PURE__ */ new Map();
529
+ if (files.length === 0) return out;
530
+ const { path: scriptPath } = await ensureScriptPath(
531
+ _goBatchScriptPath,
532
+ "ws-go-parse",
533
+ "batch.go",
534
+ GO_BATCH_SCRIPT
535
+ );
536
+ _goBatchScriptPath = scriptPath;
537
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
538
+ const result = await withSpawnGate(
539
+ () => runToolchainChild(
540
+ goBinary ?? resolveWin32Command("go"),
541
+ ["run", scriptPath],
542
+ payload,
543
+ batchTimeoutMs(files.length)
544
+ )
545
+ );
546
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
547
+ for (const entry of parseParserBatchOutput(result.stdout, "go")) {
548
+ if (entry.error !== void 0) continue;
549
+ out.set(entry.file, {
550
+ file: entry.file,
551
+ lang: "go",
552
+ symbols: entry.symbols.map((s) => ({
553
+ id: 0,
554
+ lang: "go",
555
+ kind: s.kind,
556
+ name: s.name,
557
+ file: entry.file,
558
+ line: s.line,
559
+ col: s.col,
560
+ signature: s.signature ?? "",
561
+ docComment: "",
562
+ scope: s.scope ?? "",
563
+ text: `${s.name} ${s.signature ?? ""}`.trim()
564
+ })),
565
+ refs: entry.refs,
566
+ mtimeMs: Date.now()
567
+ });
635
568
  }
636
- return stack.length > 0;
569
+ return out;
637
570
  }
638
- async function syncGoParse(filePath, content, lang) {
639
- try {
640
- let scriptPath = _cachedGoScriptPath;
641
- if (!scriptPath) {
642
- const tmpDir = await fs4.mkdtemp(path8.join(os.tmpdir(), "ws-go-parse-"));
643
- scriptPath = path8.join(tmpDir, "parse.go");
644
- await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
645
- _cachedGoScriptPath = scriptPath;
646
- }
647
- const goBinary = resolveWin32Command("go");
648
- const goResult = await new Promise(
649
- (resolve4, reject) => {
650
- let settled = false;
651
- const proc = spawn(goBinary, ["run", scriptPath], {
652
- stdio: ["pipe", "pipe", "pipe"],
653
- windowsHide: true
654
- });
655
- proc.on("error", (err) => {
656
- if (settled) return;
657
- settled = true;
658
- reject(err);
659
- });
660
- let stdout2 = "";
661
- proc.stdout?.on("data", (chunk) => {
662
- stdout2 += chunk.toString();
663
- });
664
- proc.stderr?.resume();
665
- proc.stdin?.write(content);
666
- proc.stdin?.end();
667
- const timer = setTimeout(() => {
668
- if (settled) return;
669
- settled = true;
670
- proc.kill("SIGKILL");
671
- reject(new Error("timeout"));
672
- }, 15e3);
673
- timer.unref?.();
674
- proc.on("close", (code2) => {
675
- if (settled) return;
676
- settled = true;
677
- clearTimeout(timer);
678
- resolve4({ code: code2, stdout: stdout2 });
679
- });
680
- }
681
- );
682
- const { code, stdout } = goResult;
683
- if (code !== 0 || !stdout.trim()) {
684
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
685
- }
686
- const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
687
- const symbols = rawSymbols.map((s) => ({
688
- id: 0,
689
- lang,
690
- kind: s.kind,
691
- name: s.name,
692
- file: filePath,
693
- line: s.line,
694
- col: s.col,
695
- signature: s.signature ?? "",
696
- docComment: "",
697
- scope: s.scope ?? "",
698
- text: `${s.name} ${s.signature ?? ""}`.trim()
699
- }));
700
- return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
701
- } catch {
702
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
571
+ async function runPyBatch(files, pythonBinary) {
572
+ const out = /* @__PURE__ */ new Map();
573
+ if (files.length === 0) return out;
574
+ const { path: scriptPath } = await ensureScriptPath(
575
+ _pyBatchScriptPath,
576
+ "ws-py-parse",
577
+ "batch.py",
578
+ PY_BATCH_SCRIPT
579
+ );
580
+ _pyBatchScriptPath = scriptPath;
581
+ const payload = JSON.stringify(files.map((f) => ({ file: f.file, content: f.content })));
582
+ const result = await withSpawnGate(
583
+ () => runToolchainChild(pythonBinary, [scriptPath], payload, batchTimeoutMs(files.length))
584
+ );
585
+ if (result?.code !== 0 || !result.stdout.trim()) return out;
586
+ for (const entry of parseParserBatchOutput(result.stdout, "py")) {
587
+ if (entry.error !== void 0) continue;
588
+ out.set(entry.file, {
589
+ file: entry.file,
590
+ lang: "py",
591
+ symbols: entry.symbols.map((s) => ({
592
+ id: 0,
593
+ lang: "py",
594
+ kind: s.kind,
595
+ name: s.name,
596
+ file: entry.file,
597
+ line: s.line,
598
+ col: s.col,
599
+ signature: s.signature ?? "",
600
+ docComment: "",
601
+ scope: s.scope ?? "",
602
+ text: `${s.name} ${s.signature ?? ""}`.trim()
603
+ })),
604
+ refs: entry.refs,
605
+ mtimeMs: Date.now()
606
+ });
703
607
  }
608
+ return out;
704
609
  }
705
- var GO_PARSE_SCRIPT, _cachedGoScriptPath;
706
- var init_go_parser = __esm({
707
- "src/codebase-index/go-parser.ts"() {
610
+ var MAX_BATCH_FILES, MAX_BATCH_BYTES, GO_BATCH_SCRIPT, PY_BATCH_SCRIPT, _goBatchScriptPath, _pyBatchScriptPath;
611
+ var init_parser_batch = __esm({
612
+ "src/codebase-index/parser-batch.ts"() {
708
613
  "use strict";
709
614
  init_win32_resolve();
710
615
  init_parser_output();
711
616
  init_spawn_gate();
712
- init_languages();
713
- GO_PARSE_SCRIPT = `
617
+ MAX_BATCH_FILES = 100;
618
+ MAX_BATCH_BYTES = 8 * 1024 * 1024;
619
+ GO_BATCH_SCRIPT = `
714
620
  package main
715
621
 
716
622
  import (
@@ -734,8 +640,6 @@ type Sym struct {
734
640
  Scope string \`json:"scope"\`
735
641
  }
736
642
 
737
- // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
738
- // yields both. Module is the import path for CallType "import", else empty.
739
643
  type Ref struct {
740
644
  ToName string \`json:"toName"\`
741
645
  CallType string \`json:"callType"\`
@@ -743,57 +647,53 @@ type Ref struct {
743
647
  Module string \`json:"module"\`
744
648
  }
745
649
 
746
- type Result struct {
747
- Symbols []Sym \`json:"symbols"\`
748
- Refs []Ref \`json:"refs"\`
650
+ type FileResult struct {
651
+ File string \`json:"file"\`
652
+ Error string \`json:"error,omitempty"\`
653
+ Symbols []Sym \`json:"symbols"\`
654
+ Refs []Ref \`json:"refs"\`
749
655
  }
750
656
 
751
- func emptyResult() string {
752
- return "{\\"symbols\\":[],\\"refs\\":[]}"
657
+ type BatchResult struct {
658
+ Version int \`json:"version"\`
659
+ Results []FileResult \`json:"results"\`
753
660
  }
754
661
 
755
- func main() {
756
- src, err := io.ReadAll(os.Stdin)
757
- if err != nil {
758
- fmt.Print(emptyResult())
759
- return
760
- }
662
+ type inputFile struct {
663
+ File string \`json:"file"\`
664
+ Content string \`json:"content"\`
665
+ }
666
+
667
+ func parseOne(name string, src []byte) FileResult {
668
+ res := FileResult{File: name, Symbols: []Sym{}, Refs: []Ref{}}
761
669
  fset := token.NewFileSet()
762
670
  node, err := parser.ParseFile(fset, "src.go", src, 0)
763
671
  if err != nil {
764
- fmt.Print(emptyResult())
765
- return
672
+ res.Error = err.Error()
673
+ return res
766
674
  }
767
-
768
- var syms []Sym
769
-
770
- // Package-level scope
771
675
  pkgScope := node.Name.Name
772
-
773
- // Collect all top-level declarations
774
676
  for _, decl := range node.Decls {
775
677
  switch d := decl.(type) {
776
678
  case *ast.FuncDecl:
777
- name := d.Name.Name
679
+ symName := d.Name.Name
778
680
  kind := "function"
779
681
  scope := pkgScope
780
682
  if d.Recv != nil && len(d.Recv.List) > 0 {
781
- scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
683
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + symName
782
684
  kind = "method"
783
685
  } else {
784
- scope = pkgScope + "." + name
686
+ scope = pkgScope + "." + symName
785
687
  }
786
688
  pos := fset.Position(d.Pos())
787
- sig := formatFuncSig(d)
788
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
789
-
689
+ res.Symbols = append(res.Symbols, Sym{Name: symName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: formatFuncSig(d), Scope: scope})
790
690
  case *ast.GenDecl:
791
691
  for _, spec := range d.Specs {
792
692
  switch s := spec.(type) {
793
693
  case *ast.TypeSpec:
794
- name := s.Name.Name
694
+ typeName := s.Name.Name
795
695
  pos := fset.Position(s.Pos())
796
- sig := "type " + name
696
+ sig := "type " + typeName
797
697
  if s.TypeParams != nil {
798
698
  sig += formatTypeParams(s.TypeParams)
799
699
  }
@@ -804,64 +704,72 @@ func main() {
804
704
  } else {
805
705
  sig += " = " + formatType(s.Type)
806
706
  }
807
- syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
808
-
707
+ res.Symbols = append(res.Symbols, Sym{Name: typeName, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
809
708
  case *ast.ValueSpec:
810
709
  for _, n := range s.Names {
811
- name := n.Name
710
+ valueName := n.Name
812
711
  pos := fset.Position(n.Pos())
813
712
  kind := "var"
814
713
  if d.Tok == token.CONST {
815
714
  kind = "const"
816
715
  }
817
- sig := kind + " " + name
716
+ sig := kind + " " + valueName
818
717
  if s.Type != nil {
819
718
  sig += " " + formatType(s.Type)
820
719
  }
821
- syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
720
+ res.Symbols = append(res.Symbols, Sym{Name: valueName, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
822
721
  }
823
722
  }
824
723
  }
825
724
  }
826
725
  }
827
-
828
- refs := []Ref{}
829
726
  ast.Inspect(node, func(n ast.Node) bool {
830
727
  switch expr := n.(type) {
831
728
  case *ast.CallExpr:
832
729
  line := fset.Position(expr.Pos()).Line
833
730
  switch fun := expr.Fun.(type) {
834
731
  case *ast.Ident:
835
- refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
732
+ res.Refs = append(res.Refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
836
733
  case *ast.SelectorExpr:
837
- // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
838
- // declared symbol name, so it resolves the same way the TypeScript
839
- // and Python extractors' call refs do.
840
- refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
734
+ res.Refs = append(res.Refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
841
735
  }
842
736
  case *ast.ImportSpec:
843
737
  if expr.Path != nil {
844
738
  if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
845
739
  line := fset.Position(expr.Pos()).Line
846
- // A Go import names a package, not a symbol; the package's
847
- // last path segment is the name it is referenced by.
848
740
  name := importPath
849
741
  if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
850
742
  name = importPath[idx+1:]
851
743
  }
852
- refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
744
+ res.Refs = append(res.Refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
853
745
  }
854
746
  }
855
747
  }
856
748
  return true
857
749
  })
750
+ return res
751
+ }
858
752
 
859
- if syms == nil {
860
- syms = []Sym{}
753
+ func main() {
754
+ raw, err := io.ReadAll(os.Stdin)
755
+ if err != nil {
756
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
757
+ fmt.Print(string(out))
758
+ return
861
759
  }
862
- data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
760
+ var inputs []inputFile
761
+ if err := json.Unmarshal(raw, &inputs); err != nil {
762
+ out, _ := json.Marshal(BatchResult{Version: 1, Results: []FileResult{}})
763
+ fmt.Print(string(out))
764
+ return
765
+ }
766
+ results := make([]FileResult, 0, len(inputs))
767
+ for _, in := range inputs {
768
+ results = append(results, parseOne(in.File, []byte(in.Content)))
769
+ }
770
+ data, err := json.Marshal(BatchResult{Version: 1, Results: results})
863
771
  if err != nil {
864
- fmt.Print(emptyResult())
772
+ fmt.Print("{\\"version\\":1,\\"results\\":[]}")
865
773
  return
866
774
  }
867
775
  fmt.Print(string(data))
@@ -981,10 +889,8 @@ func formatType(t ast.Expr) string {
981
889
  case *ast.BasicLit:
982
890
  return v.Value
983
891
  case *ast.IndexExpr:
984
- // Generic instantiation with one type arg, e.g. Logger[int].
985
892
  return formatType(v.X) + "[" + formatType(v.Index) + "]"
986
893
  case *ast.IndexListExpr:
987
- // Generic instantiation with multiple type args, e.g. Map[K, V].
988
894
  args := make([]string, len(v.Indices))
989
895
  for i, idx := range v.Indices {
990
896
  args[i] = formatType(idx)
@@ -995,7 +901,187 @@ func formatType(t ast.Expr) string {
995
901
  }
996
902
  }
997
903
  `;
998
- _cachedGoScriptPath = null;
904
+ PY_BATCH_SCRIPT = `import ast, json, sys
905
+
906
+ def get_name(node):
907
+ if isinstance(node, ast.Name):
908
+ return node.id
909
+ elif isinstance(node, ast.Attribute):
910
+ return get_name(node.value) + "." + node.attr
911
+ elif isinstance(node, ast.Subscript):
912
+ return get_name(node.value)
913
+ elif isinstance(node, ast.Call):
914
+ return get_name(node.func)
915
+ elif isinstance(node, ast.Constant):
916
+ return str(node.value)
917
+ return ""
918
+
919
+ def leaf_name(node):
920
+ if isinstance(node, ast.Attribute):
921
+ return node.attr
922
+ if isinstance(node, ast.Name):
923
+ return node.id
924
+ return get_name(node).split(".")[-1]
925
+
926
+ def is_private(name):
927
+ return name.startswith("__") and not name.endswith("__")
928
+
929
+ def parse_one(name, source, module_name):
930
+ result = {"file": name, "symbols": [], "refs": []}
931
+ try:
932
+ tree = ast.parse(source, filename=name)
933
+ except Exception as e:
934
+ result["error"] = str(e)
935
+ return result
936
+ syms = []
937
+ refs = []
938
+ scope_stack = [module_name]
939
+
940
+ def sym(d):
941
+ return {
942
+ "name": d["name"], "kind": d["kind"], "line": d["line"], "col": d["col"],
943
+ "signature": d["signature"], "scope": d["scope"],
944
+ }
945
+
946
+ class Visitor(ast.NodeVisitor):
947
+ def visit_ClassDef(self, node):
948
+ bases = [get_name(b) for b in node.bases]
949
+ sig = "class " + node.name
950
+ if bases:
951
+ sig += "(" + ", ".join(bases) + ")"
952
+ sig += ": ..."
953
+ syms.append(sym({
954
+ "name": node.name, "kind": "class", "line": node.lineno,
955
+ "col": node.col_offset, "signature": sig,
956
+ "scope": ".".join(scope_stack) + "." + node.name,
957
+ }))
958
+ scope_stack.append(node.name)
959
+ self.generic_visit(node)
960
+ scope_stack.pop()
961
+
962
+ def visit_FunctionDef(self, node):
963
+ args = ", ".join(a.arg for a in node.args.args)
964
+ returns = get_name(node.returns) if node.returns is not None else ""
965
+ is_async = isinstance(node, ast.AsyncFunctionDef)
966
+ kind = "function"
967
+ prefix = "def "
968
+ for dec in node.decorator_list:
969
+ d = get_name(dec)
970
+ if d.endswith(".staticmethod"):
971
+ kind = "staticmethod"
972
+ elif d.endswith(".classmethod"):
973
+ kind = "classmethod"
974
+ elif d == "property":
975
+ kind = "property"
976
+ if is_async:
977
+ kind = "async_" + kind
978
+ sig = f"{prefix}{node.name}({args})"
979
+ if returns:
980
+ sig += f" -> {returns}"
981
+ syms.append(sym({
982
+ "name": node.name, "kind": kind, "line": node.lineno,
983
+ "col": node.col_offset, "signature": sig,
984
+ "scope": ".".join(scope_stack) + "." + node.name,
985
+ }))
986
+
987
+ def visit_AsyncFunctionDef(self, node):
988
+ self.visit_FunctionDef(node)
989
+
990
+ def visit_Assign(self, node):
991
+ for target in node.targets:
992
+ if isinstance(target, ast.Name):
993
+ tname = target.id
994
+ if is_private(tname):
995
+ continue
996
+ kind = "const" if tname.isupper() else "var"
997
+ col = target.col_offset if hasattr(target, "col_offset") else 0
998
+ syms.append(sym({
999
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
1000
+ "signature": f"{tname} = ...", "scope": ".".join(scope_stack),
1001
+ }))
1002
+
1003
+ def visit_AnnAssign(self, node):
1004
+ if isinstance(node.target, ast.Name):
1005
+ tname = node.target.id
1006
+ if is_private(tname):
1007
+ return
1008
+ kind = "const" if tname.isupper() else "var"
1009
+ col = node.target.col_offset if hasattr(node.target, "col_offset") else 0
1010
+ sig = f"{tname}: {get_name(node.annotation)}"
1011
+ if node.value:
1012
+ sig += " = ..."
1013
+ syms.append(sym({
1014
+ "name": tname, "kind": kind, "line": node.lineno, "col": col,
1015
+ "signature": sig, "scope": ".".join(scope_stack),
1016
+ }))
1017
+
1018
+ def visit_Import(self, node):
1019
+ # Parity with the single-file parser: imports are symbols too.
1020
+ for alias in node.names:
1021
+ name = alias.asname or alias.name
1022
+ syms.append(sym({
1023
+ "name": name, "kind": "import", "line": node.lineno,
1024
+ "col": node.col_offset, "signature": f"import {alias.name}",
1025
+ "scope": ".".join(scope_stack),
1026
+ }))
1027
+
1028
+ def visit_ImportFrom(self, node):
1029
+ module = node.module or ""
1030
+ for alias in node.names:
1031
+ name = alias.asname or alias.name
1032
+ syms.append(sym({
1033
+ "name": name, "kind": "import", "line": node.lineno,
1034
+ "col": node.col_offset, "signature": f"from {module} import {alias.name}",
1035
+ "scope": ".".join(scope_stack),
1036
+ }))
1037
+
1038
+ Visitor().visit(tree)
1039
+
1040
+ for node in ast.walk(tree):
1041
+ if isinstance(node, ast.Call):
1042
+ cname = leaf_name(node.func)
1043
+ if cname:
1044
+ refs.append({"toName": cname, "callType": "call", "line": node.lineno})
1045
+ elif isinstance(node, ast.Import):
1046
+ for alias in node.names:
1047
+ refs.append({
1048
+ "toName": alias.name.split(".")[-1], "callType": "import",
1049
+ "line": node.lineno, "module": alias.name,
1050
+ })
1051
+ elif isinstance(node, ast.ImportFrom):
1052
+ module = ("." * (node.level or 0)) + (node.module or "")
1053
+ for alias in node.names:
1054
+ refs.append({
1055
+ "toName": alias.name, "callType": "import",
1056
+ "line": node.lineno, "module": module,
1057
+ })
1058
+ elif isinstance(node, ast.ClassDef):
1059
+ for base in node.bases:
1060
+ bname = leaf_name(base)
1061
+ if bname:
1062
+ refs.append({"toName": bname, "callType": "inherit", "line": node.lineno})
1063
+
1064
+ result["symbols"] = syms
1065
+ result["refs"] = refs
1066
+ return result
1067
+
1068
+ def main():
1069
+ try:
1070
+ inputs = json.loads(sys.stdin.read())
1071
+ except Exception:
1072
+ print(json.dumps({"version": 1, "results": []}))
1073
+ return
1074
+ results = []
1075
+ for entry in inputs:
1076
+ name = entry.get("file", "")
1077
+ module_name = name.rsplit("/", 1)[-1].rsplit("\\\\", 1)[-1][:-3]
1078
+ results.append(parse_one(name, entry.get("content", ""), module_name))
1079
+ print(json.dumps({"version": 1, "results": results}))
1080
+
1081
+ main()
1082
+ `;
1083
+ _goBatchScriptPath = null;
1084
+ _pyBatchScriptPath = null;
999
1085
  }
1000
1086
  });
1001
1087
 
@@ -1005,7 +1091,7 @@ __export(generic_parser_exports, {
1005
1091
  GENERIC_MAX_FILE_CHARS: () => GENERIC_MAX_FILE_CHARS,
1006
1092
  GENERIC_MAX_SYMBOLS_DEFAULT: () => GENERIC_MAX_SYMBOLS_DEFAULT,
1007
1093
  parseGeneric: () => parseGeneric,
1008
- parseSymbols: () => parseSymbols3
1094
+ parseSymbols: () => parseSymbols
1009
1095
  });
1010
1096
  function patternsFor(lang) {
1011
1097
  return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
@@ -1093,7 +1179,7 @@ function parseGeneric(opts) {
1093
1179
  }
1094
1180
  return { file, lang, symbols, mtimeMs };
1095
1181
  }
1096
- async function parseSymbols3(opts) {
1182
+ async function parseSymbols(opts) {
1097
1183
  return parseGeneric(opts);
1098
1184
  }
1099
1185
  var C_LIKE, LANG_PATTERNS, KEYWORDS, GENERIC_MAX_SYMBOLS_DEFAULT, GENERIC_MAX_FILE_CHARS;
@@ -1326,13 +1412,14 @@ var init_generic_parser = __esm({
1326
1412
  var py_parser_exports = {};
1327
1413
  __export(py_parser_exports, {
1328
1414
  detectLang: () => detectLang,
1329
- parseSymbols: () => parseSymbols4
1415
+ parseSymbols: () => parseSymbols2,
1416
+ resolvePythonBinary: () => resolvePythonBinary
1330
1417
  });
1331
1418
  import { spawn as spawn2 } from "node:child_process";
1332
1419
  import * as fs5 from "node:fs/promises";
1333
1420
  import * as os2 from "node:os";
1334
1421
  import * as path9 from "node:path";
1335
- async function parseSymbols4(opts) {
1422
+ async function parseSymbols2(opts) {
1336
1423
  const { file, content, lang } = opts;
1337
1424
  try {
1338
1425
  const native = await withSpawnGate(() => syncPyParse(file, content, lang));
@@ -1406,6 +1493,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1406
1493
  });
1407
1494
  });
1408
1495
  }
1496
+ function resolvePythonBinary() {
1497
+ cachedPyBinary ??= resolvePython();
1498
+ return cachedPyBinary;
1499
+ }
1409
1500
  async function syncPyParse(filePath, content, lang) {
1410
1501
  try {
1411
1502
  if (!_cachedScriptPath) {
@@ -1696,9 +1787,713 @@ for node in ast.walk(tree):
1696
1787
  if name:
1697
1788
  refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
1698
1789
 
1699
- print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1790
+ print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
1791
+ `;
1792
+ _cachedScriptPath = null;
1793
+ }
1794
+ });
1795
+
1796
+ // src/codebase-index/ts-parser.ts
1797
+ var ts_parser_exports = {};
1798
+ __export(ts_parser_exports, {
1799
+ detectLang: () => detectLang,
1800
+ parseSymbols: () => parseSymbols3
1801
+ });
1802
+ function loadTypescript2() {
1803
+ tsLoad2 ??= import("@typescript/typescript6").then((m) => {
1804
+ ts = m.default ?? m;
1805
+ return ts;
1806
+ });
1807
+ return tsLoad2;
1808
+ }
1809
+ function kindMap() {
1810
+ kindMapCache ??= {
1811
+ [ts.SyntaxKind.ClassDeclaration]: "class",
1812
+ [ts.SyntaxKind.InterfaceDeclaration]: "interface",
1813
+ [ts.SyntaxKind.EnumDeclaration]: "enum",
1814
+ [ts.SyntaxKind.TypeAliasDeclaration]: "type",
1815
+ [ts.SyntaxKind.FunctionDeclaration]: "function",
1816
+ [ts.SyntaxKind.MethodDeclaration]: "method",
1817
+ [ts.SyntaxKind.GetAccessor]: "property",
1818
+ [ts.SyntaxKind.SetAccessor]: "property",
1819
+ [ts.SyntaxKind.PropertyDeclaration]: "property",
1820
+ [ts.SyntaxKind.Parameter]: "parameter",
1821
+ [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
1822
+ };
1823
+ return kindMapCache;
1824
+ }
1825
+ function kindOf(node) {
1826
+ if (ts.isVariableDeclaration(node)) {
1827
+ const parent = node.parent;
1828
+ if (ts.isVariableDeclarationList(parent)) {
1829
+ const flags = parent.flags;
1830
+ if (flags & ts.NodeFlags.Let) return "let";
1831
+ if (flags & ts.NodeFlags.Const) return "const";
1832
+ return "var";
1833
+ }
1834
+ }
1835
+ if (ts.isModuleDeclaration(node)) return "namespace";
1836
+ return kindMap()[node.kind] ?? null;
1837
+ }
1838
+ function getSignature(printer, node, sourceFile) {
1839
+ const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1840
+ return raw.replace(/\s+/g, " ").slice(0, 500);
1841
+ }
1842
+ function getJsDoc(node, sourceFile) {
1843
+ const fullText = sourceFile.getFullText();
1844
+ const nodePos = node.getFullStart();
1845
+ const comments = ts.getLeadingCommentRanges(fullText, nodePos);
1846
+ if (!comments) return "";
1847
+ for (const range of comments) {
1848
+ const commentText = fullText.slice(range.pos, range.end);
1849
+ const trimmed = commentText.trim();
1850
+ if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
1851
+ const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
1852
+ return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
1853
+ }
1854
+ }
1855
+ return "";
1856
+ }
1857
+ function pushScopeName(node, parts) {
1858
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
1859
+ parts.push(node.name?.text ?? "Anon");
1860
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
1861
+ if (node.name && ts.isIdentifier(node.name)) {
1862
+ parts.push(node.name.text);
1863
+ }
1864
+ }
1865
+ }
1866
+ async function parseSymbols3(opts) {
1867
+ const { file, content, lang } = opts;
1868
+ await loadTypescript2();
1869
+ let sourceFile;
1870
+ try {
1871
+ sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
1872
+ } catch {
1873
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1874
+ }
1875
+ const symbols = [];
1876
+ const refs = [];
1877
+ const printer = ts.createPrinter({});
1878
+ function visit(node, funcDepth, scopeParts) {
1879
+ const kind = kindOf(node);
1880
+ if (kind) {
1881
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
1882
+ } else {
1883
+ const nameNode = node.name;
1884
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
1885
+ return;
1886
+ }
1887
+ const name = nameNode.text;
1888
+ const pos2 = nameNode.getStart(sourceFile);
1889
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
1890
+ const scope = scopeParts.join(".");
1891
+ const signature = getSignature(printer, node, sourceFile);
1892
+ const docComment = getJsDoc(node, sourceFile);
1893
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
1894
+ symbols.push({
1895
+ id: 0,
1896
+ lang,
1897
+ kind,
1898
+ name,
1899
+ file,
1900
+ line: line2 + 1,
1901
+ col: character,
1902
+ signature,
1903
+ docComment,
1904
+ scope,
1905
+ text
1906
+ });
1907
+ }
1908
+ }
1909
+ const pos = node.getStart(sourceFile);
1910
+ const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1911
+ const lineNum = line + 1;
1912
+ if (ts.isCallExpression(node)) {
1913
+ const expr = node.expression;
1914
+ if (ts.isIdentifier(expr)) {
1915
+ refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
1916
+ }
1917
+ } else if (ts.isPropertyAccessExpression(node)) {
1918
+ if (ts.isIdentifier(node.expression)) {
1919
+ refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
1920
+ }
1921
+ } else if (ts.isTypeReferenceNode(node)) {
1922
+ const name = getTypeName(node.typeName);
1923
+ if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
1924
+ } else if (ts.isHeritageClause(node)) {
1925
+ for (const t of node.types) {
1926
+ const name = getTypeName(t.expression);
1927
+ if (name)
1928
+ refs.push({
1929
+ fromId: 0,
1930
+ toName: name,
1931
+ callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement",
1932
+ line: lineNum
1933
+ });
1934
+ }
1935
+ } else if (ts.isImportDeclaration(node)) {
1936
+ emitImportSpecifierRefs(node, refs, lineNum);
1937
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
1938
+ emitExportSpecifierRefs(node, refs, lineNum);
1939
+ }
1940
+ const scopeIdx = scopeParts.length;
1941
+ pushScopeName(node, scopeParts);
1942
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
1943
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
1944
+ scopeParts.length = scopeIdx;
1945
+ }
1946
+ visit(sourceFile, 0, []);
1947
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1948
+ }
1949
+ function getTypeName(name) {
1950
+ if (ts.isIdentifier(name)) return name.text;
1951
+ if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
1952
+ return "";
1953
+ }
1954
+ function deduplicateRefs(refs) {
1955
+ const seen = /* @__PURE__ */ new Set();
1956
+ return refs.filter((r) => {
1957
+ const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
1958
+ if (seen.has(key)) return false;
1959
+ seen.add(key);
1960
+ return true;
1961
+ });
1962
+ }
1963
+ function getImportSpecifierName(spec) {
1964
+ return spec.propertyName?.text ?? spec.name.text;
1965
+ }
1966
+ function emitImportSpecifierRefs(node, refs, lineNum) {
1967
+ const module = moduleSpecifierOf(node.moduleSpecifier);
1968
+ const clause = node.importClause;
1969
+ if (!clause) {
1970
+ if (module) {
1971
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
1972
+ }
1973
+ return;
1974
+ }
1975
+ if (clause.name) {
1976
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
1977
+ }
1978
+ const bindings = clause.namedBindings;
1979
+ if (!bindings) return;
1980
+ if (ts.isNamedImports(bindings)) {
1981
+ for (const element of bindings.elements) {
1982
+ refs.push({
1983
+ fromId: 0,
1984
+ toName: getImportSpecifierName(element),
1985
+ callType: "import",
1986
+ line: lineNum,
1987
+ module
1988
+ });
1989
+ }
1990
+ } else if (ts.isNamespaceImport(bindings)) {
1991
+ refs.push({
1992
+ fromId: 0,
1993
+ toName: bindings.name.text,
1994
+ callType: "import",
1995
+ line: lineNum,
1996
+ module
1997
+ });
1998
+ }
1999
+ }
2000
+ function moduleSpecifierOf(node) {
2001
+ return node && ts.isStringLiteral(node) ? node.text : void 0;
2002
+ }
2003
+ function emitExportSpecifierRefs(node, refs, lineNum) {
2004
+ const module = moduleSpecifierOf(node.moduleSpecifier);
2005
+ const clause = node.exportClause;
2006
+ if (clause && ts.isNamespaceExport(clause)) {
2007
+ refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
2008
+ return;
2009
+ }
2010
+ if (clause && ts.isNamedExports(clause)) {
2011
+ for (const element of clause.elements) {
2012
+ const originalName = element.propertyName?.text ?? element.name.text;
2013
+ refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
2014
+ }
2015
+ return;
2016
+ }
2017
+ if (module) {
2018
+ refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
2019
+ }
2020
+ }
2021
+ var ts, tsLoad2, kindMapCache;
2022
+ var init_ts_parser = __esm({
2023
+ "src/codebase-index/ts-parser.ts"() {
2024
+ "use strict";
2025
+ init_languages();
2026
+ tsLoad2 = null;
2027
+ kindMapCache = null;
2028
+ }
2029
+ });
2030
+
2031
+ // src/codebase-index/go-parser.ts
2032
+ var go_parser_exports = {};
2033
+ __export(go_parser_exports, {
2034
+ detectLang: () => detectLang,
2035
+ parseSymbols: () => parseSymbols4
2036
+ });
2037
+ import { spawn as spawn3 } from "node:child_process";
2038
+ import * as os3 from "node:os";
2039
+ import * as path10 from "node:path";
2040
+ import * as fs6 from "node:fs/promises";
2041
+ async function parseSymbols4(opts) {
2042
+ const { file, content, lang } = opts;
2043
+ try {
2044
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
2045
+ if (parsed.symbols.length > 0) {
2046
+ return parsed;
2047
+ }
2048
+ const fallback = fallbackParse(file, content, lang);
2049
+ return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
2050
+ } catch {
2051
+ return fallbackParse(file, content, lang);
2052
+ }
2053
+ }
2054
+ function fallbackParse(filePath, content, lang) {
2055
+ if (!/^\s*package\s+[A-Za-z_]\w*/m.test(content) || hasUnbalancedDelimiters(content)) {
2056
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2057
+ }
2058
+ const symbols = [];
2059
+ const packageName = content.match(/^\s*package\s+([A-Za-z_]\w*)/m)?.[1] ?? "";
2060
+ const lines = content.split(/\r?\n/);
2061
+ for (const [idx, line] of lines.entries()) {
2062
+ const trimmed = line.trimStart();
2063
+ const col = line.length - trimmed.length + 1;
2064
+ const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
2065
+ if (fn?.[1]) {
2066
+ addFallbackSymbol(symbols, {
2067
+ filePath,
2068
+ lang,
2069
+ kind: trimmed.startsWith("func (") ? "method" : "function",
2070
+ name: fn[1],
2071
+ line: idx + 1,
2072
+ col,
2073
+ signature: trimmed,
2074
+ scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
2075
+ });
2076
+ continue;
2077
+ }
2078
+ const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2079
+ if (typeDecl?.[1]) {
2080
+ addFallbackSymbol(symbols, {
2081
+ filePath,
2082
+ lang,
2083
+ kind: "type",
2084
+ name: typeDecl[1],
2085
+ line: idx + 1,
2086
+ col,
2087
+ signature: trimmed,
2088
+ scope: packageName
2089
+ });
2090
+ continue;
2091
+ }
2092
+ const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
2093
+ if (valueDecl?.[1] && valueDecl[2]) {
2094
+ addFallbackSymbol(symbols, {
2095
+ filePath,
2096
+ lang,
2097
+ kind: valueDecl[1],
2098
+ name: valueDecl[2],
2099
+ line: idx + 1,
2100
+ col,
2101
+ signature: trimmed,
2102
+ scope: packageName
2103
+ });
2104
+ }
2105
+ }
2106
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2107
+ }
2108
+ function addFallbackSymbol(symbols, opts) {
2109
+ symbols.push({
2110
+ id: 0,
2111
+ lang: opts.lang,
2112
+ kind: opts.kind,
2113
+ name: opts.name,
2114
+ file: opts.filePath,
2115
+ line: opts.line,
2116
+ col: opts.col,
2117
+ signature: opts.signature,
2118
+ docComment: "",
2119
+ scope: opts.scope,
2120
+ text: `${opts.name} ${opts.signature}`.trim()
2121
+ });
2122
+ }
2123
+ function hasUnbalancedDelimiters(content) {
2124
+ const pairs = { "(": ")", "[": "]", "{": "}" };
2125
+ const closers = new Set(Object.values(pairs));
2126
+ const stack = [];
2127
+ for (const ch of content) {
2128
+ if (pairs[ch]) {
2129
+ stack.push(pairs[ch]);
2130
+ } else if (closers.has(ch) && stack.pop() !== ch) {
2131
+ return true;
2132
+ }
2133
+ }
2134
+ return stack.length > 0;
2135
+ }
2136
+ async function syncGoParse(filePath, content, lang) {
2137
+ try {
2138
+ let scriptPath = _cachedGoScriptPath;
2139
+ if (!scriptPath) {
2140
+ const tmpDir = await fs6.mkdtemp(path10.join(os3.tmpdir(), "ws-go-parse-"));
2141
+ scriptPath = path10.join(tmpDir, "parse.go");
2142
+ await fs6.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2143
+ _cachedGoScriptPath = scriptPath;
2144
+ }
2145
+ const goBinary = resolveWin32Command("go");
2146
+ const goResult = await new Promise(
2147
+ (resolve4, reject) => {
2148
+ let settled = false;
2149
+ const proc = spawn3(goBinary, ["run", scriptPath], {
2150
+ stdio: ["pipe", "pipe", "pipe"],
2151
+ windowsHide: true
2152
+ });
2153
+ proc.on("error", (err) => {
2154
+ if (settled) return;
2155
+ settled = true;
2156
+ reject(err);
2157
+ });
2158
+ let stdout2 = "";
2159
+ proc.stdout?.on("data", (chunk) => {
2160
+ stdout2 += chunk.toString();
2161
+ });
2162
+ proc.stderr?.resume();
2163
+ proc.stdin?.write(content);
2164
+ proc.stdin?.end();
2165
+ const timer = setTimeout(() => {
2166
+ if (settled) return;
2167
+ settled = true;
2168
+ proc.kill("SIGKILL");
2169
+ reject(new Error("timeout"));
2170
+ }, 15e3);
2171
+ timer.unref?.();
2172
+ proc.on("close", (code2) => {
2173
+ if (settled) return;
2174
+ settled = true;
2175
+ clearTimeout(timer);
2176
+ resolve4({ code: code2, stdout: stdout2 });
2177
+ });
2178
+ }
2179
+ );
2180
+ const { code, stdout } = goResult;
2181
+ if (code !== 0 || !stdout.trim()) {
2182
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2183
+ }
2184
+ const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
2185
+ const symbols = rawSymbols.map((s) => ({
2186
+ id: 0,
2187
+ lang,
2188
+ kind: s.kind,
2189
+ name: s.name,
2190
+ file: filePath,
2191
+ line: s.line,
2192
+ col: s.col,
2193
+ signature: s.signature ?? "",
2194
+ docComment: "",
2195
+ scope: s.scope ?? "",
2196
+ text: `${s.name} ${s.signature ?? ""}`.trim()
2197
+ }));
2198
+ return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
2199
+ } catch {
2200
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2201
+ }
2202
+ }
2203
+ var GO_PARSE_SCRIPT, _cachedGoScriptPath;
2204
+ var init_go_parser = __esm({
2205
+ "src/codebase-index/go-parser.ts"() {
2206
+ "use strict";
2207
+ init_win32_resolve();
2208
+ init_parser_output();
2209
+ init_spawn_gate();
2210
+ init_languages();
2211
+ GO_PARSE_SCRIPT = `
2212
+ package main
2213
+
2214
+ import (
2215
+ "encoding/json"
2216
+ "fmt"
2217
+ "go/ast"
2218
+ "go/parser"
2219
+ "go/token"
2220
+ "io"
2221
+ "os"
2222
+ "strconv"
2223
+ "strings"
2224
+ )
2225
+
2226
+ type Sym struct {
2227
+ Name string \`json:"name"\`
2228
+ Kind string \`json:"kind"\`
2229
+ Line int \`json:"line"\`
2230
+ Col int \`json:"col"\`
2231
+ Signature string \`json:"signature"\`
2232
+ Scope string \`json:"scope"\`
2233
+ }
2234
+
2235
+ // Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
2236
+ // yields both. Module is the import path for CallType "import", else empty.
2237
+ type Ref struct {
2238
+ ToName string \`json:"toName"\`
2239
+ CallType string \`json:"callType"\`
2240
+ Line int \`json:"line"\`
2241
+ Module string \`json:"module"\`
2242
+ }
2243
+
2244
+ type Result struct {
2245
+ Symbols []Sym \`json:"symbols"\`
2246
+ Refs []Ref \`json:"refs"\`
2247
+ }
2248
+
2249
+ func emptyResult() string {
2250
+ return "{\\"symbols\\":[],\\"refs\\":[]}"
2251
+ }
2252
+
2253
+ func main() {
2254
+ src, err := io.ReadAll(os.Stdin)
2255
+ if err != nil {
2256
+ fmt.Print(emptyResult())
2257
+ return
2258
+ }
2259
+ fset := token.NewFileSet()
2260
+ node, err := parser.ParseFile(fset, "src.go", src, 0)
2261
+ if err != nil {
2262
+ fmt.Print(emptyResult())
2263
+ return
2264
+ }
2265
+
2266
+ var syms []Sym
2267
+
2268
+ // Package-level scope
2269
+ pkgScope := node.Name.Name
2270
+
2271
+ // Collect all top-level declarations
2272
+ for _, decl := range node.Decls {
2273
+ switch d := decl.(type) {
2274
+ case *ast.FuncDecl:
2275
+ name := d.Name.Name
2276
+ kind := "function"
2277
+ scope := pkgScope
2278
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2279
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
2280
+ kind = "method"
2281
+ } else {
2282
+ scope = pkgScope + "." + name
2283
+ }
2284
+ pos := fset.Position(d.Pos())
2285
+ sig := formatFuncSig(d)
2286
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
2287
+
2288
+ case *ast.GenDecl:
2289
+ for _, spec := range d.Specs {
2290
+ switch s := spec.(type) {
2291
+ case *ast.TypeSpec:
2292
+ name := s.Name.Name
2293
+ pos := fset.Position(s.Pos())
2294
+ sig := "type " + name
2295
+ if s.TypeParams != nil {
2296
+ sig += formatTypeParams(s.TypeParams)
2297
+ }
2298
+ if st, ok := s.Type.(*ast.StructType); ok {
2299
+ sig += " = struct { " + formatFields(st.Fields.List) + " }"
2300
+ } else if it, ok := s.Type.(*ast.InterfaceType); ok {
2301
+ sig += " = interface { " + formatMethods(it.Methods.List) + " }"
2302
+ } else {
2303
+ sig += " = " + formatType(s.Type)
2304
+ }
2305
+ syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2306
+
2307
+ case *ast.ValueSpec:
2308
+ for _, n := range s.Names {
2309
+ name := n.Name
2310
+ pos := fset.Position(n.Pos())
2311
+ kind := "var"
2312
+ if d.Tok == token.CONST {
2313
+ kind = "const"
2314
+ }
2315
+ sig := kind + " " + name
2316
+ if s.Type != nil {
2317
+ sig += " " + formatType(s.Type)
2318
+ }
2319
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
2320
+ }
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2325
+
2326
+ refs := []Ref{}
2327
+ ast.Inspect(node, func(n ast.Node) bool {
2328
+ switch expr := n.(type) {
2329
+ case *ast.CallExpr:
2330
+ line := fset.Position(expr.Pos()).Line
2331
+ switch fun := expr.Fun.(type) {
2332
+ case *ast.Ident:
2333
+ refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
2334
+ case *ast.SelectorExpr:
2335
+ // Record the selected name (\`Join\` of \`filepath.Join\`): it is the
2336
+ // declared symbol name, so it resolves the same way the TypeScript
2337
+ // and Python extractors' call refs do.
2338
+ refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
2339
+ }
2340
+ case *ast.ImportSpec:
2341
+ if expr.Path != nil {
2342
+ if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
2343
+ line := fset.Position(expr.Pos()).Line
2344
+ // A Go import names a package, not a symbol; the package's
2345
+ // last path segment is the name it is referenced by.
2346
+ name := importPath
2347
+ if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
2348
+ name = importPath[idx+1:]
2349
+ }
2350
+ refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
2351
+ }
2352
+ }
2353
+ }
2354
+ return true
2355
+ })
2356
+
2357
+ if syms == nil {
2358
+ syms = []Sym{}
2359
+ }
2360
+ data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
2361
+ if err != nil {
2362
+ fmt.Print(emptyResult())
2363
+ return
2364
+ }
2365
+ fmt.Print(string(data))
2366
+ }
2367
+
2368
+ func recvTypeName(t ast.Expr) string {
2369
+ switch v := t.(type) {
2370
+ case *ast.Ident:
2371
+ return v.Name
2372
+ case *ast.StarExpr:
2373
+ return recvTypeName(v.X)
2374
+ default:
2375
+ return "?"
2376
+ }
2377
+ }
2378
+
2379
+ func formatFuncSig(d *ast.FuncDecl) string {
2380
+ scope := ""
2381
+ if d.Recv != nil && len(d.Recv.List) > 0 {
2382
+ scope = "(" + formatFieldList(d.Recv.List) + ") "
2383
+ }
2384
+ scope += formatFuncType(d.Type)
2385
+ return "func " + scope
2386
+ }
2387
+
2388
+ func formatFuncType(f *ast.FuncType) string {
2389
+ params := formatFieldList(f.Params.List)
2390
+ results := ""
2391
+ if f.Results != nil {
2392
+ results = " -> " + formatFieldList(f.Results.List)
2393
+ }
2394
+ return params + results
2395
+ }
2396
+
2397
+ func formatFieldList(fields []*ast.Field) string {
2398
+ if len(fields) == 0 {
2399
+ return "()"
2400
+ }
2401
+ names := make([]string, 0, len(fields))
2402
+ for _, f := range fields {
2403
+ name := ""
2404
+ if len(f.Names) > 0 {
2405
+ name = f.Names[0].Name
2406
+ }
2407
+ t := formatType(f.Type)
2408
+ if name != "" {
2409
+ names = append(names, name+" "+t)
2410
+ } else {
2411
+ names = append(names, t)
2412
+ }
2413
+ }
2414
+ return "(" + strings.Join(names, ", ") + ")"
2415
+ }
2416
+
2417
+ func formatFields(fields []*ast.Field) string {
2418
+ lines := make([]string, 0)
2419
+ for _, f := range fields {
2420
+ name := ""
2421
+ if len(f.Names) > 0 {
2422
+ name = f.Names[0].Name
2423
+ }
2424
+ t := formatType(f.Type)
2425
+ if name != "" {
2426
+ lines = append(lines, name+" "+t)
2427
+ } else {
2428
+ lines = append(lines, t)
2429
+ }
2430
+ }
2431
+ return strings.Join(lines, "; ")
2432
+ }
2433
+
2434
+ func formatMethods(fields []*ast.Field) string {
2435
+ return formatFields(fields)
2436
+ }
2437
+
2438
+ func formatTypeParams(tp *ast.FieldList) string {
2439
+ if tp == nil || len(tp.List) == 0 {
2440
+ return ""
2441
+ }
2442
+ params := make([]string, len(tp.List))
2443
+ for i, p := range tp.List {
2444
+ if len(p.Names) > 0 {
2445
+ params[i] = p.Names[0].Name
2446
+ } else {
2447
+ params[i] = "T"
2448
+ }
2449
+ }
2450
+ return "[" + strings.Join(params, ", ") + "]"
2451
+ }
2452
+
2453
+ func formatType(t ast.Expr) string {
2454
+ if t == nil {
2455
+ return "?"
2456
+ }
2457
+ switch v := t.(type) {
2458
+ case *ast.Ident:
2459
+ return v.Name
2460
+ case *ast.SelectorExpr:
2461
+ return formatType(v.X) + "." + v.Sel.Name
2462
+ case *ast.StarExpr:
2463
+ return "*" + formatType(v.X)
2464
+ case *ast.ArrayType:
2465
+ if v.Len == nil {
2466
+ return "[]" + formatType(v.Elt)
2467
+ }
2468
+ return "[...]" + formatType(v.Elt)
2469
+ case *ast.MapType:
2470
+ return "map[" + formatType(v.Key) + "]" + formatType(v.Value)
2471
+ case *ast.InterfaceType:
2472
+ return "interface{}"
2473
+ case *ast.StructType:
2474
+ return "struct{}"
2475
+ case *ast.FuncType:
2476
+ return formatFuncType(v)
2477
+ case *ast.ChanType:
2478
+ return "chan " + formatType(v.Value)
2479
+ case *ast.BasicLit:
2480
+ return v.Value
2481
+ case *ast.IndexExpr:
2482
+ // Generic instantiation with one type arg, e.g. Logger[int].
2483
+ return formatType(v.X) + "[" + formatType(v.Index) + "]"
2484
+ case *ast.IndexListExpr:
2485
+ // Generic instantiation with multiple type args, e.g. Map[K, V].
2486
+ args := make([]string, len(v.Indices))
2487
+ for i, idx := range v.Indices {
2488
+ args[i] = formatType(idx)
2489
+ }
2490
+ return formatType(v.X) + "[" + strings.Join(args, ", ") + "]"
2491
+ default:
2492
+ return "?"
2493
+ }
2494
+ }
1700
2495
  `;
1701
- _cachedScriptPath = null;
2496
+ _cachedGoScriptPath = null;
1702
2497
  }
1703
2498
  });
1704
2499
 
@@ -1794,7 +2589,7 @@ __export(json_parser_exports, {
1794
2589
  parseSymbols: () => parseSymbols6
1795
2590
  });
1796
2591
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1797
- import * as path10 from "node:path";
2592
+ import * as path11 from "node:path";
1798
2593
  function parseSymbols6(opts) {
1799
2594
  const { file, content, lang } = opts;
1800
2595
  try {
@@ -1806,7 +2601,7 @@ function parseSymbols6(opts) {
1806
2601
  function regexParse2(opts) {
1807
2602
  const { file, content, lang } = opts;
1808
2603
  const symbols = [];
1809
- const basename7 = path10.basename(file).toLowerCase();
2604
+ const basename7 = path11.basename(file).toLowerCase();
1810
2605
  const isPackageJson = basename7 === "package.json";
1811
2606
  const isTsconfig = basename7 === "tsconfig.json" || basename7 === "tsconfig.build.json";
1812
2607
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1832,11 +2627,11 @@ function regexParse2(opts) {
1832
2627
  const line = lineFromOffset(offset);
1833
2628
  symbols.push(
1834
2629
  makeSymbol({
1835
- name: path10.basename(file),
2630
+ name: path11.basename(file),
1836
2631
  kind: "object",
1837
2632
  line,
1838
2633
  col: 0,
1839
- signature: `"${path10.basename(file)}" = { ... }`,
2634
+ signature: `"${path11.basename(file)}" = { ... }`,
1840
2635
  file,
1841
2636
  lang
1842
2637
  })
@@ -2164,6 +2959,106 @@ var init_yaml_parser = __esm({
2164
2959
  });
2165
2960
 
2166
2961
  // src/codebase-index/tree-sitter/queries.ts
2962
+ function parseGroupedUse(text) {
2963
+ const open = text.indexOf("{");
2964
+ const close = text.lastIndexOf("}");
2965
+ if (open < 0 || close <= open) return null;
2966
+ const prefix = text.slice(0, open).replace(/[\\/]+$/, "");
2967
+ const out = [];
2968
+ for (const rawMember of text.slice(open + 1, close).split(",")) {
2969
+ let member = rawMember.trim();
2970
+ if (!member) continue;
2971
+ member = member.replace(
2972
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2973
+ ""
2974
+ ).trim();
2975
+ if (!member) continue;
2976
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(member);
2977
+ if (aliasMatch) member = member.slice(0, aliasMatch.index).trim();
2978
+ if (!member) continue;
2979
+ const module = prefix ? `${prefix}\\${member}` : member;
2980
+ const toName = member.split(/[\\/]/).filter(Boolean).pop();
2981
+ if (toName) out.push({ toName, callType: "import", module });
2982
+ }
2983
+ return out.length ? out : null;
2984
+ }
2985
+ function importFromText(prefixes) {
2986
+ return (node) => {
2987
+ let text = node.text.replace(/\s+/g, " ").trim();
2988
+ for (const prefix of prefixes) {
2989
+ if (text.startsWith(prefix)) text = text.slice(prefix.length).trim();
2990
+ }
2991
+ text = text.replace(
2992
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
2993
+ ""
2994
+ ).trim();
2995
+ if (text.includes("{")) return parseGroupedUse(text);
2996
+ if (text.includes(",") && !text.includes("<") && !text.includes("=")) {
2997
+ const out = [];
2998
+ for (const clause of text.split(",")) {
2999
+ const one = oneImportClause(clause.trim());
3000
+ if (one) out.push(one);
3001
+ }
3002
+ return out.length ? out : null;
3003
+ }
3004
+ const single = oneImportClause(text);
3005
+ return single ? [single] : null;
3006
+ };
3007
+ }
3008
+ function oneImportClause(rawClause) {
3009
+ let text = rawClause;
3010
+ text = text.replace(
3011
+ /^(static|final|type|class|struct|enum|protocol|var|func|let|typealias|function|const)\s+/i,
3012
+ ""
3013
+ ).trim();
3014
+ text = text.replace(/[;}]+$/g, "").trim();
3015
+ const aliasMatch = /\s+as\s+([A-Za-z_]\w*)\s*$/i.exec(text);
3016
+ if (aliasMatch) text = text.slice(0, aliasMatch.index).trim();
3017
+ const eqMatch = /^([A-Za-z_]\w*)\s*=\s*(.+)$/.exec(text);
3018
+ if (eqMatch) text = eqMatch[2].trim();
3019
+ if (!text) return null;
3020
+ if (text.endsWith("*")) text = text.slice(0, -1).replace(/[.]$/, "");
3021
+ if (!text) return null;
3022
+ const module = text;
3023
+ const toName = module.split(/[.\\/]/).filter(Boolean).pop()?.replace(/<.*>$/s, "");
3024
+ if (!toName) return null;
3025
+ return { toName, callType: "import", module };
3026
+ }
3027
+ function heritageLeaf(node, depth) {
3028
+ if (depth > 6) return null;
3029
+ const named = node.childForFieldName("name");
3030
+ if (named) {
3031
+ if (named.type === "scoped_type_identifier" || named.type === "qualified_name" || named.type === "scope_resolution" || named.type === "user_type") {
3032
+ return heritageLeaf(named, depth + 1);
3033
+ }
3034
+ return named.text;
3035
+ }
3036
+ const children = [];
3037
+ for (let i = 0; i < node.namedChildCount; i++) {
3038
+ const c = node.namedChild(i);
3039
+ if (c) children.push(c);
3040
+ }
3041
+ for (let i = children.length - 1; i >= 0; i--) {
3042
+ const c = children[i];
3043
+ if (c.type === "type_arguments" || c.type === "type_argument_list" || // cpp: (template_type arguments: (template_argument_list …)) — the
3044
+ // descriptor's type_identifier inside it is never the declared base.
3045
+ c.type === "template_argument_list" || c.type === "type_parameter_list" || c.type === "type_projection" || c.type === "value_arguments") {
3046
+ continue;
3047
+ }
3048
+ if (c.type === "type_identifier" || c.type === "identifier" || c.type === "constant" || c.type === "name") {
3049
+ return c.text;
3050
+ }
3051
+ if (c.type === "scoped_type_identifier" || c.type === "qualified_name" || c.type === "scope_resolution" || c.type === "user_type") {
3052
+ return heritageLeaf(c, depth + 1);
3053
+ }
3054
+ }
3055
+ return leafSegment(
3056
+ node.text.replace(/\\/g, ".").replace(/::/g, ".").replace(/<[^<>]*>$/, "")
3057
+ );
3058
+ }
3059
+ function leafSegment(text) {
3060
+ return text.split(".").filter(Boolean).pop() ?? text;
3061
+ }
2167
3062
  function getQueries(lang) {
2168
3063
  return LANG_QUERIES[lang] ?? DEFAULT_QUERIES;
2169
3064
  }
@@ -2175,10 +3070,127 @@ function readFirstString(node) {
2175
3070
  const child = node.namedChild(0);
2176
3071
  return child ? readFirstString(child) : null;
2177
3072
  }
2178
- var DEFAULT_QUERIES, LANG_QUERIES;
3073
+ var heritageExtractor, cCallExtractor, rubyCallExtractor, firstIdentifierCallExtractor, cIncludeExtractor, phpConstructorExtractor, DEFAULT_QUERIES, LANG_QUERIES;
2179
3074
  var init_queries = __esm({
2180
3075
  "src/codebase-index/tree-sitter/queries.ts"() {
2181
3076
  "use strict";
3077
+ heritageExtractor = (node) => {
3078
+ const out = [];
3079
+ const SKIP_SUBTREES = /* @__PURE__ */ new Set([
3080
+ "type_arguments",
3081
+ "type_argument_list",
3082
+ // tree-sitter-cpp names its argument subtree template_argument_list —
3083
+ // verified AST: (base_class_clause (template_type name:
3084
+ // (type_identifier) arguments: (template_argument_list
3085
+ // (type_descriptor type: (type_identifier))))). Without this entry
3086
+ // `class D : Base<Foo>` recurses into the descriptor and emits Foo as a
3087
+ // phantom inherit ref.
3088
+ "template_argument_list",
3089
+ "type_parameter_list",
3090
+ "type_projection",
3091
+ "value_arguments"
3092
+ ]);
3093
+ const collect = (current, depth) => {
3094
+ if (depth > 4) return;
3095
+ for (let i = 0; i < current.namedChildCount; i++) {
3096
+ const child = current.namedChild(i);
3097
+ if (!child) continue;
3098
+ if (SKIP_SUBTREES.has(child.type)) continue;
3099
+ if (child.type === "type_identifier" || child.type === "identifier" || child.type === "named_type" || child.type === "type" || // PHP heritage carries `name`; Ruby a `constant`.
3100
+ child.type === "constant" || child.type === "name") {
3101
+ const name = child.type === "named_type" ? leafSegment(child.text) : child.text;
3102
+ if (name) out.push({ toName: name });
3103
+ continue;
3104
+ }
3105
+ if (child.type === "generic_type" || child.type === "generic_name") {
3106
+ for (let j = 0; j < child.namedChildCount; j++) {
3107
+ const inner = child.namedChild(j);
3108
+ if (inner && !SKIP_SUBTREES.has(inner.type) && (inner.type === "type_identifier" || inner.type === "identifier" || inner.type === "name")) {
3109
+ out.push({ toName: inner.text });
3110
+ break;
3111
+ }
3112
+ }
3113
+ continue;
3114
+ }
3115
+ if (child.type === "qualified_name" || child.type === "scoped_type_identifier" || child.type === "user_type" || child.type === "scope_resolution") {
3116
+ const leaf = heritageLeaf(child, 0);
3117
+ if (leaf) out.push({ toName: leaf });
3118
+ continue;
3119
+ }
3120
+ collect(child, depth + 1);
3121
+ }
3122
+ };
3123
+ collect(node, 0);
3124
+ return out;
3125
+ };
3126
+ cCallExtractor = (node) => {
3127
+ const fn = node.childForFieldName("function");
3128
+ if (!fn) return null;
3129
+ if (fn.type === "field_expression") {
3130
+ const field = fn.childForFieldName("field");
3131
+ if (field) return [{ toName: field.text, callType: "call" }];
3132
+ const seg = fn.text.split("->").filter(Boolean).pop();
3133
+ if (seg) return [{ toName: leafSegment(seg.split(".")[0] ?? seg), callType: "call" }];
3134
+ return null;
3135
+ }
3136
+ if (fn.type === "qualified_identifier") {
3137
+ const name = fn.childForFieldName("name");
3138
+ if (name) return [{ toName: name.text, callType: "call" }];
3139
+ const seg = fn.text.split("::").filter(Boolean).pop();
3140
+ if (seg) return [{ toName: seg.split(/[<(]/)[0].trim(), callType: "call" }];
3141
+ return null;
3142
+ }
3143
+ return [{ toName: fn.text.split(/[<(]/)[0].trim(), callType: "call" }];
3144
+ };
3145
+ rubyCallExtractor = (node) => {
3146
+ const emissions = [];
3147
+ const method = node.childForFieldName("method");
3148
+ if (method) {
3149
+ const name = method.text;
3150
+ if (name && !name.includes(" ")) emissions.push({ toName: name, callType: "call" });
3151
+ if (name === "require" || name === "require_relative") {
3152
+ const args = node.childForFieldName("arguments");
3153
+ const first = args?.namedChild(0);
3154
+ if (first) {
3155
+ const raw = first.text.replace(/^['"]|['"]$/g, "");
3156
+ const toName = raw.split("/").filter(Boolean).pop();
3157
+ if (toName) emissions.push({ toName, callType: "import", module: raw });
3158
+ }
3159
+ }
3160
+ }
3161
+ return emissions;
3162
+ };
3163
+ firstIdentifierCallExtractor = (node) => {
3164
+ for (let i = 0; i < node.namedChildCount; i++) {
3165
+ const child = node.namedChild(i);
3166
+ if (child && (child.type === "simple_identifier" || child.type === "identifier")) {
3167
+ return [{ toName: child.text, callType: "call" }];
3168
+ }
3169
+ }
3170
+ const first = node.namedChild(0);
3171
+ if (!first) return null;
3172
+ const leaf = leafSegment(first.text.split(/[<(]/)[0] ?? first.text);
3173
+ if (!leaf) return null;
3174
+ return [{ toName: leaf, callType: "call" }];
3175
+ };
3176
+ cIncludeExtractor = (node) => {
3177
+ const raw = node.text.replace(/^#\s*include\s*/i, "").trim();
3178
+ const module = raw.replace(/^["'<]|["'>]$/g, "");
3179
+ if (!module) return null;
3180
+ const toName = module.split("/").pop()?.replace(/\.h$/, "");
3181
+ if (!toName) return null;
3182
+ return [{ toName, callType: "import", module }];
3183
+ };
3184
+ phpConstructorExtractor = (node) => {
3185
+ for (let i = 0; i < node.namedChildCount; i++) {
3186
+ const child = node.namedChild(i);
3187
+ if (child && (child.type === "qualified_name" || child.type === "name")) {
3188
+ const leaf = child.text.split(/[\\]/).filter(Boolean).pop();
3189
+ if (leaf) return [{ toName: leaf, callType: "call" }];
3190
+ }
3191
+ }
3192
+ return null;
3193
+ };
2182
3194
  DEFAULT_QUERIES = {
2183
3195
  declKinds: {}
2184
3196
  };
@@ -2211,7 +3223,13 @@ var init_queries = __esm({
2211
3223
  "struct_specifier",
2212
3224
  "union_specifier",
2213
3225
  "enum_specifier"
2214
- ])
3226
+ ]),
3227
+ refRules: {
3228
+ // `obj->run()` and `Cls::stat()` carry structured function fields —
3229
+ // cCallExtractor handles all three AST shapes.
3230
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3231
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor }
3232
+ }
2215
3233
  },
2216
3234
  cpp: {
2217
3235
  declKinds: {
@@ -2242,7 +3260,13 @@ var init_queries = __esm({
2242
3260
  "union_specifier",
2243
3261
  "enum_specifier",
2244
3262
  "namespace_definition"
2245
- ])
3263
+ ]),
3264
+ refRules: {
3265
+ call_expression: { callType: "call", nameExtractor: cCallExtractor },
3266
+ preproc_include: { callType: "import", nameExtractor: cIncludeExtractor },
3267
+ // `class Foo : public Bar, private Baz` — the base-class clause.
3268
+ base_class_clause: { callType: "inherit", nameExtractor: heritageExtractor }
3269
+ }
2246
3270
  },
2247
3271
  java: {
2248
3272
  declKinds: {
@@ -2277,7 +3301,22 @@ var init_queries = __esm({
2277
3301
  "interface_declaration",
2278
3302
  "enum_declaration",
2279
3303
  "record_declaration"
2280
- ])
3304
+ ]),
3305
+ refRules: {
3306
+ method_invocation: { callType: "call", field: "name" },
3307
+ object_creation_expression: { callType: "call", field: "type" },
3308
+ // Verified AST: `superclass: (superclass (type_identifier))` and
3309
+ // `interfaces: (super_interfaces (type_list ...))` — no underscores.
3310
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor },
3311
+ super_interfaces: {
3312
+ callType: "implement",
3313
+ nameExtractor: heritageExtractor
3314
+ },
3315
+ import_declaration: {
3316
+ callType: "import",
3317
+ nameExtractor: importFromText(["import "])
3318
+ }
3319
+ }
2281
3320
  },
2282
3321
  csharp: {
2283
3322
  // C# 10+ `namespace Foo.Bar;` produces this node type. The legacy block
@@ -2315,7 +3354,18 @@ var init_queries = __esm({
2315
3354
  "struct_declaration",
2316
3355
  "enum_declaration",
2317
3356
  "record_declaration"
2318
- ])
3357
+ ]),
3358
+ refRules: {
3359
+ // Verified AST: `invocation_expression function: (identifier)` — the
3360
+ // callee field is `function` (C-style), not `name`.
3361
+ invocation_expression: { callType: "call", field: "function" },
3362
+ object_creation_expression: { callType: "call", field: "type" },
3363
+ base_list: { callType: "inherit", nameExtractor: heritageExtractor },
3364
+ using_directive: {
3365
+ callType: "import",
3366
+ nameExtractor: importFromText(["using "])
3367
+ }
3368
+ }
2319
3369
  },
2320
3370
  php: {
2321
3371
  declKinds: {
@@ -2334,7 +3384,7 @@ var init_queries = __esm({
2334
3384
  interface_declaration: "name",
2335
3385
  trait_declaration: "name",
2336
3386
  enum_declaration: "name",
2337
- namespace_declaration: "name"
3387
+ namespace_definition: "name"
2338
3388
  },
2339
3389
  scopeNodes: /* @__PURE__ */ new Set([
2340
3390
  "program",
@@ -2343,7 +3393,24 @@ var init_queries = __esm({
2343
3393
  "interface_declaration",
2344
3394
  "trait_declaration",
2345
3395
  "enum_declaration"
2346
- ])
3396
+ ]),
3397
+ refRules: {
3398
+ function_call_expression: { callType: "call", field: "function" },
3399
+ // Verified AST: `new App\Model\User()` carries a BARE qualified_name
3400
+ // child (no `name:` field), so the field default never fires.
3401
+ object_creation_expression: { callType: "call", nameExtractor: phpConstructorExtractor },
3402
+ base_clause: { callType: "inherit", nameExtractor: heritageExtractor },
3403
+ class_interface_clause: {
3404
+ callType: "implement",
3405
+ nameExtractor: heritageExtractor
3406
+ },
3407
+ // Verified AST: `namespace_use_declaration (namespace_use_clause
3408
+ // (qualified_name ...))` — not `use_declaration`.
3409
+ namespace_use_declaration: {
3410
+ callType: "import",
3411
+ nameExtractor: importFromText(["use "])
3412
+ }
3413
+ }
2347
3414
  },
2348
3415
  // ─── Scripting / mobile ────────────────────────────────────────────────────
2349
3416
  ruby: {
@@ -2361,7 +3428,13 @@ var init_queries = __esm({
2361
3428
  module: "name",
2362
3429
  constant: "name"
2363
3430
  },
2364
- scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"])
3431
+ scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"]),
3432
+ refRules: {
3433
+ // `call` covers both `foo(...)` and `obj.foo(...)` — the extractor
3434
+ // records the method leaf, plus `require`/`require_relative` imports.
3435
+ call: { callType: "call", nameExtractor: rubyCallExtractor },
3436
+ superclass: { callType: "inherit", nameExtractor: heritageExtractor }
3437
+ }
2365
3438
  },
2366
3439
  swift: {
2367
3440
  declKinds: {
@@ -2394,7 +3467,18 @@ var init_queries = __esm({
2394
3467
  "protocol_declaration",
2395
3468
  "actor_declaration",
2396
3469
  "extension_declaration"
2397
- ])
3470
+ ]),
3471
+ refRules: {
3472
+ // Verified AST: `call_expression (simple_identifier) (call_suffix …)` —
3473
+ // the callee is a bare first child, no field name.
3474
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3475
+ // Verified AST: `inheritance_specifier inherits_from: (user_type …)`.
3476
+ inheritance_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3477
+ import_declaration: {
3478
+ callType: "import",
3479
+ nameExtractor: importFromText(["import ", "import type ", "@testable import "])
3480
+ }
3481
+ }
2398
3482
  },
2399
3483
  kotlin: {
2400
3484
  declKinds: {
@@ -2419,7 +3503,17 @@ var init_queries = __esm({
2419
3503
  "object_declaration",
2420
3504
  "interface_declaration",
2421
3505
  "function_declaration"
2422
- ])
3506
+ ]),
3507
+ refRules: {
3508
+ call_expression: { callType: "call", nameExtractor: firstIdentifierCallExtractor },
3509
+ // Verified AST: `delegation_specifier (user_type (type_identifier))` —
3510
+ // the `: Handler` / `: Base()` clause.
3511
+ delegation_specifier: { callType: "inherit", nameExtractor: heritageExtractor },
3512
+ import_header: {
3513
+ callType: "import",
3514
+ nameExtractor: importFromText(["import "])
3515
+ }
3516
+ }
2423
3517
  },
2424
3518
  elixir: {
2425
3519
  declKinds: {
@@ -2491,7 +3585,38 @@ function visitTree(tree, content, file, lang, queries) {
2491
3585
  const boundedContent = content.length > TREE_SITTER_MAX_FILE_CHARS ? content.slice(0, TREE_SITTER_MAX_FILE_CHARS) : content;
2492
3586
  const nlOffsets = newlineOffsets3(boundedContent);
2493
3587
  const symbols = [];
3588
+ const refs = [];
3589
+ const seenRefs = /* @__PURE__ */ new Set();
2494
3590
  const scopeStack = [];
3591
+ function emitRefsForNode(node, rule) {
3592
+ const emissions = rule.nameExtractor?.(node) ?? defaultRefTarget(node, rule);
3593
+ if (!emissions) return;
3594
+ const { line } = lineColAt2(nlOffsets, node.startIndex);
3595
+ for (const emission of emissions) {
3596
+ if (!emission.toName) continue;
3597
+ const callType = emission.callType ?? rule.callType;
3598
+ const key = `${emission.toName}:${callType}:${line}:${emission.module ?? ""}:${node.startIndex}`;
3599
+ if (seenRefs.has(key)) continue;
3600
+ seenRefs.add(key);
3601
+ refs.push({
3602
+ fromId: 0,
3603
+ // assignRefsToSymbols attaches owners after insertion
3604
+ toName: emission.toName.slice(0, 200),
3605
+ callType,
3606
+ line,
3607
+ lang,
3608
+ module: emission.module
3609
+ });
3610
+ }
3611
+ }
3612
+ function defaultRefTarget(node, rule) {
3613
+ if (!rule.field) return null;
3614
+ const field = node.childForFieldName(rule.field);
3615
+ if (!field) return null;
3616
+ const leaf = field.text.split(/[.:\\]/).filter(Boolean).pop()?.split(/[<(]/)[0];
3617
+ if (!leaf) return null;
3618
+ return [{ toName: leaf.trim() }];
3619
+ }
2495
3620
  function visit(node, depth) {
2496
3621
  if (symbols.length >= TREE_SITTER_MAX_SYMBOLS) return;
2497
3622
  if (node.isMissing || node.isError) {
@@ -2510,6 +3635,8 @@ function visitTree(tree, content, file, lang, queries) {
2510
3635
  );
2511
3636
  if (emitted) symbols.push(emitted);
2512
3637
  }
3638
+ const refRule = queries.refRules?.[node.type];
3639
+ if (refRule) emitRefsForNode(node, refRule);
2513
3640
  }
2514
3641
  const pushesScope = queries.scopeNodes?.has(node.type) ?? false;
2515
3642
  const pushIdx = pushesScope ? pushScope(scopeStack, node, queries) : -1;
@@ -2527,7 +3654,7 @@ function visitTree(tree, content, file, lang, queries) {
2527
3654
  if (pushIdx !== -1) scopeStack.pop();
2528
3655
  }
2529
3656
  visit(tree.rootNode, 0);
2530
- return { symbols };
3657
+ return { symbols, refs };
2531
3658
  }
2532
3659
  function pushScope(scopeStack, node, queries) {
2533
3660
  const name = extractName(node, queries);
@@ -2614,7 +3741,7 @@ __export(tree_sitter_parser_exports, {
2614
3741
  parseSymbols: () => parseSymbols8,
2615
3742
  parseTreeSitterAst: () => parseTreeSitterAst
2616
3743
  });
2617
- import * as path11 from "node:path";
3744
+ import * as path12 from "node:path";
2618
3745
  import { fileURLToPath } from "node:url";
2619
3746
  function optInEnabled(env) {
2620
3747
  return process.env[env] === "1" || process.env[env] === "true";
@@ -2637,7 +3764,7 @@ async function loadLanguage(lang) {
2637
3764
  if (!grammarName) {
2638
3765
  throw new Error(`tree-sitter: no grammar registered for lang "${lang}"`);
2639
3766
  }
2640
- const wasmPath = path11.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
3767
+ const wasmPath = path12.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
2641
3768
  const { Language, init } = await getRuntime();
2642
3769
  await init();
2643
3770
  const languageObj = await Language.load(wasmPath);
@@ -2658,7 +3785,7 @@ function isTreeSitterSupported(lang) {
2658
3785
  function getGrammarWasmPath(lang) {
2659
3786
  const name = resolveGrammarName(lang);
2660
3787
  if (!name) return void 0;
2661
- return path11.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
3788
+ return path12.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
2662
3789
  }
2663
3790
  async function parseSymbols8(opts) {
2664
3791
  const { file, content, lang } = opts;
@@ -2674,89 +3801,229 @@ async function parseSymbols8(opts) {
2674
3801
  if (!tree) {
2675
3802
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2676
3803
  }
2677
- const { symbols } = visitTree(tree, content, file, lang, getQueries(lang));
2678
- parser.delete();
2679
- tree.delete();
2680
- return { file, lang, symbols, refs: [], mtimeMs: Date.now() };
2681
- } catch {
2682
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2683
- }
2684
- }
2685
- async function loadTreeSitterLanguage(lang) {
2686
- const cached = await loadLanguage(lang);
2687
- return cached.Language;
2688
- }
2689
- async function __smokeRootType(opts) {
2690
- if (!isTreeSitterSupported(opts.lang)) {
2691
- throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
2692
- }
2693
- const { Parser } = await getRuntime();
2694
- const cached = await loadLanguage(opts.lang);
2695
- const parser = new Parser();
2696
- parser.setLanguage(cached.Language);
2697
- let tree = null;
2698
- try {
2699
- tree = parser.parse(opts.content);
2700
- if (!tree) throw new Error("tree-sitter: parser.parse returned null");
2701
- return tree.rootNode.type;
2702
- } finally {
2703
- tree?.delete();
2704
- parser.delete();
3804
+ const { symbols, refs } = visitTree(tree, content, file, lang, getQueries(lang));
3805
+ parser.delete();
3806
+ tree.delete();
3807
+ return { file, lang, symbols, refs, mtimeMs: Date.now() };
3808
+ } catch {
3809
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
3810
+ }
3811
+ }
3812
+ async function loadTreeSitterLanguage(lang) {
3813
+ const cached = await loadLanguage(lang);
3814
+ return cached.Language;
3815
+ }
3816
+ async function __smokeRootType(opts) {
3817
+ if (!isTreeSitterSupported(opts.lang)) {
3818
+ throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
3819
+ }
3820
+ const { Parser } = await getRuntime();
3821
+ const cached = await loadLanguage(opts.lang);
3822
+ const parser = new Parser();
3823
+ parser.setLanguage(cached.Language);
3824
+ let tree = null;
3825
+ try {
3826
+ tree = parser.parse(opts.content);
3827
+ if (!tree) throw new Error("tree-sitter: parser.parse returned null");
3828
+ return tree.rootNode.type;
3829
+ } finally {
3830
+ tree?.delete();
3831
+ parser.delete();
3832
+ }
3833
+ }
3834
+ async function parseTreeSitterAst(opts) {
3835
+ const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
3836
+ if (!grammar) return null;
3837
+ try {
3838
+ const { Parser, Language, init } = await getRuntime();
3839
+ await init();
3840
+ const wasmPath = path12.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
3841
+ const languageObj = await Language.load(wasmPath);
3842
+ const parser = new Parser();
3843
+ parser.setLanguage(languageObj);
3844
+ const tree = parser.parse(opts.content);
3845
+ if (!tree) {
3846
+ parser.delete();
3847
+ return null;
3848
+ }
3849
+ return { tree, parser };
3850
+ } catch {
3851
+ return null;
3852
+ }
3853
+ }
3854
+ var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
3855
+ var init_tree_sitter_parser = __esm({
3856
+ "src/codebase-index/tree-sitter-parser.ts"() {
3857
+ "use strict";
3858
+ init_queries();
3859
+ init_visitor();
3860
+ WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
3861
+ RUNTIME_WASM = path12.join(WASM_DIR, "tree-sitter-runtime.wasm");
3862
+ LANG_TO_GRAMMAR = {
3863
+ c: "c",
3864
+ cpp: "cpp",
3865
+ java: "java",
3866
+ csharp: "c_sharp",
3867
+ // tree-sitter directory uses underscore
3868
+ php: "php",
3869
+ ruby: "ruby",
3870
+ swift: "swift",
3871
+ kotlin: "kotlin",
3872
+ shell: "bash",
3873
+ // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
3874
+ // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
3875
+ elixir: "elixir"
3876
+ };
3877
+ GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
3878
+ PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
3879
+ RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
3880
+ runtimePromise = null;
3881
+ languageCache = /* @__PURE__ */ new Map();
3882
+ }
3883
+ });
3884
+
3885
+ // src/codebase-index/parser-dispatch.ts
3886
+ var parser_dispatch_exports = {};
3887
+ __export(parser_dispatch_exports, {
3888
+ parseFileContent: () => parseFileContent,
3889
+ parseFilesContent: () => parseFilesContent
3890
+ });
3891
+ async function parseFileContent(file, content, lang) {
3892
+ const parsed = await dispatch(file, content, lang);
3893
+ return withRelations(parsed, content, lang);
3894
+ }
3895
+ async function parseFilesContent(files) {
3896
+ if (files.length === 0) return [];
3897
+ const slots = files.map(() => ({ result: null }));
3898
+ const batchingEnabled = process.env["WRONGSTACK_TOOLCHAIN_BATCH"] !== "0";
3899
+ if (batchingEnabled) {
3900
+ const goFiles = [];
3901
+ const pyFiles = [];
3902
+ files.forEach((f, index) => {
3903
+ if (f.lang === "go") goFiles.push({ ...f, index });
3904
+ else if (f.lang === "py") pyFiles.push({ ...f, index });
3905
+ });
3906
+ if (goFiles.length > 0) {
3907
+ await applyBatchResults(slots, goFiles, (chunks) => runGoBatch(chunks), "go");
3908
+ }
3909
+ if (pyFiles.length > 0) {
3910
+ const pyBinary = await resolvePythonBinary();
3911
+ if (pyBinary) {
3912
+ await applyBatchResults(slots, pyFiles, (chunks) => runPyBatch(chunks, pyBinary), "py");
3913
+ }
3914
+ }
3915
+ }
3916
+ const jobs = [];
3917
+ for (let i = 0; i < files.length; i++) {
3918
+ if (slots[i].result !== null) continue;
3919
+ const { file, content, lang } = files[i];
3920
+ const slot = slots[i];
3921
+ jobs.push(
3922
+ (async () => {
3923
+ try {
3924
+ slot.result = await parseFileContent(file, content, lang);
3925
+ } catch (err) {
3926
+ slot.error = err instanceof Error ? err.message : String(err);
3927
+ }
3928
+ })()
3929
+ );
3930
+ }
3931
+ await Promise.all(jobs);
3932
+ return slots;
3933
+ }
3934
+ async function applyBatchResults(slots, batchFiles, runBatch, lang) {
3935
+ for (const chunk of chunkBatchFiles(batchFiles)) {
3936
+ let byFile = null;
3937
+ try {
3938
+ byFile = await runBatch(chunk);
3939
+ } catch {
3940
+ byFile = null;
3941
+ }
3942
+ if (!byFile) continue;
3943
+ for (const item of chunk) {
3944
+ const parsed = byFile.get(item.file);
3945
+ if (!parsed) continue;
3946
+ slots[item.index] = { result: withRelations(parsed, item.content, lang) };
3947
+ }
3948
+ }
3949
+ }
3950
+ async function dispatch(file, content, lang) {
3951
+ switch (lang) {
3952
+ case "ts":
3953
+ case "tsx":
3954
+ case "js":
3955
+ case "jsx": {
3956
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3957
+ return parseSymbols9({ file, content, lang });
3958
+ }
3959
+ case "go": {
3960
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3961
+ return parseSymbols9({ file, content, lang: "go" });
3962
+ }
3963
+ case "py": {
3964
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3965
+ return parseSymbols9({ file, content, lang: "py" });
3966
+ }
3967
+ case "rs": {
3968
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3969
+ return parseSymbols9({ file, content, lang: "rs" });
3970
+ }
3971
+ case "json": {
3972
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3973
+ return parseSymbols9({ file, content, lang: "json" });
3974
+ }
3975
+ case "yaml": {
3976
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3977
+ return parseSymbols9({ file, content, lang: "yaml" });
3978
+ }
3979
+ // Phase 1: ten languages now route through the Tree-Sitter WASM
3980
+ // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3981
+ // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3982
+ // or the parser returns zero symbols — preserving the indexable-file
3983
+ // contract that "missing a parser must never mean skipping the file".
3984
+ case "c":
3985
+ case "cpp":
3986
+ case "java":
3987
+ case "csharp":
3988
+ case "php":
3989
+ case "ruby":
3990
+ case "swift":
3991
+ case "kotlin":
3992
+ case "shell":
3993
+ case "elixir": {
3994
+ try {
3995
+ const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
3996
+ const parsed = await parseSymbols10({ file, content, lang });
3997
+ if (parsed.symbols.length > 0) return parsed;
3998
+ } catch {
3999
+ }
4000
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4001
+ return parseSymbols9({ file, content, lang });
4002
+ }
4003
+ default: {
4004
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4005
+ return parseSymbols9({ file, content, lang });
4006
+ }
2705
4007
  }
2706
4008
  }
2707
- async function parseTreeSitterAst(opts) {
2708
- const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
2709
- if (!grammar) return null;
2710
- try {
2711
- const { Parser, Language, init } = await getRuntime();
2712
- await init();
2713
- const wasmPath = path11.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
2714
- const languageObj = await Language.load(wasmPath);
2715
- const parser = new Parser();
2716
- parser.setLanguage(languageObj);
2717
- const tree = parser.parse(opts.content);
2718
- if (!tree) {
2719
- parser.delete();
2720
- return null;
2721
- }
2722
- return { tree, parser };
2723
- } catch {
2724
- return null;
4009
+ function withRelations(parsed, content, lang) {
4010
+ let refs = parsed.refs ?? [];
4011
+ if (refs.length === 0 && hasImportPatterns(lang)) {
4012
+ refs = extractImports({ content, lang });
2725
4013
  }
4014
+ return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
2726
4015
  }
2727
- var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
2728
- var init_tree_sitter_parser = __esm({
2729
- "src/codebase-index/tree-sitter-parser.ts"() {
4016
+ var init_parser_dispatch = __esm({
4017
+ "src/codebase-index/parser-dispatch.ts"() {
2730
4018
  "use strict";
2731
- init_queries();
2732
- init_visitor();
2733
- WASM_DIR = fileURLToPath(new URL("./wasm/", import.meta.url));
2734
- RUNTIME_WASM = path11.join(WASM_DIR, "tree-sitter-runtime.wasm");
2735
- LANG_TO_GRAMMAR = {
2736
- c: "c",
2737
- cpp: "cpp",
2738
- java: "java",
2739
- csharp: "c_sharp",
2740
- // tree-sitter directory uses underscore
2741
- php: "php",
2742
- ruby: "ruby",
2743
- swift: "swift",
2744
- kotlin: "kotlin",
2745
- shell: "bash",
2746
- // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
2747
- // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
2748
- elixir: "elixir"
2749
- };
2750
- GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
2751
- PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
2752
- RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
2753
- runtimePromise = null;
2754
- languageCache = /* @__PURE__ */ new Map();
4019
+ init_import_extractor();
4020
+ init_parser_batch();
4021
+ init_py_parser();
2755
4022
  }
2756
4023
  });
2757
4024
 
2758
4025
  // src/write.ts
2759
- import * as fs14 from "node:fs/promises";
4026
+ import * as fs15 from "node:fs/promises";
2760
4027
  import { ToolValidationError } from "@wrongstack/core/types";
2761
4028
  import {
2762
4029
  atomicWrite,
@@ -2942,7 +4209,7 @@ function takeHeadBytes(s, maxBytes) {
2942
4209
  }
2943
4210
 
2944
4211
  // src/codebase-index/background-indexer.ts
2945
- import * as fs13 from "node:fs";
4212
+ import * as fs14 from "node:fs";
2946
4213
  import { fileURLToPath as fileURLToPath6 } from "node:url";
2947
4214
  import { Worker as Worker2 } from "node:worker_threads";
2948
4215
 
@@ -3030,9 +4297,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
3030
4297
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3031
4298
  import { execFile } from "node:child_process";
3032
4299
  import { createHash as createHash2 } from "node:crypto";
3033
- import * as fs9 from "node:fs/promises";
4300
+ import * as fs10 from "node:fs/promises";
3034
4301
  import { availableParallelism } from "node:os";
3035
- import * as path14 from "node:path";
4302
+ import * as path15 from "node:path";
3036
4303
  import {
3037
4304
  DEFAULT_WALK_IGNORE_DIRS,
3038
4305
  indexParallelBatchSize,
@@ -3700,283 +4967,96 @@ var ModuleResolver = class {
3700
4967
  const segments = spec.split("::").filter(Boolean);
3701
4968
  if (segments.length === 0) return void 0;
3702
4969
  const head = segments[0];
3703
- if (head === "self" || head === "super") {
3704
- let base = path6.posix.dirname(fromFile);
3705
- for (const segment of segments) {
3706
- if (segment === "super") base = path6.posix.dirname(base);
3707
- else if (segment !== "self") break;
3708
- }
3709
- const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
3710
- return this.lookupWithExtensions(path6.posix.join(base, ...rest2), "rs");
3711
- }
3712
- const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
3713
- const crate = head === "crate" ? owningCrate : this.structure.roots.find(
3714
- (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
3715
- );
3716
- if (!crate) {
3717
- return this.lookupWithExtensions(
3718
- path6.posix.join(path6.posix.dirname(fromFile), ...segments),
3719
- "rs"
3720
- );
3721
- }
3722
- const rest = segments.slice(1);
3723
- for (const base of crate.sourceRoots) {
3724
- const parent = rest.length > 1 ? this.lookupWithExtensions(path6.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
3725
- const exact = this.lookupWithExtensions(path6.posix.join(base, ...rest), "rs");
3726
- const hit = exact ?? parent ?? this.lookupWithExtensions(path6.posix.join(base, "lib"), "rs");
3727
- if (hit) return hit;
3728
- }
3729
- return void 0;
3730
- }
3731
- /** `com.example.Thing` and `com.example.*` against JVM source roots. */
3732
- resolveJvm(spec) {
3733
- const segments = spec.split(".").filter(Boolean);
3734
- if (segments.length === 0) return void 0;
3735
- const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
3736
- const wildcard = segments[segments.length - 1] === "*";
3737
- const parts = wildcard ? segments.slice(0, -1) : segments;
3738
- for (const base of [...sourceRoots, this.structure.projectRoot]) {
3739
- const target = path6.posix.join(base, ...parts);
3740
- const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
3741
- if (hit) return hit;
3742
- }
3743
- return void 0;
3744
- }
3745
- /** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
3746
- resolveInclude(fromFile, spec) {
3747
- const relative3 = this.lookupWithExtensions(
3748
- path6.posix.join(path6.posix.dirname(fromFile), spec),
3749
- "c"
3750
- );
3751
- if (relative3) return relative3;
3752
- for (const base of [
3753
- path6.posix.join(this.structure.projectRoot, "include"),
3754
- this.structure.projectRoot
3755
- ]) {
3756
- const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "c");
3757
- if (hit) return hit;
3758
- }
3759
- return void 0;
3760
- }
3761
- /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
3762
- resolveRuby(fromFile, spec) {
3763
- const relative3 = this.lookupWithExtensions(
3764
- path6.posix.join(path6.posix.dirname(fromFile), spec),
3765
- "ruby"
3766
- );
3767
- if (relative3) return relative3;
3768
- for (const base of [
3769
- path6.posix.join(this.structure.projectRoot, "lib"),
3770
- this.structure.projectRoot
3771
- ]) {
3772
- const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "ruby");
3773
- if (hit) return hit;
3774
- }
3775
- return void 0;
3776
- }
3777
- };
3778
-
3779
- // src/codebase-index/import-extractor.ts
3780
- var IMPORT_MAX_FILE_CHARS = 512 * 1024;
3781
- var IMPORT_MAX_PER_FILE = 400;
3782
- var DOTTED_IMPORT = [
3783
- { re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
3784
- ];
3785
- var LANG_IMPORTS = {
3786
- // Go and Python have real AST extractors; these patterns are the fallback for
3787
- // machines with no Go toolchain or Python interpreter installed, where the
3788
- // parser degrades to regex symbols and would otherwise contribute no edges.
3789
- go: [
3790
- { re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
3791
- // Grouped form: inside `import ( … )` each line is an optional alias plus a
3792
- // quoted path. A stray match elsewhere resolves to no file and is dropped.
3793
- { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
3794
- ],
3795
- py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
3796
- rs: [
3797
- // use a::b::C; | use a::b::{C, D}; → the path before any brace
3798
- { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
3799
- // mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
3800
- { re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
3801
- ],
3802
- java: DOTTED_IMPORT,
3803
- kotlin: DOTTED_IMPORT,
3804
- scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
3805
- csharp: [
3806
- // using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
3807
- { re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
3808
- { re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
3809
- ],
3810
- // Quoted includes only: <stdio.h> is a system header with no indexed file.
3811
- c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3812
- cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
3813
- ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
3814
- php: [
3815
- // `use A\B\C` imports the class C, which is what the index has a symbol
3816
- // for — the namespace symbol only covers the `A\B` prefix.
3817
- { re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
3818
- { re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
3819
- ],
3820
- swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
3821
- dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
3822
- lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
3823
- elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
3824
- haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
3825
- zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
3826
- proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
3827
- // `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
3828
- css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
3829
- // A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
3830
- vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3831
- svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
3832
- html: [
3833
- { re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
3834
- { re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
3835
- ],
3836
- shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
3837
- r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
3838
- };
3839
- function lastSegment(specifier) {
3840
- const pathLike = /[/\\]|::/.test(specifier);
3841
- const segments = specifier.split(/[/\\]|::/).filter(Boolean);
3842
- let last = segments[segments.length - 1] ?? specifier;
3843
- if (last === "*" || last === "_") {
3844
- last = segments[segments.length - 2] ?? specifier;
3845
- }
3846
- if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
3847
- const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
3848
- return dotted[dotted.length - 1] ?? last;
3849
- }
3850
- function newlineOffsets(content) {
3851
- const offsets = [];
3852
- for (let i = 0; i < content.length; i++) {
3853
- if (content.charCodeAt(i) === 10) offsets.push(i);
3854
- }
3855
- return offsets;
3856
- }
3857
- function lineAt(offsets, index) {
3858
- let low = 0;
3859
- let high = offsets.length;
3860
- while (low < high) {
3861
- const mid = low + high >>> 1;
3862
- if ((offsets[mid] ?? 0) < index) low = mid + 1;
3863
- else high = mid;
3864
- }
3865
- return low + 1;
3866
- }
3867
- function hasImportPatterns(lang) {
3868
- return LANG_IMPORTS[lang] !== void 0;
3869
- }
3870
- function extractImports(opts) {
3871
- const patterns = LANG_IMPORTS[opts.lang];
3872
- if (!patterns || !opts.content) return [];
3873
- const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
3874
- const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
3875
- const refs = [];
3876
- const seen = /* @__PURE__ */ new Set();
3877
- const offsets = newlineOffsets(content);
3878
- for (const pattern of patterns) {
3879
- const re = new RegExp(pattern.re.source, pattern.re.flags);
3880
- for (const match of content.matchAll(re)) {
3881
- if (refs.length >= limit) return refs;
3882
- const specifier = match[1]?.trim();
3883
- if (!specifier) continue;
3884
- const module = specifier;
3885
- const toName = pattern.name === "full" ? module : lastSegment(module);
3886
- if (!toName) continue;
3887
- const key = `${module}\0${toName}`;
3888
- if (seen.has(key)) continue;
3889
- seen.add(key);
3890
- refs.push({
3891
- fromId: 0,
3892
- toName,
3893
- callType: "import",
3894
- line: lineAt(offsets, match.index ?? 0),
3895
- lang: opts.lang,
3896
- module
3897
- });
3898
- }
3899
- }
3900
- return refs;
3901
- }
3902
-
3903
- // src/codebase-index/parser-dispatch.ts
3904
- async function parseFileContent(file, content, lang) {
3905
- const parsed = await dispatch(file, content, lang);
3906
- return withRelations(parsed, content, lang);
3907
- }
3908
- async function dispatch(file, content, lang) {
3909
- switch (lang) {
3910
- case "ts":
3911
- case "tsx":
3912
- case "js":
3913
- case "jsx": {
3914
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
3915
- return parseSymbols9({ file, content, lang });
3916
- }
3917
- case "go": {
3918
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
3919
- return parseSymbols9({ file, content, lang: "go" });
3920
- }
3921
- case "py": {
3922
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
3923
- return parseSymbols9({ file, content, lang: "py" });
3924
- }
3925
- case "rs": {
3926
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
3927
- return parseSymbols9({ file, content, lang: "rs" });
4970
+ if (head === "self" || head === "super") {
4971
+ let base = path6.posix.dirname(fromFile);
4972
+ for (const segment of segments) {
4973
+ if (segment === "super") base = path6.posix.dirname(base);
4974
+ else if (segment !== "self") break;
4975
+ }
4976
+ const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
4977
+ return this.lookupWithExtensions(path6.posix.join(base, ...rest2), "rs");
3928
4978
  }
3929
- case "json": {
3930
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
3931
- return parseSymbols9({ file, content, lang: "json" });
4979
+ const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
4980
+ const crate = head === "crate" ? owningCrate : this.structure.roots.find(
4981
+ (root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
4982
+ );
4983
+ if (!crate) {
4984
+ return this.lookupWithExtensions(
4985
+ path6.posix.join(path6.posix.dirname(fromFile), ...segments),
4986
+ "rs"
4987
+ );
3932
4988
  }
3933
- case "yaml": {
3934
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
3935
- return parseSymbols9({ file, content, lang: "yaml" });
4989
+ const rest = segments.slice(1);
4990
+ for (const base of crate.sourceRoots) {
4991
+ const parent = rest.length > 1 ? this.lookupWithExtensions(path6.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
4992
+ const exact = this.lookupWithExtensions(path6.posix.join(base, ...rest), "rs");
4993
+ const hit = exact ?? parent ?? this.lookupWithExtensions(path6.posix.join(base, "lib"), "rs");
4994
+ if (hit) return hit;
3936
4995
  }
3937
- // Phase 1: ten languages now route through the Tree-Sitter WASM
3938
- // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
3939
- // the regex extractor in `generic-parser.ts` whenever WASM loading fails
3940
- // or the parser returns zero symbols — preserving the indexable-file
3941
- // contract that "missing a parser must never mean skipping the file".
3942
- case "c":
3943
- case "cpp":
3944
- case "java":
3945
- case "csharp":
3946
- case "php":
3947
- case "ruby":
3948
- case "swift":
3949
- case "kotlin":
3950
- case "shell":
3951
- case "elixir": {
3952
- try {
3953
- const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
3954
- const parsed = await parseSymbols10({ file, content, lang });
3955
- if (parsed.symbols.length > 0) return parsed;
3956
- } catch {
3957
- }
3958
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
3959
- return parseSymbols9({ file, content, lang });
4996
+ return void 0;
4997
+ }
4998
+ /** `com.example.Thing` and `com.example.*` against JVM source roots. */
4999
+ resolveJvm(spec) {
5000
+ const segments = spec.split(".").filter(Boolean);
5001
+ if (segments.length === 0) return void 0;
5002
+ const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
5003
+ const wildcard = segments[segments.length - 1] === "*";
5004
+ const parts = wildcard ? segments.slice(0, -1) : segments;
5005
+ for (const base of [...sourceRoots, this.structure.projectRoot]) {
5006
+ const target = path6.posix.join(base, ...parts);
5007
+ const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
5008
+ if (hit) return hit;
3960
5009
  }
3961
- default: {
3962
- const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
3963
- return parseSymbols9({ file, content, lang });
5010
+ return void 0;
5011
+ }
5012
+ /** `#include "foo/bar.h"` quoted form only; `<…>` is a system header. */
5013
+ resolveInclude(fromFile, spec) {
5014
+ const relative3 = this.lookupWithExtensions(
5015
+ path6.posix.join(path6.posix.dirname(fromFile), spec),
5016
+ "c"
5017
+ );
5018
+ if (relative3) return relative3;
5019
+ for (const base of [
5020
+ path6.posix.join(this.structure.projectRoot, "include"),
5021
+ this.structure.projectRoot
5022
+ ]) {
5023
+ const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "c");
5024
+ if (hit) return hit;
3964
5025
  }
5026
+ return void 0;
3965
5027
  }
3966
- }
3967
- function withRelations(parsed, content, lang) {
3968
- let refs = parsed.refs ?? [];
3969
- if (refs.length === 0 && hasImportPatterns(lang)) {
3970
- refs = extractImports({ content, lang });
5028
+ /** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
5029
+ resolveRuby(fromFile, spec) {
5030
+ const relative3 = this.lookupWithExtensions(
5031
+ path6.posix.join(path6.posix.dirname(fromFile), spec),
5032
+ "ruby"
5033
+ );
5034
+ if (relative3) return relative3;
5035
+ for (const base of [
5036
+ path6.posix.join(this.structure.projectRoot, "lib"),
5037
+ this.structure.projectRoot
5038
+ ]) {
5039
+ const hit = this.lookupWithExtensions(path6.posix.join(base, spec), "ruby");
5040
+ if (hit) return hit;
5041
+ }
5042
+ return void 0;
3971
5043
  }
3972
- return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
3973
- }
5044
+ };
5045
+
5046
+ // src/codebase-index/indexer.ts
5047
+ init_parser_dispatch();
3974
5048
 
3975
5049
  // src/codebase-index/parser-worker-pool.ts
5050
+ import * as fs7 from "node:fs";
5051
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
3976
5052
  import { Worker } from "node:worker_threads";
3977
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3978
- import * as fs6 from "node:fs";
3979
5053
  var WORKER_POOL_THRESHOLD = 500;
5054
+ function resolveWorkerPoolThreshold() {
5055
+ const raw = process.env["WRONGSTACK_INDEX_WORKER_THRESHOLD"];
5056
+ if (raw === void 0) return WORKER_POOL_THRESHOLD;
5057
+ if (!/^\d+$/.test(raw)) return WORKER_POOL_THRESHOLD;
5058
+ return Number.parseInt(raw, 10);
5059
+ }
3980
5060
  var ParserWorkerPool = class {
3981
5061
  constructor(maxWorkers = defaultWorkerCount()) {
3982
5062
  this.maxWorkers = maxWorkers;
@@ -4020,7 +5100,8 @@ var ParserWorkerPool = class {
4020
5100
  w.unref();
4021
5101
  w.on("message", (msg) => this.handleMessage(msg));
4022
5102
  w.on("error", (err) => this.handleError(err, w));
4023
- this.workers.push({ worker: w, busy: false });
5103
+ w.on("exit", () => this.retireByReference(w));
5104
+ this.workers.push({ worker: w, workerId: w.threadId, busy: false });
4024
5105
  } catch {
4025
5106
  if (this.workers.length === 0) {
4026
5107
  this.unavailable = true;
@@ -4036,7 +5117,7 @@ var ParserWorkerPool = class {
4036
5117
  }
4037
5118
  /**
4038
5119
  * Parse files in parallel across the worker pool. Returns a flat
4039
- * `FileSymbols[]` in completion order (caller sorts if needed).
5120
+ * `FileSymbols[]` in completion order (caller matches by file path).
4040
5121
  *
4041
5122
  * Content is pre-read by the main thread (for the content-hash check)
4042
5123
  * and passed to workers to avoid a second disk read. Files are
@@ -4057,21 +5138,19 @@ var ParserWorkerPool = class {
4057
5138
  chunks[i % workerCount].push(files[i]);
4058
5139
  }
4059
5140
  return new Promise((resolve4, reject) => {
5141
+ const pendingChunks = /* @__PURE__ */ new Map();
4060
5142
  this.pending.set(batchId, {
4061
5143
  resolve: resolve4,
4062
5144
  reject,
4063
5145
  accumulated: [],
4064
- expectedWorkers: workerCount,
4065
- completedWorkers: 0
5146
+ pendingChunks,
5147
+ settled: false
4066
5148
  });
4067
5149
  for (let i = 0; i < workerCount; i++) {
4068
5150
  const pw = this.workers[i];
4069
5151
  pw.busy = true;
4070
- pw.worker.postMessage({
4071
- type: "parse",
4072
- id: batchId,
4073
- files: chunks[i]
4074
- });
5152
+ pendingChunks.set(pw.workerId, chunks[i]);
5153
+ pw.worker.postMessage({ type: "parse", id: batchId, files: chunks[i] });
4075
5154
  }
4076
5155
  });
4077
5156
  }
@@ -4080,6 +5159,12 @@ var ParserWorkerPool = class {
4080
5159
  const workers = this.workers.map((w) => w.worker);
4081
5160
  this.workers = [];
4082
5161
  this.unavailable = false;
5162
+ for (const [, p] of this.pending) {
5163
+ if (p.settled) continue;
5164
+ p.settled = true;
5165
+ p.reject(new Error("ParserWorkerPool shut down"));
5166
+ }
5167
+ this.pending.clear();
4083
5168
  for (const w of workers) {
4084
5169
  try {
4085
5170
  w.postMessage({ type: "shutdown" });
@@ -4100,39 +5185,117 @@ var ParserWorkerPool = class {
4100
5185
  })
4101
5186
  )
4102
5187
  );
4103
- for (const [, p] of this.pending) p.reject(new Error("ParserWorkerPool shut down"));
4104
- this.pending.clear();
4105
5188
  }
4106
5189
  handleMessage(msg) {
4107
5190
  const batch = this.pending.get(msg.id);
4108
5191
  if (!batch) return;
5192
+ const worker2 = this.workers.find((w) => w.workerId === msg.workerId);
5193
+ if (worker2) worker2.busy = false;
5194
+ batch.pendingChunks.delete(msg.workerId);
4109
5195
  batch.accumulated.push(...msg.results);
4110
- batch.completedWorkers++;
4111
- const freeWorker = this.workers.find((w) => w.busy);
4112
- if (freeWorker) freeWorker.busy = false;
4113
- if (batch.completedWorkers >= batch.expectedWorkers) {
5196
+ if (batch.pendingChunks.size === 0) {
5197
+ if (batch.settled) return;
5198
+ batch.settled = true;
4114
5199
  this.pending.delete(msg.id);
4115
5200
  batch.resolve(batch.accumulated);
4116
5201
  }
4117
5202
  }
4118
- handleError(err, source) {
4119
- this.workers = this.workers.filter((w) => w.worker !== source);
4120
- if (this.workers.length === 0) {
4121
- for (const [, p] of this.pending) p.reject(err);
4122
- this.pending.clear();
4123
- this.unavailable = true;
5203
+ /**
5204
+ * Remove a worker from the pool and salvage any chunk it still owed.
5205
+ *
5206
+ * Idempotent by workerId `error` and `exit` can both fire for one
5207
+ * death, and a worker may die while no batch references it. When the
5208
+ * dead worker owed files to an in-flight batch and other workers remain,
5209
+ * those files are re-parsed inline on this thread (one fewer worker
5210
+ * should cost latency, not correctness). When it was the last worker,
5211
+ * every remaining batch rejects so the indexer's existing inline
5212
+ * fallback takes over the whole batch.
5213
+ */
5214
+ retireWorker(workerId) {
5215
+ const entry = this.workers.find((w) => w.workerId === workerId);
5216
+ if (!entry) return;
5217
+ this.workers = this.workers.filter((w) => w.workerId !== workerId);
5218
+ for (const [batchId, batch] of [...this.pending]) {
5219
+ const orphaned = batch.pendingChunks.get(workerId);
5220
+ if (!orphaned) continue;
5221
+ if (this.workers.length === 0) {
5222
+ this.pending.delete(batchId);
5223
+ this.unavailable = true;
5224
+ if (!batch.settled) {
5225
+ batch.settled = true;
5226
+ batch.reject(new Error("ParserWorkerPool: all workers died mid-batch"));
5227
+ }
5228
+ continue;
5229
+ }
5230
+ void this.reparseInline(batch, orphaned, workerId);
5231
+ }
5232
+ }
5233
+ /**
5234
+ * Salvage path: re-parse an orphaned chunk on this thread. Files that
5235
+ * fail here stay absent from the results — same contract as a per-file
5236
+ * error inside a live worker (see handleMessage).
5237
+ */
5238
+ async reparseInline(batch, orphaned, workerId) {
5239
+ try {
5240
+ const { parseFileContent: parseFileContent2 } = await Promise.resolve().then(() => (init_parser_dispatch(), parser_dispatch_exports));
5241
+ for (const item of orphaned) {
5242
+ if (batch.settled) return;
5243
+ try {
5244
+ const parsed = await parseFileContent2(item.file, item.content, item.lang);
5245
+ batch.accumulated.push(parsed);
5246
+ } catch {
5247
+ }
5248
+ await new Promise((resolve4) => setImmediate(resolve4));
5249
+ }
5250
+ } finally {
5251
+ this.finishSalvage(batch, workerId);
5252
+ }
5253
+ }
5254
+ /**
5255
+ * Terminal tail of a salvage — runs on every exit path. Kept free of
5256
+ * control flow inside a `finally` (noUnsafeFinally): releases the
5257
+ * pending-marker and resolves the batch if this was its last chunk.
5258
+ */
5259
+ finishSalvage(batch, workerId) {
5260
+ batch.pendingChunks.delete(workerId);
5261
+ if (batch.pendingChunks.size === 0) {
5262
+ for (const [batchId, tracked] of this.pending) {
5263
+ if (tracked === batch) {
5264
+ if (batch.settled) return;
5265
+ batch.settled = true;
5266
+ this.pending.delete(batchId);
5267
+ batch.resolve(batch.accumulated);
5268
+ return;
5269
+ }
5270
+ }
4124
5271
  }
4125
5272
  }
5273
+ /**
5274
+ * Retire by worker object rather than threadId. `threadId` is -1 before
5275
+ * the worker emits `online`, so a death during script load would make a
5276
+ * threadId-keyed lookup silently no-op and leak the entry (with its
5277
+ * pending chunk) — reference identity is correct in every case.
5278
+ */
5279
+ retireByReference(source) {
5280
+ const entry = this.workers.find((w) => w.worker === source);
5281
+ if (entry) this.retireWorker(entry.workerId);
5282
+ }
5283
+ handleError(err, source) {
5284
+ void err;
5285
+ this.retireByReference(source);
5286
+ }
4126
5287
  };
4127
5288
  function defaultWorkerCount() {
4128
5289
  const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
4129
5290
  return Math.max(1, Math.min(4, cores - 1));
4130
5291
  }
4131
5292
  function resolveWorkerScriptUrl() {
5293
+ const override = process.env["WRONGSTACK_PARSER_WORKER_SCRIPT"];
5294
+ if (override) return pathToFileURL(override);
4132
5295
  for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
4133
5296
  try {
4134
5297
  const url = new URL(rel, import.meta.url);
4135
- if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
5298
+ if (url.protocol === "file:" && fs7.existsSync(fileURLToPath2(url))) return url;
4136
5299
  } catch {
4137
5300
  }
4138
5301
  }
@@ -4145,8 +5308,8 @@ function getParserPool() {
4145
5308
  }
4146
5309
 
4147
5310
  // src/codebase-index/writer.ts
4148
- import * as fs8 from "node:fs";
4149
- import * as path13 from "node:path";
5311
+ import * as fs9 from "node:fs";
5312
+ import * as path14 from "node:path";
4150
5313
 
4151
5314
  // src/codebase-index/bm25.ts
4152
5315
  var K1 = 1.5;
@@ -4241,11 +5404,11 @@ var Bm25Index = class {
4241
5404
  init_languages();
4242
5405
 
4243
5406
  // src/codebase-index/schema.ts
4244
- var SCHEMA_VERSION = 4;
5407
+ var SCHEMA_VERSION = 5;
4245
5408
 
4246
5409
  // src/codebase-index/sqlite-runtime.ts
4247
- import { createRequire } from "node:module";
4248
5410
  import { toErrorMessage } from "@wrongstack/core/utils";
5411
+ import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
4249
5412
  var warningSilenced = false;
4250
5413
  function silenceSqliteExperimentalWarning() {
4251
5414
  if (warningSilenced) return;
@@ -4263,11 +5426,10 @@ function loadDatabaseSync() {
4263
5426
  if (DatabaseSyncCtor) return DatabaseSyncCtor;
4264
5427
  silenceSqliteExperimentalWarning();
4265
5428
  try {
4266
- const req = createRequire(import.meta.url);
4267
- DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
5429
+ DatabaseSyncCtor = loadRuntimeDatabaseSync();
4268
5430
  } catch (err) {
4269
5431
  throw new Error(
4270
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
5432
+ `The codebase index needs node:sqlite (Node >= 22.5) or bun:sqlite. This runtime doesn't provide it: ${toErrorMessage(err)}`
4271
5433
  );
4272
5434
  }
4273
5435
  return DatabaseSyncCtor;
@@ -4310,9 +5472,87 @@ function runSqliteWithRetry(fn) {
4310
5472
  throw lastError;
4311
5473
  }
4312
5474
 
5475
+ // src/codebase-index/vector-search.ts
5476
+ var RRF_K = 60;
5477
+ function vectorEmbeddingEnabled() {
5478
+ return process.env["WRONGSTACK_INDEX_VECTORS"] === "1";
5479
+ }
5480
+ var VECTOR_DIMENSIONS = 384;
5481
+ var NGRAM_SIZE = 3;
5482
+ function embedText(text) {
5483
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
5484
+ const normalized = text.toLowerCase().trim();
5485
+ if (normalized.length < NGRAM_SIZE) {
5486
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5487
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5488
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
5489
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5490
+ vec[bucket] += 1;
5491
+ }
5492
+ } else {
5493
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5494
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
5495
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5496
+ vec[bucket] += 1;
5497
+ }
5498
+ }
5499
+ let norm = 0;
5500
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5501
+ norm += vec[i] * vec[i];
5502
+ }
5503
+ norm = Math.sqrt(norm);
5504
+ if (norm > 0) {
5505
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5506
+ vec[i] /= norm;
5507
+ }
5508
+ }
5509
+ return vec;
5510
+ }
5511
+ function hashNgram(str) {
5512
+ let hash = 2166136261;
5513
+ for (let i = 0; i < str.length; i++) {
5514
+ hash ^= str.charCodeAt(i);
5515
+ hash = Math.imul(hash, 16777619);
5516
+ }
5517
+ return hash >>> 0;
5518
+ }
5519
+ function cosineSimilarity(a, b) {
5520
+ let dot = 0;
5521
+ const len = Math.min(a.length, b.length);
5522
+ for (let i = 0; i < len; i++) {
5523
+ dot += a[i] * b[i];
5524
+ }
5525
+ return dot;
5526
+ }
5527
+ function encodeVector(vec) {
5528
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5529
+ }
5530
+ function decodeVector(buf) {
5531
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5532
+ const copy = new Float32Array(buf.byteLength / 4);
5533
+ for (let i = 0; i < copy.length; i++) {
5534
+ copy[i] = view.getFloat32(i * 4, true);
5535
+ }
5536
+ return copy;
5537
+ }
5538
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5539
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5540
+ const scored = [];
5541
+ for (const id of allIds) {
5542
+ const bm25Rank = bm25Ranks.get(id);
5543
+ const vecRank = vectorRanks.get(id);
5544
+ let score = 0;
5545
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5546
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5547
+ scored.push([id, score]);
5548
+ }
5549
+ scored.sort((a, b) => b[1] - a[1]);
5550
+ return scored;
5551
+ }
5552
+
4313
5553
  // src/codebase-index/writer-admin.ts
4314
- import * as fs7 from "node:fs";
4315
- import * as path12 from "node:path";
5554
+ import * as fs8 from "node:fs";
5555
+ import * as path13 from "node:path";
4316
5556
  var DB_FILE = "index.db";
4317
5557
  function getAllIndexableWithStatement(stmt) {
4318
5558
  return stmt("SELECT id, text FROM symbols").all().map(
@@ -4348,6 +5588,14 @@ function getMetadataWithStatement(stmt, key) {
4348
5588
  const rows = stmt("SELECT value FROM metadata WHERE key = ?").all(key);
4349
5589
  return rows[0]?.value;
4350
5590
  }
5591
+ function getIndexSummaryWithStatement(stmt) {
5592
+ const fileRows = stmt("SELECT COUNT(*) AS n FROM files").all();
5593
+ const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
5594
+ return {
5595
+ totalFiles: fileRows[0] ? Number(fileRows[0].n) : 0,
5596
+ lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null
5597
+ };
5598
+ }
4351
5599
  function getFileMetaWithStatement(stmt, file) {
4352
5600
  const rows = stmt(
4353
5601
  "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
@@ -4377,22 +5625,102 @@ function getAllFileMetasWithStatement(stmt) {
4377
5625
  }
4378
5626
  function getIndexDbSizeBytes(indexDir) {
4379
5627
  try {
4380
- return fs7.statSync(path12.join(indexDir, DB_FILE)).size;
5628
+ return fs8.statSync(path13.join(indexDir, DB_FILE)).size;
4381
5629
  } catch {
4382
5630
  return 0;
4383
5631
  }
4384
5632
  }
4385
5633
 
5634
+ // src/codebase-index/writer-helpers.ts
5635
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
5636
+ function escapeLike(value) {
5637
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
5638
+ }
5639
+ function ladderChunkSizes(total, max) {
5640
+ if (total <= 0) return [];
5641
+ const sizes = [];
5642
+ let remaining = total;
5643
+ while (remaining > 0) {
5644
+ const cap = Math.min(remaining, max);
5645
+ const pow = cap >= 1 ? 2 ** Math.floor(Math.log2(cap)) : 1;
5646
+ const take = Math.max(1, Math.min(pow, remaining));
5647
+ sizes.push(take);
5648
+ remaining -= take;
5649
+ }
5650
+ return sizes;
5651
+ }
5652
+ function nextPow2(count) {
5653
+ return count <= 1 ? 1 : 2 ** Math.ceil(Math.log2(count));
5654
+ }
5655
+ function padToInBucket(values) {
5656
+ if (values.length <= 1) return values.slice();
5657
+ const target = nextPow2(values.length);
5658
+ const padded = values.slice();
5659
+ while (padded.length < target) padded.push(padded[0]);
5660
+ return padded;
5661
+ }
5662
+ function placeholders(count) {
5663
+ return Array.from({ length: count }, () => "?").join(",");
5664
+ }
5665
+ function inListChunks(total, max) {
5666
+ if (total <= 0) return [];
5667
+ if (nextPow2(total) <= max) return [total];
5668
+ const powMax = Math.max(1, 2 ** Math.floor(Math.log2(max)));
5669
+ return ladderChunkSizes(total, powMax);
5670
+ }
5671
+ function posixIndexPath(file) {
5672
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
5673
+ }
5674
+ function indexedFileMatchSql(column = "file") {
5675
+ return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
5676
+ }
5677
+ function indexedFileMatchArgs(file) {
5678
+ const posix4 = posixIndexPath(file.trim());
5679
+ return [file, posix4, `%/${escapeLike(posix4)}`];
5680
+ }
5681
+ function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
5682
+ if (packageLabel === filter) return true;
5683
+ const posixFile = posixIndexPath(storedFile);
5684
+ const posixFilter = posixIndexPath(filter.trim());
5685
+ if (!posixFilter) return false;
5686
+ return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
5687
+ }
5688
+ function assignRefsToSymbols(refs, symbols) {
5689
+ if (refs.length === 0 || symbols.length === 0) return [];
5690
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
5691
+ const seen = /* @__PURE__ */ new Set();
5692
+ const assigned = [];
5693
+ for (const ref of refs) {
5694
+ let owner;
5695
+ for (const symbol of ordered) {
5696
+ if (symbol.line > ref.line) break;
5697
+ owner = symbol;
5698
+ }
5699
+ if (!owner && ref.callType === "import") owner = ordered[0];
5700
+ if (!owner || owner.id <= 0) continue;
5701
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5702
+ if (seen.has(key)) continue;
5703
+ seen.add(key);
5704
+ assigned.push({ ...ref, fromId: owner.id });
5705
+ }
5706
+ return assigned;
5707
+ }
5708
+ function resolveIndexDir(projectRoot, override) {
5709
+ return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
5710
+ }
5711
+
4386
5712
  // src/codebase-index/writer-bulk-insert.ts
4387
5713
  function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4388
5714
  if (rows.length === 0) return;
4389
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 12));
4390
- for (let i = 0; i < rows.length; i += chunkSize) {
4391
- const chunk = rows.slice(i, i + chunkSize);
4392
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5715
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 12)));
5716
+ let cursor = 0;
5717
+ for (const take of ladder) {
5718
+ const chunk = rows.slice(cursor, cursor + take);
5719
+ cursor += take;
5720
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4393
5721
  const insert = stmt(
4394
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
4395
- VALUES ${placeholders}`
5722
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text)
5723
+ VALUES ${placeholders2}`
4396
5724
  );
4397
5725
  const binds = [];
4398
5726
  for (const r of chunk) {
@@ -4407,8 +5735,7 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4407
5735
  r.signature,
4408
5736
  r.docComment,
4409
5737
  r.scope,
4410
- r.text,
4411
- r.file
5738
+ r.text
4412
5739
  );
4413
5740
  }
4414
5741
  insert.run(...binds);
@@ -4416,11 +5743,13 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
4416
5743
  }
4417
5744
  function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4418
5745
  if (!ftsAvailable || rows.length === 0) return;
4419
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4420
- for (let i = 0; i < rows.length; i += chunkSize) {
4421
- const chunk = rows.slice(i, i + chunkSize);
4422
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4423
- const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
5746
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5747
+ let cursor = 0;
5748
+ for (const take of ladder) {
5749
+ const chunk = rows.slice(cursor, cursor + take);
5750
+ cursor += take;
5751
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5752
+ const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders2}`);
4424
5753
  const binds = [];
4425
5754
  for (const r of chunk) binds.push(r.id, r.text);
4426
5755
  insert.run(...binds);
@@ -4428,11 +5757,13 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
4428
5757
  }
4429
5758
  function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4430
5759
  if (rows.length === 0) return;
4431
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
4432
- for (let i = 0; i < rows.length; i += chunkSize) {
4433
- const chunk = rows.slice(i, i + chunkSize);
4434
- const placeholders = chunk.map(() => "(?, ?)").join(", ");
4435
- const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders}`);
5760
+ const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
5761
+ let cursor = 0;
5762
+ for (const take of ladder) {
5763
+ const chunk = rows.slice(cursor, cursor + take);
5764
+ cursor += take;
5765
+ const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
5766
+ const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders2}`);
4436
5767
  const binds = [];
4437
5768
  for (const r of chunk) binds.push(r.id, r.vector);
4438
5769
  insert.run(...binds);
@@ -4440,13 +5771,15 @@ function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
4440
5771
  }
4441
5772
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
4442
5773
  if (refs.length === 0) return;
4443
- const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
4444
- for (let i = 0; i < refs.length; i += chunkSize) {
4445
- const chunk = refs.slice(i, i + chunkSize);
4446
- const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
5774
+ const ladder = ladderChunkSizes(refs.length, Math.max(1, Math.floor(maxSqlVars / 8)));
5775
+ let cursor = 0;
5776
+ for (const take of ladder) {
5777
+ const chunk = refs.slice(cursor, cursor + take);
5778
+ cursor += take;
5779
+ const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
4447
5780
  const insert = stmt(
4448
5781
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
4449
- VALUES ${placeholders}`
5782
+ VALUES ${placeholders2}`
4450
5783
  );
4451
5784
  const binds = [];
4452
5785
  for (const ref of chunk) {
@@ -4600,52 +5933,6 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
4600
5933
  return edges;
4601
5934
  }
4602
5935
 
4603
- // src/codebase-index/writer-helpers.ts
4604
- import { resolveWstackPaths } from "@wrongstack/core/utils";
4605
- function escapeLike(value) {
4606
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
4607
- }
4608
- function posixIndexPath(file) {
4609
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
4610
- }
4611
- function indexedFileMatchSql(column = "file") {
4612
- return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
4613
- }
4614
- function indexedFileMatchArgs(file) {
4615
- const posix4 = posixIndexPath(file.trim());
4616
- return [file, posix4, `%/${escapeLike(posix4)}`];
4617
- }
4618
- function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
4619
- if (packageLabel === filter) return true;
4620
- const posixFile = posixIndexPath(storedFile);
4621
- const posixFilter = posixIndexPath(filter.trim());
4622
- if (!posixFilter) return false;
4623
- return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
4624
- }
4625
- function assignRefsToSymbols(refs, symbols) {
4626
- if (refs.length === 0 || symbols.length === 0) return [];
4627
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4628
- const seen = /* @__PURE__ */ new Set();
4629
- const assigned = [];
4630
- for (const ref of refs) {
4631
- let owner;
4632
- for (const symbol of ordered) {
4633
- if (symbol.line > ref.line) break;
4634
- owner = symbol;
4635
- }
4636
- if (!owner && ref.callType === "import") owner = ordered[0];
4637
- if (!owner || owner.id <= 0) continue;
4638
- const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4639
- if (seen.has(key)) continue;
4640
- seen.add(key);
4641
- assigned.push({ ...ref, fromId: owner.id });
4642
- }
4643
- return assigned;
4644
- }
4645
- function resolveIndexDir(projectRoot, override) {
4646
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4647
- }
4648
-
4649
5936
  // src/codebase-index/writer-ref-mapper.ts
4650
5937
  function mapWriterRefRow(row) {
4651
5938
  return {
@@ -4667,20 +5954,20 @@ function mapWriterRefRow(row) {
4667
5954
  var MAX_SQL_VARS = 900;
4668
5955
  function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
4669
5956
  const results = [];
4670
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4671
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4672
- const placeholders = chunk.map(() => "?").join(",");
4673
- const sql = buildSql(placeholders);
5957
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5958
+ const chunk = padToInBucket(ids.slice(0, take));
5959
+ ids = ids.slice(take);
5960
+ const sql = buildSql(placeholders(chunk.length));
4674
5961
  results.push(...stmt(sql).all(...chunk, ...extraArgs));
4675
5962
  }
4676
5963
  return results;
4677
5964
  }
4678
5965
  function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
4679
5966
  let total = 0;
4680
- for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
4681
- const chunk = ids.slice(start, start + MAX_SQL_VARS);
4682
- const placeholders = chunk.map(() => "?").join(",");
4683
- const sql = buildSql(placeholders);
5967
+ for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
5968
+ const chunk = padToInBucket(ids.slice(0, take));
5969
+ ids = ids.slice(take);
5970
+ const sql = buildSql(placeholders(chunk.length));
4684
5971
  const rows = stmt(sql).all(...chunk, ...extraArgs);
4685
5972
  total += rows[0]?.n ?? 0;
4686
5973
  }
@@ -4709,16 +5996,24 @@ function resolveIndexedFiles(stmt, file) {
4709
5996
  }
4710
5997
  function resolveSymbolIds(stmt, symbolName, file) {
4711
5998
  if (!file) {
4712
- const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
4713
- return rows2.map((r) => r.id);
5999
+ const rows = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(
6000
+ symbolName
6001
+ );
6002
+ return rows.map((r) => r.id);
4714
6003
  }
4715
6004
  const indexedFiles = resolveIndexedFiles(stmt, file);
4716
6005
  if (indexedFiles.length === 0) return [];
4717
- const placeholders = indexedFiles.map(() => "?").join(",");
4718
- const rows = stmt(
4719
- `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
4720
- ).all(symbolName, ...indexedFiles);
4721
- return rows.map((r) => r.id);
6006
+ const ids = [];
6007
+ let cursor = 0;
6008
+ for (const take of inListChunks(indexedFiles.length, MAX_SQL_VARS)) {
6009
+ const files = padToInBucket(indexedFiles.slice(cursor, cursor + take));
6010
+ cursor += take;
6011
+ const rows = stmt(
6012
+ `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders(files.length)}) ORDER BY id`
6013
+ ).all(symbolName, ...files);
6014
+ ids.push(...rows.map((r) => r.id));
6015
+ }
6016
+ return ids;
4722
6017
  }
4723
6018
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4724
6019
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
@@ -5152,9 +6447,9 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5152
6447
  const loadedIds = new Set(syms.map((s) => s.id));
5153
6448
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
5154
6449
  if (missingIds.length > 0) {
5155
- const placeholders = missingIds.map(() => "?").join(",");
6450
+ const placeholders2 = missingIds.map(() => "?").join(",");
5156
6451
  const extras = stmt(
5157
- `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
6452
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders2})`
5158
6453
  ).all(...missingIds);
5159
6454
  for (const s of extras) symById.set(s.id, s);
5160
6455
  }
@@ -5167,81 +6462,6 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5167
6462
  return { nodes, edges };
5168
6463
  }
5169
6464
 
5170
- // src/codebase-index/vector-search.ts
5171
- var RRF_K = 60;
5172
- var VECTOR_DIMENSIONS = 384;
5173
- var NGRAM_SIZE = 3;
5174
- function embedText(text) {
5175
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5176
- const normalized = text.toLowerCase().trim();
5177
- if (normalized.length < NGRAM_SIZE) {
5178
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5179
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5180
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5181
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5182
- vec[bucket] += 1;
5183
- }
5184
- } else {
5185
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5186
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5187
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5188
- vec[bucket] += 1;
5189
- }
5190
- }
5191
- let norm = 0;
5192
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5193
- norm += vec[i] * vec[i];
5194
- }
5195
- norm = Math.sqrt(norm);
5196
- if (norm > 0) {
5197
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5198
- vec[i] /= norm;
5199
- }
5200
- }
5201
- return vec;
5202
- }
5203
- function hashNgram(str) {
5204
- let hash = 2166136261;
5205
- for (let i = 0; i < str.length; i++) {
5206
- hash ^= str.charCodeAt(i);
5207
- hash = Math.imul(hash, 16777619);
5208
- }
5209
- return hash >>> 0;
5210
- }
5211
- function cosineSimilarity(a, b) {
5212
- let dot = 0;
5213
- const len = Math.min(a.length, b.length);
5214
- for (let i = 0; i < len; i++) {
5215
- dot += a[i] * b[i];
5216
- }
5217
- return dot;
5218
- }
5219
- function encodeVector(vec) {
5220
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5221
- }
5222
- function decodeVector(buf) {
5223
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5224
- const copy = new Float32Array(buf.byteLength / 4);
5225
- for (let i = 0; i < copy.length; i++) {
5226
- copy[i] = view.getFloat32(i * 4, true);
5227
- }
5228
- return copy;
5229
- }
5230
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5231
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5232
- const scored = [];
5233
- for (const id of allIds) {
5234
- const bm25Rank = bm25Ranks.get(id);
5235
- const vecRank = vectorRanks.get(id);
5236
- let score = 0;
5237
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5238
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5239
- scored.push([id, score]);
5240
- }
5241
- scored.sort((a, b) => b[1] - a[1]);
5242
- return scored;
5243
- }
5244
-
5245
6465
  // src/codebase-index/writer-mutations.ts
5246
6466
  function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5247
6467
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
@@ -5253,24 +6473,29 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5253
6473
  for (const ref of entry.refs) affectedNames.add(ref.toName);
5254
6474
  }
5255
6475
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5256
- const placeholders = options.deleteForFiles.map(() => "?").join(",");
5257
6476
  for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5258
6477
  affectedNames.add(name);
5259
6478
  }
5260
- if (ftsAvailable) {
5261
- stmtFn(
5262
- `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5263
- ).run(...options.deleteForFiles);
5264
- }
5265
- if (vectorsAvailable) {
6479
+ let cursor = 0;
6480
+ for (const take of inListChunks(options.deleteForFiles.length, Math.floor(maxSqlVars / 4))) {
6481
+ const bucket = padToInBucket(options.deleteForFiles.slice(cursor, cursor + take));
6482
+ cursor += take;
6483
+ const ph = placeholders(bucket.length);
6484
+ if (ftsAvailable) {
6485
+ stmtFn(
6486
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6487
+ ).run(...bucket);
6488
+ }
6489
+ if (vectorsAvailable) {
6490
+ stmtFn(
6491
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6492
+ ).run(...bucket);
6493
+ }
5266
6494
  stmtFn(
5267
- `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5268
- ).run(...options.deleteForFiles);
6495
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
6496
+ ).run(...bucket);
6497
+ stmtFn(`DELETE FROM symbols WHERE file IN (${ph})`).run(...bucket);
5269
6498
  }
5270
- stmtFn(
5271
- `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5272
- ).run(...options.deleteForFiles);
5273
- stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5274
6499
  }
5275
6500
  const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5276
6501
  let nextId = allocateSymbolIds(totalSymbols);
@@ -5302,12 +6527,14 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
5302
6527
  text: buildIndexableText(s.name, s.signature, s.docComment)
5303
6528
  });
5304
6529
  }
5305
- vectorRows.push({
5306
- id,
5307
- vector: encodeVector(
5308
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5309
- )
5310
- });
6530
+ if (vectorsAvailable) {
6531
+ vectorRows.push({
6532
+ id,
6533
+ vector: encodeVector(
6534
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6535
+ )
6536
+ });
6537
+ }
5311
6538
  const inserted = { ...s, id };
5312
6539
  allInserted.push(inserted);
5313
6540
  insertedForEntry.push(inserted);
@@ -5399,9 +6626,8 @@ var CORE_TABLES_SQL = `
5399
6626
  signature TEXT NOT NULL DEFAULT '',
5400
6627
  doc_comment TEXT NOT NULL DEFAULT '',
5401
6628
  scope TEXT NOT NULL DEFAULT '',
5402
- text TEXT NOT NULL DEFAULT '',
5403
- file_fk TEXT NOT NULL
5404
- );
6629
+ text TEXT NOT NULL DEFAULT ''
6630
+ );
5405
6631
  `;
5406
6632
  var FILE_INDEX_SQL = [
5407
6633
  "CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
@@ -5412,7 +6638,6 @@ var SYMBOL_INDEX_SQL = [
5412
6638
  "CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
5413
6639
  "CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
5414
6640
  "CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
5415
- "CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)",
5416
6641
  "CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
5417
6642
  ];
5418
6643
  var REFS_TABLE_SQL = `
@@ -5487,11 +6712,12 @@ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5487
6712
  return stmtFn(base).all();
5488
6713
  }
5489
6714
  const out = [];
5490
- for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5491
- const chunk = onlyFiles.slice(i, i + maxSqlVars);
5492
- const placeholders = chunk.map(() => "?").join(",");
6715
+ let cursor = 0;
6716
+ for (const take of inListChunks(onlyFiles.length, maxSqlVars)) {
6717
+ const chunk = padToInBucket(onlyFiles.slice(cursor, cursor + take));
6718
+ cursor += take;
5493
6719
  out.push(
5494
- ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
6720
+ ...stmtFn(`${base} AND s.file IN (${placeholders(chunk.length)})`).all(...chunk)
5495
6721
  );
5496
6722
  }
5497
6723
  return out;
@@ -5562,16 +6788,18 @@ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVar
5562
6788
  )`
5563
6789
  );
5564
6790
  const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5565
- for (let i = 0; i < resolutions.length; i += chunkSize) {
5566
- const chunk = resolutions.slice(i, i + chunkSize);
5567
- const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
6791
+ let cursor = 0;
6792
+ for (const take of ladderChunkSizes(resolutions.length, chunkSize)) {
6793
+ const chunk = resolutions.slice(cursor, cursor + take);
6794
+ cursor += take;
6795
+ const valuesPh = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5568
6796
  const binds = [];
5569
6797
  for (const entry of chunk) {
5570
6798
  binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5571
6799
  }
5572
6800
  stmtFn(
5573
6801
  `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5574
- VALUES ${placeholders}`
6802
+ VALUES ${valuesPh}`
5575
6803
  ).run(...binds);
5576
6804
  }
5577
6805
  db.exec(
@@ -5608,9 +6836,11 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5608
6836
  const list = [...names].filter((name) => name.length > 0);
5609
6837
  if (list.length === 0) return 0;
5610
6838
  let total = 0;
5611
- for (let i = 0; i < list.length; i += maxSqlVars) {
5612
- const chunk = list.slice(i, i + maxSqlVars);
5613
- const placeholders = chunk.map(() => "?").join(",");
6839
+ let cursor = 0;
6840
+ for (const take of inListChunks(list.length, maxSqlVars)) {
6841
+ const chunk = padToInBucket(list.slice(cursor, cursor + take));
6842
+ cursor += take;
6843
+ const ph = placeholders(chunk.length);
5614
6844
  try {
5615
6845
  const result = stmtFn(
5616
6846
  `UPDATE refs
@@ -5619,16 +6849,16 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5619
6849
  SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5620
6850
  FROM symbols sym
5621
6851
  JOIN lang_family lf ON lf.lang = sym.lang
5622
- WHERE sym.name IN (${placeholders})
6852
+ WHERE sym.name IN (${ph})
5623
6853
  GROUP BY sym.name, lf.family
5624
6854
  UNION ALL
5625
6855
  SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5626
6856
  FROM symbols sym
5627
- WHERE sym.name IN (${placeholders})
6857
+ WHERE sym.name IN (${ph})
5628
6858
  GROUP BY sym.name
5629
6859
  ) AS s,
5630
6860
  lang_family AS rf
5631
- WHERE refs.to_name IN (${placeholders})
6861
+ WHERE refs.to_name IN (${ph})
5632
6862
  AND rf.lang = refs.lang
5633
6863
  AND s.name = refs.to_name
5634
6864
  AND s.family = rf.family`
@@ -5640,7 +6870,7 @@ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5640
6870
  SELECT sym.id FROM symbols sym
5641
6871
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5642
6872
  ORDER BY sym.id LIMIT 1
5643
- ) WHERE refs.to_name IN (${placeholders})
6873
+ ) WHERE refs.to_name IN (${ph})
5644
6874
  AND EXISTS (
5645
6875
  SELECT 1 FROM symbols sym
5646
6876
  WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
@@ -5697,7 +6927,7 @@ function buildWriterSearchWhere(query, filter) {
5697
6927
  const conditions = [];
5698
6928
  const values = [];
5699
6929
  let effectiveKind = filter?.kind;
5700
- if (filter?.lspKind !== void 0) {
6930
+ if (filter?.lspKind != null) {
5701
6931
  const mapped = lspKindToInternalKind(filter.lspKind);
5702
6932
  if (mapped !== null) {
5703
6933
  effectiveKind = mapped;
@@ -5776,7 +7006,7 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5776
7006
  );
5777
7007
  }
5778
7008
  let effectiveKind = filter?.kind;
5779
- if (filter?.lspKind !== void 0) {
7009
+ if (filter?.lspKind != null) {
5780
7010
  const mapped = lspKindToInternalKind(filter.lspKind);
5781
7011
  if (mapped === null) return { results: [], total: 0 };
5782
7012
  effectiveKind = mapped;
@@ -5813,15 +7043,14 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5813
7043
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5814
7044
  }
5815
7045
  const where = conditions.join(" AND ");
5816
- const countRows = stmtFn(
5817
- `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5818
- ).all(...values);
5819
- const total = countRows[0] ? Number(countRows[0].n) : 0;
5820
- if (total === 0) return { results: [], total: 0 };
5821
7046
  const bm25Rows = stmtFn(
5822
7047
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5823
7048
  -bm25(symbols_fts) AS score,
5824
- snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
7049
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet,
7050
+ -- Keep this uncorrelated: referencing outer columns turns it into
7051
+ -- a per-row subquery and defeats the one-count-per-statement win.
7052
+ (SELECT COUNT(*) FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
7053
+ WHERE ${where}) AS total_count
5825
7054
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5826
7055
  WHERE ${where}
5827
7056
  ORDER BY
@@ -5830,13 +7059,15 @@ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvaila
5830
7059
  ELSE 2 END,
5831
7060
  bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5832
7061
  LIMIT ?`
5833
- ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5834
- if (vectorsAvailable && bm25Rows.length > 0) {
7062
+ ).all(...values, ...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
7063
+ if (bm25Rows.length === 0) return { results: [], total: 0 };
7064
+ const total = Number(bm25Rows[0]?.total_count ?? 0);
7065
+ if (vectorsAvailable) {
5835
7066
  const queryVec = embedText(query);
5836
7067
  const candidateIds = bm25Rows.map((r) => r.id);
5837
- const placeholders = candidateIds.map(() => "?").join(",");
7068
+ const placeholders2 = candidateIds.map(() => "?").join(",");
5838
7069
  const vecRows = stmtFn(
5839
- `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
7070
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders2})`
5840
7071
  ).all(...candidateIds);
5841
7072
  const vecScores = vecRows.map((r) => ({
5842
7073
  id: r.symbol_id,
@@ -6024,9 +7255,9 @@ var IndexStore = class _IndexStore {
6024
7255
  }
6025
7256
  constructor(projectRoot, opts = {}) {
6026
7257
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6027
- fs8.mkdirSync(this.indexDir, { recursive: true });
7258
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6028
7259
  const Database = loadDatabaseSync();
6029
- this.db = new Database(path13.join(this.indexDir, DB_FILE2));
7260
+ this.db = new Database(path14.join(this.indexDir, DB_FILE2));
6030
7261
  applyIndexStorePragmas(this.db);
6031
7262
  this.initSchema();
6032
7263
  }
@@ -6155,7 +7386,11 @@ var IndexStore = class _IndexStore {
6155
7386
  );
6156
7387
  if (symbolCount !== ftsCount) {
6157
7388
  this.db.exec("DELETE FROM symbols_fts");
6158
- if (this.vectorsAvailable) this.db.exec("DELETE FROM symbol_vectors");
7389
+ if (vectorEmbeddingEnabled() && this.stmt(
7390
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbol_vectors'"
7391
+ ).get() !== void 0) {
7392
+ this.db.exec("DELETE FROM symbol_vectors");
7393
+ }
6159
7394
  const rows = this.stmt(
6160
7395
  "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
6161
7396
  ).all();
@@ -6174,8 +7409,13 @@ var IndexStore = class _IndexStore {
6174
7409
  this.ftsAvailable = false;
6175
7410
  }
6176
7411
  try {
6177
- this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
6178
- this.vectorsAvailable = true;
7412
+ if (vectorEmbeddingEnabled()) {
7413
+ this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
7414
+ this.vectorsAvailable = true;
7415
+ } else {
7416
+ this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
7417
+ this.vectorsAvailable = false;
7418
+ }
6179
7419
  } catch {
6180
7420
  this.vectorsAvailable = false;
6181
7421
  }
@@ -6210,14 +7450,22 @@ var IndexStore = class _IndexStore {
6210
7450
  }
6211
7451
  invalidateIncomingRefsForFiles(files) {
6212
7452
  if (files.length === 0) return /* @__PURE__ */ new Set();
6213
- const placeholders = files.map(() => "?").join(",");
6214
- const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
6215
- ...files
6216
- ).map((row) => row.name);
6217
- this.stmt(
6218
- `UPDATE refs SET to_id = NULL
6219
- WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6220
- ).run(...files);
7453
+ const names = [];
7454
+ let cursor = 0;
7455
+ for (const take of inListChunks(files.length, _IndexStore.MAX_SQL_VARS)) {
7456
+ const bucket = padToInBucket(files.slice(cursor, cursor + take));
7457
+ cursor += take;
7458
+ const ph = placeholders(bucket.length);
7459
+ for (const row of this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${ph})`).all(
7460
+ ...bucket
7461
+ )) {
7462
+ names.push(row.name);
7463
+ }
7464
+ this.stmt(
7465
+ `UPDATE refs SET to_id = NULL
7466
+ WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
7467
+ ).run(...bucket);
7468
+ }
6221
7469
  return new Set(names);
6222
7470
  }
6223
7471
  resolveRefsForNamesUnsafe(names) {
@@ -6251,6 +7499,14 @@ var IndexStore = class _IndexStore {
6251
7499
  if (this.ftsAvailable) {
6252
7500
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
6253
7501
  }
7502
+ if (this.vectorsAvailable) {
7503
+ vectorRows.push({
7504
+ id,
7505
+ vector: encodeVector(
7506
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
7507
+ )
7508
+ });
7509
+ }
6254
7510
  result.push({ ...s, id });
6255
7511
  }
6256
7512
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -6283,15 +7539,15 @@ var IndexStore = class _IndexStore {
6283
7539
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6284
7540
  if (this.ftsAvailable) {
6285
7541
  this.stmt(
6286
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7542
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6287
7543
  ).run(file);
6288
7544
  }
6289
7545
  if (this.vectorsAvailable) {
6290
7546
  this.stmt(
6291
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7547
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6292
7548
  ).run(file);
6293
7549
  }
6294
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7550
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6295
7551
  this.resolveRefsForNamesUnsafe(affectedNames);
6296
7552
  this.commitWriteTransaction(ownsTransaction);
6297
7553
  } catch (error) {
@@ -6308,18 +7564,18 @@ var IndexStore = class _IndexStore {
6308
7564
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
6309
7565
  if (this.ftsAvailable) {
6310
7566
  this.stmt(
6311
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7567
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6312
7568
  ).run(file);
6313
7569
  }
6314
7570
  if (this.vectorsAvailable) {
6315
7571
  this.stmt(
6316
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7572
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6317
7573
  ).run(file);
6318
7574
  }
6319
- this.stmt(
6320
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6321
- ).run(file);
6322
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7575
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7576
+ file
7577
+ );
7578
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
6323
7579
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
6324
7580
  this.resolveRefsForNamesUnsafe(affectedNames);
6325
7581
  this.commitWriteTransaction(ownsTransaction);
@@ -6423,6 +7679,10 @@ var IndexStore = class _IndexStore {
6423
7679
  getStats() {
6424
7680
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6425
7681
  }
7682
+ /** P2.5: minimal summary for search-response piggyback (see writer-admin). */
7683
+ getIndexSummary() {
7684
+ return getIndexSummaryWithStatement((sql) => this.stmt(sql));
7685
+ }
6426
7686
  setLastIndexed(ts2) {
6427
7687
  this.runWithRetry(() => {
6428
7688
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
@@ -6524,18 +7784,18 @@ var IndexStore = class _IndexStore {
6524
7784
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6525
7785
  if (this.ftsAvailable) {
6526
7786
  this.stmt(
6527
- "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
7787
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
6528
7788
  ).run(meta.file);
6529
7789
  }
6530
7790
  if (this.vectorsAvailable) {
6531
7791
  this.stmt(
6532
- "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
7792
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
6533
7793
  ).run(meta.file);
6534
7794
  }
6535
- this.stmt(
6536
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
6537
- ).run(meta.file);
6538
- this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
7795
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
7796
+ meta.file
7797
+ );
7798
+ this.stmt("DELETE FROM symbols WHERE file = ?").run(meta.file);
6539
7799
  this.stmt(
6540
7800
  `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
6541
7801
  VALUES (?, ?, ?, ?, ?, ?)
@@ -6567,6 +7827,27 @@ var IndexStore = class _IndexStore {
6567
7827
  } catch {
6568
7828
  }
6569
7829
  }
7830
+ /**
7831
+ * P4.14: best-effort WAL checkpoint for idle-time maintenance.
7832
+ *
7833
+ * `wal_autocheckpoint` is PASSIVE and only attempts work after a COMMIT —
7834
+ * once writes stop, nothing fires again, so the WAL keeps whatever frames
7835
+ * the last burst left. This probes with PASSIVE first (never blocks; busy=1
7836
+ * means readers still hold WAL snapshots) and only issues the TRUNCATE —
7837
+ * which resets index.db-wal to zero bytes — when the checkpointer can
7838
+ * proceed immediately. Callers run this on the daemon's single thread, so
7839
+ * never wait on readers here: busy means "retry at the next idle window".
7840
+ */
7841
+ checkpointWal() {
7842
+ try {
7843
+ const probe = this.db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get();
7844
+ if (Number(probe?.busy ?? 1) !== 0) return false;
7845
+ const done = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
7846
+ return Number(done?.busy ?? 1) === 0;
7847
+ } catch {
7848
+ return false;
7849
+ }
7850
+ }
6570
7851
  compactIfNeeded(options = {}) {
6571
7852
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6572
7853
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6652,7 +7933,9 @@ function resolveParallelBatch() {
6652
7933
  return indexParallelBatchSize(availableParallelism());
6653
7934
  }
6654
7935
  function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
6655
- return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
7936
+ const threshold = resolveWorkerPoolThreshold();
7937
+ if (threshold === 0) return false;
7938
+ return !isFrugalPerf() && candidateFileCount >= threshold && parseBatchCount > 1;
6656
7939
  }
6657
7940
  function yieldEventLoop() {
6658
7941
  return new Promise((resolve4) => setImmediate(resolve4));
@@ -6675,15 +7958,15 @@ var IndexSourceChangedError = class extends Error {
6675
7958
  name = "IndexSourceChangedError";
6676
7959
  };
6677
7960
  function isWithinProject(projectRoot, file) {
6678
- const rel = path14.relative(projectRoot, file);
6679
- return rel !== "" && !rel.startsWith(`..${path14.sep}`) && rel !== ".." && !path14.isAbsolute(rel);
7961
+ const rel = path15.relative(projectRoot, file);
7962
+ return rel !== "" && !rel.startsWith(`..${path15.sep}`) && rel !== ".." && !path15.isAbsolute(rel);
6680
7963
  }
6681
7964
  function isMissingPathError(err) {
6682
7965
  const code = err?.code;
6683
7966
  return code === "ENOENT" || code === "ENOTDIR";
6684
7967
  }
6685
7968
  function normalizeComparablePath(value) {
6686
- const resolved = path14.resolve(value);
7969
+ const resolved = path15.resolve(value);
6687
7970
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6688
7971
  }
6689
7972
  function gitOutput(projectRoot, args) {
@@ -6729,24 +8012,24 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6729
8012
  const record = statusRecords[i];
6730
8013
  if (!record) continue;
6731
8014
  const status = record.slice(0, 2);
6732
- const changedPath = path14.resolve(projectRoot, record.slice(3));
8015
+ const changedPath = path15.resolve(projectRoot, record.slice(3));
6733
8016
  dirty.add(changedPath);
6734
8017
  if (status.includes("D")) deleted.add(changedPath);
6735
8018
  if (status.includes("R") || status.includes("C")) {
6736
8019
  const source = statusRecords[++i];
6737
- if (source) dirty.add(path14.resolve(projectRoot, source));
8020
+ if (source) dirty.add(path15.resolve(projectRoot, source));
6738
8021
  }
6739
8022
  }
6740
8023
  const files = [];
6741
8024
  for (const relative3 of output.toString("utf8").split("\0")) {
6742
8025
  if (!relative3) continue;
6743
8026
  const portable = relative3.replace(/\\/g, "/");
6744
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path14.posix.basename(portable))) {
8027
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path15.posix.basename(portable))) {
6745
8028
  continue;
6746
8029
  }
6747
- const full = path14.resolve(projectRoot, relative3);
8030
+ const full = path15.resolve(projectRoot, relative3);
6748
8031
  if (deleted.has(full)) continue;
6749
- const ext = path14.extname(relative3).toLowerCase();
8032
+ const ext = path15.extname(relative3).toLowerCase();
6750
8033
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6751
8034
  }
6752
8035
  const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
@@ -6754,7 +8037,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6754
8037
  for (const dirtyFile of [...dirty].sort()) {
6755
8038
  if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6756
8039
  snapshot.update("\0").update(dirtyFile).update("\0");
6757
- snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
8040
+ snapshot.update(xxhash64String(await fs10.readFile(dirtyFile, "utf8")));
6758
8041
  }
6759
8042
  return {
6760
8043
  files,
@@ -6790,7 +8073,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6790
8073
  }
6791
8074
  let entries;
6792
8075
  try {
6793
- entries = await fs9.readdir(dir, { withFileTypes: true });
8076
+ entries = await fs10.readdir(dir, { withFileTypes: true });
6794
8077
  } catch (err) {
6795
8078
  complete = false;
6796
8079
  errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6799,14 +8082,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6799
8082
  dirCount++;
6800
8083
  for (const e of entries) {
6801
8084
  if (ignoreSet.has(e.name)) continue;
6802
- const full = path14.join(dir, e.name);
6803
- const rel = path14.relative(projectRoot, full).replace(/\\/g, "/");
8085
+ const full = path15.join(dir, e.name);
8086
+ const rel = path15.relative(projectRoot, full).replace(/\\/g, "/");
6804
8087
  if (e.isDirectory()) {
6805
8088
  if (isGitIgnored(rel, true)) continue;
6806
8089
  await walk(full);
6807
8090
  } else if (e.isFile()) {
6808
8091
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
6809
- const ext = path14.extname(e.name).toLowerCase();
8092
+ const ext = path15.extname(e.name).toLowerCase();
6810
8093
  if (indexableExts.has(ext) || detectLang(full) !== null) {
6811
8094
  results.push(full);
6812
8095
  }
@@ -6892,10 +8175,10 @@ async function runIndexerAtomic(store, opts) {
6892
8175
  let trustedUnchanged;
6893
8176
  let discoverySnapshotKey;
6894
8177
  if (opts.files && opts.files.length > 0) {
6895
- files = opts.files.map((f) => path14.resolve(projectRoot, f)).filter((f) => {
8178
+ files = opts.files.map((f) => path15.resolve(projectRoot, f)).filter((f) => {
6896
8179
  if (!isWithinProject(projectRoot, f)) return false;
6897
- const rel = path14.relative(projectRoot, f).replace(/\\/g, "/");
6898
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path14.basename(f)) && !isGitIgnored(rel, false);
8180
+ const rel = path15.relative(projectRoot, f).replace(/\\/g, "/");
8181
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path15.basename(f)) && !isGitIgnored(rel, false);
6899
8182
  });
6900
8183
  } else {
6901
8184
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -6928,7 +8211,6 @@ async function runIndexerAtomic(store, opts) {
6928
8211
  if (!meta || !trustedUnchanged.has(file)) return true;
6929
8212
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
6930
8213
  symbolsIndexed += meta.symbolCount;
6931
- filesIndexed++;
6932
8214
  filesSkipped++;
6933
8215
  filesPreSkipped++;
6934
8216
  return false;
@@ -6957,7 +8239,7 @@ async function runIndexerAtomic(store, opts) {
6957
8239
  async (file) => {
6958
8240
  let stat3;
6959
8241
  try {
6960
- stat3 = await fs9.stat(file, statOpts);
8242
+ stat3 = await fs10.stat(file, statOpts);
6961
8243
  } catch (e) {
6962
8244
  if (isAbortError(e)) throw e;
6963
8245
  return {
@@ -6984,7 +8266,7 @@ async function runIndexerAtomic(store, opts) {
6984
8266
  const meta = existingMeta.get(file);
6985
8267
  let content;
6986
8268
  try {
6987
- content = await fs9.readFile(file, { encoding: "utf8", signal });
8269
+ content = await fs10.readFile(file, { encoding: "utf8", signal });
6988
8270
  } catch (e) {
6989
8271
  if (isAbortError(e)) throw e;
6990
8272
  return {
@@ -7049,22 +8331,19 @@ async function runIndexerAtomic(store, opts) {
7049
8331
  }
7050
8332
  }
7051
8333
  if (!pool) {
7052
- await Promise.all(
7053
- toParse.map(async (item) => {
7054
- try {
7055
- const parsed = await parseFileContent(item.file, item.content, item.lang);
7056
- const settled = statReadParse[item.index];
7057
- if (settled.status === "fulfilled") {
7058
- settled.value.parsed = parsed;
7059
- }
7060
- } catch (e) {
7061
- const settled = statReadParse[item.index];
7062
- if (settled.status === "fulfilled") {
7063
- settled.value.error = `parse error: ${e instanceof Error ? e.message : String(e)}`;
7064
- }
7065
- }
7066
- })
8334
+ const parsedAll = await parseFilesContent(
8335
+ toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
7067
8336
  );
8337
+ for (let pi2 = 0; pi2 < parsedAll.length && pi2 < toParse.length; pi2++) {
8338
+ const settled = statReadParse[toParse[pi2].index];
8339
+ if (settled.status !== "fulfilled") continue;
8340
+ const slot = parsedAll[pi2];
8341
+ if (slot.result) {
8342
+ settled.value.parsed = slot.result;
8343
+ } else {
8344
+ settled.value.error = `parse error: ${slot.error ?? `no result for ${toParse[pi2].file}`}`;
8345
+ }
8346
+ }
7068
8347
  }
7069
8348
  }
7070
8349
  const batchEntries = [];
@@ -7090,7 +8369,6 @@ async function runIndexerAtomic(store, opts) {
7090
8369
  if (result.skippedMeta) {
7091
8370
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
7092
8371
  symbolsIndexed += result.skippedMeta.symbolCount;
7093
- filesIndexed++;
7094
8372
  filesSkipped++;
7095
8373
  const stored = existingMeta.get(file);
7096
8374
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
@@ -7115,7 +8393,6 @@ async function runIndexerAtomic(store, opts) {
7115
8393
  lastIndexed: Date.now(),
7116
8394
  contentHash: result.contentHash ?? ""
7117
8395
  });
7118
- filesIndexed++;
7119
8396
  filesEmpty++;
7120
8397
  }
7121
8398
  continue;
@@ -7129,7 +8406,6 @@ async function runIndexerAtomic(store, opts) {
7129
8406
  lastIndexed: Date.now(),
7130
8407
  contentHash: result.contentHash ?? ""
7131
8408
  });
7132
- filesIndexed++;
7133
8409
  filesEmpty++;
7134
8410
  continue;
7135
8411
  }
@@ -7259,7 +8535,7 @@ async function indexService(args, hooks = {}) {
7259
8535
  function searchService(args) {
7260
8536
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
7261
8537
  try {
7262
- return store.searchRanked(
8538
+ const result = store.searchRanked(
7263
8539
  args.query,
7264
8540
  {
7265
8541
  kind: args.kind,
@@ -7269,6 +8545,10 @@ function searchService(args) {
7269
8545
  },
7270
8546
  args.limit
7271
8547
  );
8548
+ if (result.total === 0) {
8549
+ return { ...result, indexSummary: store.getIndexSummary() };
8550
+ }
8551
+ return result;
7272
8552
  } finally {
7273
8553
  indexStorePool.release(store);
7274
8554
  }
@@ -7332,34 +8612,71 @@ function outgoingCallsService(args) {
7332
8612
  init_languages();
7333
8613
 
7334
8614
  // src/codebase-index/project-server-client.ts
7335
- import { spawn as spawn3 } from "node:child_process";
7336
- import * as fs12 from "node:fs";
8615
+ import { spawn as spawn4 } from "node:child_process";
8616
+ import * as fs13 from "node:fs";
7337
8617
  import * as net from "node:net";
7338
- import { StringDecoder } from "node:string_decoder";
7339
8618
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7340
8619
 
7341
8620
  // src/codebase-index/binary-frame.ts
7342
8621
  import { decode, encode } from "@msgpack/msgpack";
7343
8622
  var BINARY_FRAME_MAGIC = 87;
8623
+ var MAX_BINARY_FRAME_BYTES = 256 * 1024 * 1024;
8624
+ var MAX_INBOUND_BINARY_FRAME_BYTES = 64 * 1024 * 1024;
7344
8625
  function isBinaryFrame(firstByte) {
7345
8626
  return firstByte === BINARY_FRAME_MAGIC;
7346
8627
  }
7347
8628
  function encodeBinaryFrame(message) {
7348
- const payload = encode(message);
8629
+ const payload = encode(normalizeUndefined(message));
7349
8630
  const header = Buffer.allocUnsafe(5);
7350
8631
  header[0] = BINARY_FRAME_MAGIC;
7351
8632
  header.writeUInt32BE(payload.length, 1);
7352
8633
  return Buffer.concat([header, payload], 5 + payload.length);
7353
8634
  }
8635
+ function normalizeUndefined(value) {
8636
+ if (value instanceof Date) {
8637
+ const time = value.getTime();
8638
+ return Number.isNaN(time) ? null : value.toISOString();
8639
+ }
8640
+ if (value instanceof Map) return normalizeUndefined(Object.fromEntries(value));
8641
+ if (value instanceof Set) return normalizeUndefined([...value]);
8642
+ if (value instanceof Error) {
8643
+ return normalizeUndefined({ name: value.name, message: value.message, stack: value.stack });
8644
+ }
8645
+ if (value instanceof RegExp) return String(value);
8646
+ if (value instanceof URL) return value.toJSON();
8647
+ if (Buffer.isBuffer(value)) return { type: "Buffer", data: [...value] };
8648
+ if (Array.isArray(value)) return value.map((entry) => normalizeUndefined(entry));
8649
+ if (!isPlainObject(value)) return value;
8650
+ const out = {};
8651
+ for (const [key, entry] of Object.entries(value)) {
8652
+ if (entry === void 0) continue;
8653
+ out[key] = normalizeUndefined(entry);
8654
+ }
8655
+ return out;
8656
+ }
8657
+ function isPlainObject(value) {
8658
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
8659
+ const proto = Object.getPrototypeOf(value);
8660
+ return proto === Object.prototype || proto === null;
8661
+ }
7354
8662
  function decodeBinaryFrame(payload) {
7355
8663
  return decode(payload);
7356
8664
  }
8665
+ function encodeJsonFrame(message) {
8666
+ return `${JSON.stringify(normalizeUndefined(message))}
8667
+ `;
8668
+ }
8669
+
8670
+ // src/codebase-index/project-server-client-state.ts
8671
+ import * as fs12 from "node:fs";
8672
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8673
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
7357
8674
 
7358
8675
  // src/codebase-index/project-server-endpoint.ts
7359
8676
  import { createHash as createHash3 } from "node:crypto";
7360
- import * as fs10 from "node:fs";
7361
- import * as os3 from "node:os";
7362
- import * as path15 from "node:path";
8677
+ import * as fs11 from "node:fs";
8678
+ import * as os4 from "node:os";
8679
+ import * as path16 from "node:path";
7363
8680
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7364
8681
  var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
7365
8682
  var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
@@ -7368,21 +8685,21 @@ var buildIdCache;
7368
8685
  function projectIndexServerBuildId(entrypoint) {
7369
8686
  const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
7370
8687
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
7371
- const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path15.resolve(cleanHref);
8688
+ const file = cleanHref.startsWith("file:") ? fileURLToPath3(cleanHref) : path16.resolve(cleanHref);
7372
8689
  try {
7373
- const stat3 = fs10.statSync(file);
8690
+ const stat3 = fs11.statSync(file);
7374
8691
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7375
8692
  return buildIdCache.buildId;
7376
8693
  }
7377
- const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
8694
+ const buildId = createHash3("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
7378
8695
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7379
8696
  return buildId;
7380
8697
  } catch {
7381
- return `unreadable:${path15.basename(file)}`;
8698
+ return `unreadable:${path16.basename(file)}`;
7382
8699
  }
7383
8700
  }
7384
8701
  function normalizeLocalPath(value) {
7385
- const resolved = path15.resolve(value);
8702
+ const resolved = path16.resolve(value);
7386
8703
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7387
8704
  }
7388
8705
  function projectIndexServerKey(projectRoot, indexDir) {
@@ -7394,26 +8711,16 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
7394
8711
  if (process.platform === "win32") {
7395
8712
  return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
7396
8713
  }
7397
- return path15.join(os3.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
8714
+ return path16.join(os4.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
7398
8715
  }
7399
8716
  function projectIndexServerMetadataPath(projectRoot, indexDir) {
7400
- return path15.join(
7401
- path15.resolve(resolveIndexDir(projectRoot, indexDir)),
8717
+ return path16.join(
8718
+ path16.resolve(resolveIndexDir(projectRoot, indexDir)),
7402
8719
  PROJECT_INDEX_SERVER_METADATA_FILE
7403
8720
  );
7404
8721
  }
7405
8722
 
7406
- // src/codebase-index/project-server-protocol.ts
7407
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
7408
- function encodeProjectServerMessage(message) {
7409
- return `${JSON.stringify(message)}
7410
- `;
7411
- }
7412
-
7413
8723
  // src/codebase-index/project-server-client-state.ts
7414
- import * as fs11 from "node:fs";
7415
- import { fileURLToPath as fileURLToPath4 } from "node:url";
7416
- import { checkUnixSocketPath } from "@wrongstack/core/utils";
7417
8724
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
7418
8725
  var SERVER_START_TIMEOUT_MS = 1e4;
7419
8726
  var SERVER_CONTROL_TIMEOUT_MS = 5e3;
@@ -7444,7 +8751,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
7444
8751
  for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
7445
8752
  try {
7446
8753
  const url = new URL(rel, import.meta.url);
7447
- if (url.protocol === "file:" && fs11.existsSync(fileURLToPath4(url))) {
8754
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath4(url))) {
7448
8755
  builtUrl = url;
7449
8756
  break;
7450
8757
  }
@@ -7532,6 +8839,12 @@ function cancellationError(signal) {
7532
8839
  return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
7533
8840
  }
7534
8841
 
8842
+ // src/codebase-index/project-server-protocol.ts
8843
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
8844
+ function encodeProjectServerMessage(message) {
8845
+ return encodeJsonFrame(message);
8846
+ }
8847
+
7535
8848
  // src/codebase-index/project-server-client.ts
7536
8849
  var ProjectServerConnection = class {
7537
8850
  constructor(projectRoot, indexDir, endpoint) {
@@ -7544,11 +8857,13 @@ var ProjectServerConnection = class {
7544
8857
  indexDir;
7545
8858
  endpoint;
7546
8859
  socket = null;
7547
- buffer = "";
7548
- /** P6: binary frame buffer accumulates raw bytes when in binary mode. */
7549
- binaryBuffer = [];
7550
- /** P6: StringDecoder for safe UTF-8 multibyte handling in JSON mode. */
7551
- textDecoder = null;
8860
+ /**
8861
+ * Raw inbound bytes for the unified per-frame reader. Frames are sniffed
8862
+ * individually — JSON text (newline-terminated) or binary (magic 0x57) —
8863
+ * instead of latching a read mode, so a JSON broadcast between binary
8864
+ * frames cannot desynchronize the reader.
8865
+ */
8866
+ readBuffer = Buffer.alloc(0);
7552
8867
  /** P6: true once the server advertises binary support and client accepts. */
7553
8868
  useBinary = false;
7554
8869
  info = null;
@@ -7690,7 +9005,7 @@ var ProjectServerConnection = class {
7690
9005
  this.activity = null;
7691
9006
  this.health = null;
7692
9007
  this.useBinary = false;
7693
- this.binaryBuffer = [];
9008
+ this.readBuffer = Buffer.alloc(0);
7694
9009
  this.connectReject?.(new Error("codebase-index client disconnected"));
7695
9010
  this.connectResolve = null;
7696
9011
  this.connectReject = null;
@@ -7711,7 +9026,7 @@ var ProjectServerConnection = class {
7711
9026
  currentAuthToken() {
7712
9027
  if (this.authToken === void 0) {
7713
9028
  try {
7714
- const raw = fs12.readFileSync(
9029
+ const raw = fs13.readFileSync(
7715
9030
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
7716
9031
  "utf8"
7717
9032
  );
@@ -7818,10 +9133,8 @@ var ProjectServerConnection = class {
7818
9133
  this.info = null;
7819
9134
  this.activity = null;
7820
9135
  this.health = null;
7821
- this.buffer = "";
7822
- this.binaryBuffer = [];
9136
+ this.readBuffer = Buffer.alloc(0);
7823
9137
  this.useBinary = false;
7824
- this.textDecoder = null;
7825
9138
  return new Promise((resolve4, reject) => {
7826
9139
  const socket = net.createConnection(this.endpoint);
7827
9140
  this.socket = socket;
@@ -7851,18 +9164,47 @@ var ProjectServerConnection = class {
7851
9164
  socket.on("close", () => this.onClose(socket));
7852
9165
  });
7853
9166
  }
9167
+ /**
9168
+ * Unified per-frame reader. Each frame is sniffed by its first byte:
9169
+ * `0x57` ('W') → length-prefixed MessagePack binary, anything else →
9170
+ * newline-delimited JSON text. Sniffing per frame (instead of latching a
9171
+ * mode) is what makes mixed streams work: the server may interleave a JSON
9172
+ * `index-state` broadcast between binary responses, and an old JSON-only
9173
+ * server stays readable while `useBinary` is armed.
9174
+ *
9175
+ * Multibyte UTF-8 in JSON frames is safe: raw `0x0a` only occurs as the
9176
+ * JSON delimiter (inside JSON strings `\n` is escaped), so a complete line
9177
+ * is always complete UTF-8.
9178
+ */
7854
9179
  onData(socket, chunk) {
7855
9180
  if (socket !== this.socket) return;
7856
- if (this.useBinary) {
7857
- this.onBinaryData(socket, chunk);
7858
- return;
7859
- }
7860
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
7861
- this.buffer += this.textDecoder.write(chunk);
9181
+ this.readBuffer = this.readBuffer.length === 0 ? chunk : Buffer.concat([this.readBuffer, chunk]);
7862
9182
  while (true) {
7863
- const newline = this.buffer.indexOf("\n");
9183
+ if (this.readBuffer.length === 0) return;
9184
+ if (this.useBinary && isBinaryFrame(this.readBuffer[0])) {
9185
+ if (this.readBuffer.length < 5) return;
9186
+ const frameLen = this.readBuffer.readUInt32BE(1);
9187
+ if (frameLen > MAX_BINARY_FRAME_BYTES) {
9188
+ socket.destroy();
9189
+ this.transition("offline", { error: "binary frame length exceeds the IPC limit" });
9190
+ return;
9191
+ }
9192
+ if (this.readBuffer.length < 5 + frameLen) return;
9193
+ const payload = this.readBuffer.subarray(5, 5 + frameLen);
9194
+ this.readBuffer = this.readBuffer.subarray(5 + frameLen);
9195
+ let message2;
9196
+ try {
9197
+ message2 = decodeBinaryFrame(payload);
9198
+ } catch {
9199
+ socket.destroy(new Error("invalid binary codebase-index server response"));
9200
+ return;
9201
+ }
9202
+ this.onMessage(message2);
9203
+ continue;
9204
+ }
9205
+ const newline = this.readBuffer.indexOf(10);
7864
9206
  if (newline < 0) {
7865
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
9207
+ if (this.readBuffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
7866
9208
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7867
9209
  }
7868
9210
  return;
@@ -7871,8 +9213,8 @@ var ProjectServerConnection = class {
7871
9213
  socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
7872
9214
  return;
7873
9215
  }
7874
- const line = this.buffer.slice(0, newline);
7875
- this.buffer = this.buffer.slice(newline + 1);
9216
+ const line = this.readBuffer.subarray(0, newline).toString("utf8");
9217
+ this.readBuffer = this.readBuffer.subarray(newline + 1);
7876
9218
  if (!line) continue;
7877
9219
  let message;
7878
9220
  try {
@@ -7884,44 +9226,6 @@ var ProjectServerConnection = class {
7884
9226
  this.onMessage(message);
7885
9227
  }
7886
9228
  }
7887
- /**
7888
- * P6: Parse binary frames from the raw buffer.
7889
- *
7890
- * Each frame is: [1 byte magic 0x57] [4 bytes uint32 BE length] [payload].
7891
- * The magic byte distinguishes binary from JSON — a JSON frame's first byte
7892
- * is `{` (0x7B), so there is no ambiguity even in a mixed-mode buffer.
7893
- */
7894
- onBinaryData(socket, chunk) {
7895
- this.binaryBuffer.push(chunk);
7896
- const all = Buffer.concat(this.binaryBuffer);
7897
- let offset = 0;
7898
- while (offset + 5 <= all.length) {
7899
- if (!isBinaryFrame(all[offset])) {
7900
- this.useBinary = false;
7901
- this.buffer += all.subarray(offset).toString("utf8");
7902
- this.binaryBuffer = [];
7903
- return;
7904
- }
7905
- const frameLen = all.readUInt32BE(offset + 1);
7906
- if (frameLen > 256 * 1024 * 1024) {
7907
- socket.destroy();
7908
- this.transition("offline", { error: "binary frame length exceeds 256MB limit" });
7909
- return;
7910
- }
7911
- const totalLen = 5 + frameLen;
7912
- if (offset + totalLen > all.length) break;
7913
- const payload = all.subarray(offset + 5, offset + 5 + frameLen);
7914
- try {
7915
- const message = decodeBinaryFrame(payload);
7916
- this.onMessage(message);
7917
- } catch {
7918
- socket.destroy(new Error("invalid binary codebase-index server response"));
7919
- return;
7920
- }
7921
- offset += totalLen;
7922
- }
7923
- this.binaryBuffer = offset < all.length ? [all.subarray(offset)] : [];
7924
- }
7925
9229
  onMessage(message) {
7926
9230
  if (message.type === "hello") {
7927
9231
  if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
@@ -7941,7 +9245,7 @@ var ProjectServerConnection = class {
7941
9245
  }
7942
9246
  this.info = message;
7943
9247
  this.markResponsive();
7944
- if (message.binarySupported) this.useBinary = true;
9248
+ if (message.binarySupported && binaryFramingEnabled()) this.useBinary = true;
7945
9249
  this.transition("connected", { pid: message.pid });
7946
9250
  ensureHeartbeatLoop();
7947
9251
  this.connectResolve?.();
@@ -8027,13 +9331,13 @@ var ProjectServerConnection = class {
8027
9331
  if (!url) throw new Error("built codebase-index project server is unavailable");
8028
9332
  if (process.platform !== "win32") {
8029
9333
  try {
8030
- fs12.rmSync(this.endpoint, { force: true });
9334
+ fs13.rmSync(this.endpoint, { force: true });
8031
9335
  } catch {
8032
9336
  }
8033
9337
  }
8034
9338
  const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
8035
9339
  if (this.indexDir) args.push("--index-dir", this.indexDir);
8036
- const child = spawn3(process.execPath, args, {
9340
+ const child = spawn4(process.execPath, args, {
8037
9341
  detached: true,
8038
9342
  stdio: "ignore",
8039
9343
  windowsHide: true,
@@ -8051,8 +9355,8 @@ var ProjectServerConnection = class {
8051
9355
  process.kill(pid);
8052
9356
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
8053
9357
  try {
8054
- const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
8055
- if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
9358
+ const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf8"));
9359
+ if (metadata.pid === pid) fs13.rmSync(metadataPath, { force: true });
8056
9360
  } catch {
8057
9361
  }
8058
9362
  return true;
@@ -8064,6 +9368,10 @@ var ProjectServerConnection = class {
8064
9368
  var connections = /* @__PURE__ */ new Map();
8065
9369
  var MAX_CACHED_CONNECTIONS = 8;
8066
9370
  var heartbeatTimer;
9371
+ function binaryFramingEnabled() {
9372
+ const flag = process.env["WRONGSTACK_INDEX_BINARY"];
9373
+ return flag === "1" || flag === "true";
9374
+ }
8067
9375
  function forgetConnection(endpoint, connection) {
8068
9376
  if (connections.get(endpoint) === connection) connections.delete(endpoint);
8069
9377
  connection.close();
@@ -8153,7 +9461,7 @@ function resolveWorkerUrl() {
8153
9461
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8154
9462
  try {
8155
9463
  const url = new URL(rel, import.meta.url);
8156
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
9464
+ if (url.protocol === "file:" && fs14.existsSync(fileURLToPath6(url))) return url;
8157
9465
  } catch {
8158
9466
  }
8159
9467
  }
@@ -8480,7 +9788,7 @@ var writeTool = {
8480
9788
  required: ["path", "content"]
8481
9789
  },
8482
9790
  async execute(input, ctx, opts) {
8483
- return writeFile3(input, ctx, opts?.signal);
9791
+ return writeFile4(input, ctx, opts?.signal);
8484
9792
  },
8485
9793
  async *executeStream(input, ctx, opts) {
8486
9794
  const prepared = await prepareWrite(input, ctx);
@@ -8493,7 +9801,7 @@ var writeTool = {
8493
9801
  yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
8494
9802
  }
8495
9803
  };
8496
- async function writeFile3(input, ctx, signal) {
9804
+ async function writeFile4(input, ctx, signal) {
8497
9805
  return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
8498
9806
  }
8499
9807
  async function prepareWrite(input, ctx) {
@@ -8513,14 +9821,14 @@ async function prepareWrite(input, ctx) {
8513
9821
  let existed = false;
8514
9822
  let prev = "";
8515
9823
  try {
8516
- const stat3 = await fs14.stat(absPath);
9824
+ const stat3 = await fs15.stat(absPath);
8517
9825
  existed = stat3.isFile();
8518
9826
  if (existed) {
8519
9827
  if (!ctx.hasRead(absPath)) {
8520
- prev = await fs14.readFile(absPath, "utf8");
9828
+ prev = await fs15.readFile(absPath, "utf8");
8521
9829
  ctx.recordRead(absPath, stat3.mtimeMs, "write", sha256hex(prev));
8522
9830
  } else {
8523
- prev = await fs14.readFile(absPath, "utf8");
9831
+ prev = await fs15.readFile(absPath, "utf8");
8524
9832
  }
8525
9833
  }
8526
9834
  } catch (err) {
@@ -8541,9 +9849,9 @@ async function finishWrite(input, ctx, prepared, signal) {
8541
9849
  const rawDiff = prepared.existed ? unifiedDiff(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
8542
9850
  + (new file, ${content.split("\n").length} lines)`;
8543
9851
  const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES);
8544
- const stat3 = await fs14.stat(prepared.absPath);
9852
+ const stat3 = await fs15.stat(prepared.absPath);
8545
9853
  ctx.recordRead(prepared.absPath, stat3.mtimeMs, "write", sha256hex(content));
8546
- ctx.session.recordFileChange({
9854
+ ctx.session?.recordFileChange?.({
8547
9855
  path: prepared.absPath,
8548
9856
  action: prepared.existed ? "modified" : "created",
8549
9857
  before: prepared.existed ? prepared.prev : null,