@wrongstack/tools 0.309.1 → 0.310.1

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