@wrongstack/tools 0.302.2 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/builtin.d.ts +6 -0
  2. package/dist/builtin.js +3213 -686
  3. package/dist/codebase-index/binary-frame.d.ts +43 -0
  4. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +1 -0
  5. package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
  6. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +1 -0
  7. package/dist/codebase-index/content-hash.d.ts +66 -0
  8. package/dist/codebase-index/index.js +1597 -153
  9. package/dist/codebase-index/indexer.d.ts +6 -0
  10. package/dist/codebase-index/parser-worker-pool.d.ts +63 -0
  11. package/dist/codebase-index/parser-worker-script.d.ts +42 -0
  12. package/dist/codebase-index/project-server-protocol.d.ts +2 -0
  13. package/dist/codebase-index/project-server.js +1483 -104
  14. package/dist/codebase-index/schema.d.ts +18 -0
  15. package/dist/codebase-index/tree-sitter/queries.d.ts +48 -0
  16. package/dist/codebase-index/tree-sitter/util.d.ts +31 -0
  17. package/dist/codebase-index/tree-sitter/visitor.d.ts +47 -0
  18. package/dist/codebase-index/tree-sitter-parser.d.ts +58 -0
  19. package/dist/codebase-index/vector-search.d.ts +62 -0
  20. package/dist/codebase-index/worker-protocol.d.ts +2 -0
  21. package/dist/codebase-index/worker.js +1452 -73
  22. package/dist/codebase-index/writer-bulk-insert.d.ts +5 -0
  23. package/dist/codebase-index/writer-graph-reader.d.ts +39 -0
  24. package/dist/codebase-index/writer-schema.d.ts +9 -2
  25. package/dist/codebase-index/writer.d.ts +36 -0
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +3258 -727
  28. package/dist/kanban-contract-actions.d.ts +7 -0
  29. package/dist/kanban-task-inputs.d.ts +16 -2
  30. package/dist/kanban-tool-schema.d.ts +2 -2
  31. package/dist/kanban-tool-types.d.ts +39 -2
  32. package/dist/kanban.js +580 -193
  33. package/dist/pack.js +3212 -686
  34. package/dist/plan.d.ts +4 -1
  35. package/dist/plan.js +2701 -18
  36. package/dist/read.js +1559 -103
  37. package/dist/session-kanban.d.ts +94 -1
  38. package/dist/session-kanban.js +308 -44
  39. package/dist/task.d.ts +5 -4
  40. package/dist/task.js +2825 -138
  41. package/dist/todo.d.ts +10 -1
  42. package/dist/todo.js +2474 -30
  43. package/dist/tool-tier.js +3212 -686
  44. package/package.json +8 -4
@@ -2130,10 +2130,582 @@ var init_yaml_parser = __esm({
2130
2130
  }
2131
2131
  });
2132
2132
 
2133
+ // src/codebase-index/tree-sitter/queries.ts
2134
+ function getQueries(lang) {
2135
+ return LANG_QUERIES[lang] ?? DEFAULT_QUERIES;
2136
+ }
2137
+ function readFirstString(node) {
2138
+ if (!node) return null;
2139
+ if (node.type === "string_literal" || node.type === "alias") {
2140
+ return node.text.replace(/^"|"$/g, "");
2141
+ }
2142
+ const child = node.namedChild(0);
2143
+ return child ? readFirstString(child) : null;
2144
+ }
2145
+ var DEFAULT_QUERIES, LANG_QUERIES;
2146
+ var init_queries = __esm({
2147
+ "src/codebase-index/tree-sitter/queries.ts"() {
2148
+ "use strict";
2149
+ DEFAULT_QUERIES = {
2150
+ declKinds: {}
2151
+ };
2152
+ LANG_QUERIES = {
2153
+ // ─── C family ──────────────────────────────────────────────────────────────
2154
+ c: {
2155
+ declKinds: {
2156
+ function_definition: "function",
2157
+ declaration: "function",
2158
+ // K&R-style `int foo(...)` ambiguous w/ local var; the visitor prefers the function branch when the declarator field is present
2159
+ struct_specifier: "struct",
2160
+ union_specifier: "struct",
2161
+ enum_specifier: "enum",
2162
+ type_definition: "type",
2163
+ // `typedef … X;`
2164
+ preproc_def: "const"
2165
+ // `#define NAME …`
2166
+ },
2167
+ nameField: {
2168
+ function_definition: "declarator",
2169
+ declaration: "declarator",
2170
+ struct_specifier: "name",
2171
+ enum_specifier: "name",
2172
+ type_definition: "declarator",
2173
+ preproc_def: "name"
2174
+ },
2175
+ scopeNodes: /* @__PURE__ */ new Set([
2176
+ "translation_unit",
2177
+ "function_definition",
2178
+ "struct_specifier",
2179
+ "union_specifier",
2180
+ "enum_specifier"
2181
+ ])
2182
+ },
2183
+ cpp: {
2184
+ declKinds: {
2185
+ function_definition: "function",
2186
+ template_declaration: "function",
2187
+ // `template<typename T> …`
2188
+ class_specifier: "class",
2189
+ struct_specifier: "struct",
2190
+ union_specifier: "struct",
2191
+ enum_specifier: "enum",
2192
+ namespace_definition: "namespace",
2193
+ type_definition: "type"
2194
+ },
2195
+ nameField: {
2196
+ function_definition: "declarator",
2197
+ template_declaration: "name",
2198
+ class_specifier: "name",
2199
+ struct_specifier: "name",
2200
+ enum_specifier: "name",
2201
+ namespace_definition: "name",
2202
+ type_definition: "declarator"
2203
+ },
2204
+ scopeNodes: /* @__PURE__ */ new Set([
2205
+ "translation_unit",
2206
+ "function_definition",
2207
+ "class_specifier",
2208
+ "struct_specifier",
2209
+ "union_specifier",
2210
+ "enum_specifier",
2211
+ "namespace_definition"
2212
+ ])
2213
+ },
2214
+ java: {
2215
+ declKinds: {
2216
+ class_declaration: "class",
2217
+ interface_declaration: "interface",
2218
+ enum_declaration: "enum",
2219
+ record_declaration: "class",
2220
+ annotation_type_declaration: "interface",
2221
+ method_declaration: "method",
2222
+ constructor_declaration: "method",
2223
+ field_declaration: "property"
2224
+ },
2225
+ nameField: {
2226
+ class_declaration: "name",
2227
+ interface_declaration: "name",
2228
+ enum_declaration: "name",
2229
+ record_declaration: "name",
2230
+ annotation_type_declaration: "name",
2231
+ method_declaration: "name",
2232
+ constructor_declaration: "name"
2233
+ },
2234
+ // `field_declaration` has no single `name` field — it carries a list of
2235
+ // variable declarators. We emit one Symbol per node using the first
2236
+ // identifier-shaped named child (see `extractName` fallback in
2237
+ // `visitor.ts`). `int a, b, c;` therefore indexes only `a` — splitting
2238
+ // multi-declarator fields into separate Symbols is a separate refactor
2239
+ // that needs the visitor to know it has multiple names per node, and no
2240
+ // current test relies on it.
2241
+ scopeNodes: /* @__PURE__ */ new Set([
2242
+ "program",
2243
+ "class_declaration",
2244
+ "interface_declaration",
2245
+ "enum_declaration",
2246
+ "record_declaration"
2247
+ ])
2248
+ },
2249
+ csharp: {
2250
+ // C# 10+ `namespace Foo.Bar;` produces this node type. The legacy block
2251
+ // form `namespace Foo.Bar { ... }` produces `namespace_declaration`. Both
2252
+ // carry a `qualified_name` child whose text already includes the dots.
2253
+ // `using_directive` is intentionally not a declaration. Imports are
2254
+ // extracted separately; indexing a using directive as a namespace makes
2255
+ // the resolver bind it to its own source file before the real declaration.
2256
+ declKinds: {
2257
+ file_scoped_namespace_declaration: "namespace",
2258
+ class_declaration: "class",
2259
+ interface_declaration: "interface",
2260
+ struct_declaration: "struct",
2261
+ enum_declaration: "enum",
2262
+ record_declaration: "class",
2263
+ method_declaration: "method",
2264
+ constructor_declaration: "method",
2265
+ property_declaration: "property",
2266
+ field_declaration: "property",
2267
+ namespace_declaration: "namespace"
2268
+ },
2269
+ // Custom name extractor: take the full dotted name verbatim.
2270
+ nameExtractor: (node) => {
2271
+ const inner = node.namedChild(0);
2272
+ if (inner && (inner.type === "qualified_name" || inner.type === "name")) {
2273
+ return inner.text;
2274
+ }
2275
+ return null;
2276
+ },
2277
+ scopeNodes: /* @__PURE__ */ new Set([
2278
+ "compilation_unit",
2279
+ "namespace_declaration",
2280
+ "class_declaration",
2281
+ "interface_declaration",
2282
+ "struct_declaration",
2283
+ "enum_declaration",
2284
+ "record_declaration"
2285
+ ])
2286
+ },
2287
+ php: {
2288
+ declKinds: {
2289
+ function_definition: "function",
2290
+ method_declaration: "method",
2291
+ class_declaration: "class",
2292
+ interface_declaration: "interface",
2293
+ trait_declaration: "class",
2294
+ enum_declaration: "enum",
2295
+ namespace_definition: "namespace"
2296
+ },
2297
+ nameField: {
2298
+ function_definition: "name",
2299
+ method_declaration: "name",
2300
+ class_declaration: "name",
2301
+ interface_declaration: "name",
2302
+ trait_declaration: "name",
2303
+ enum_declaration: "name",
2304
+ namespace_declaration: "name"
2305
+ },
2306
+ scopeNodes: /* @__PURE__ */ new Set([
2307
+ "program",
2308
+ "namespace_definition",
2309
+ "class_declaration",
2310
+ "interface_declaration",
2311
+ "trait_declaration",
2312
+ "enum_declaration"
2313
+ ])
2314
+ },
2315
+ // ─── Scripting / mobile ────────────────────────────────────────────────────
2316
+ ruby: {
2317
+ declKinds: {
2318
+ method: "function",
2319
+ singleton_method: "method",
2320
+ class: "class",
2321
+ module: "namespace",
2322
+ constant: "const"
2323
+ },
2324
+ nameField: {
2325
+ method: "name",
2326
+ singleton_method: "name",
2327
+ class: "name",
2328
+ module: "name",
2329
+ constant: "name"
2330
+ },
2331
+ scopeNodes: /* @__PURE__ */ new Set(["program", "class", "module", "singleton_method", "method"])
2332
+ },
2333
+ swift: {
2334
+ declKinds: {
2335
+ function_declaration: "function",
2336
+ class_declaration: "class",
2337
+ struct_declaration: "struct",
2338
+ enum_declaration: "enum",
2339
+ protocol_declaration: "interface",
2340
+ actor_declaration: "class",
2341
+ extension_declaration: "class",
2342
+ initializer: "method",
2343
+ property_declaration: "property"
2344
+ },
2345
+ nameField: {
2346
+ function_declaration: "name",
2347
+ class_declaration: "name",
2348
+ struct_declaration: "name",
2349
+ enum_declaration: "name",
2350
+ protocol_declaration: "name",
2351
+ actor_declaration: "name",
2352
+ extension_declaration: "name",
2353
+ initializer: "name",
2354
+ property_declaration: "name"
2355
+ },
2356
+ scopeNodes: /* @__PURE__ */ new Set([
2357
+ "source_file",
2358
+ "class_declaration",
2359
+ "struct_declaration",
2360
+ "enum_declaration",
2361
+ "protocol_declaration",
2362
+ "actor_declaration",
2363
+ "extension_declaration"
2364
+ ])
2365
+ },
2366
+ kotlin: {
2367
+ declKinds: {
2368
+ class_declaration: "class",
2369
+ object_declaration: "class",
2370
+ interface_declaration: "interface",
2371
+ function_declaration: "function",
2372
+ property_declaration: "property",
2373
+ type_alias: "type"
2374
+ },
2375
+ nameField: {
2376
+ class_declaration: "name",
2377
+ object_declaration: "name",
2378
+ interface_declaration: "name",
2379
+ function_declaration: "name",
2380
+ property_declaration: "name",
2381
+ type_alias: "name"
2382
+ },
2383
+ scopeNodes: /* @__PURE__ */ new Set([
2384
+ "source_file",
2385
+ "class_declaration",
2386
+ "object_declaration",
2387
+ "interface_declaration",
2388
+ "function_declaration"
2389
+ ])
2390
+ },
2391
+ elixir: {
2392
+ declKinds: {
2393
+ // `def foo`, `defp foo`, `defmacro foo`, `macrop foo` all surface as
2394
+ // `call` nodes in the tree-sitter grammar — there is no
2395
+ // `function_definition`. The `nameExtractor` walks the call's
2396
+ // children to pick the right sibling identifier.
2397
+ call: "function",
2398
+ module: "namespace"
2399
+ },
2400
+ nameExtractor: (node) => {
2401
+ if (node.type === "module") {
2402
+ const aliasNode = node.childForFieldName("alias");
2403
+ return readFirstString(aliasNode) ?? null;
2404
+ }
2405
+ if (node.type !== "call") return null;
2406
+ const first = node.namedChild(0);
2407
+ if (!first) return null;
2408
+ const target = first.text;
2409
+ if (target !== "def" && target !== "defp" && target !== "defmacro" && target !== "defp_macro" && target !== "macrop" && target !== "defprotocol" && target !== "defguard" && target !== "defguardp") {
2410
+ return null;
2411
+ }
2412
+ const nameNode = node.namedChild(1);
2413
+ return nameNode?.text ?? null;
2414
+ },
2415
+ scopeNodes: /* @__PURE__ */ new Set(["source", "module"])
2416
+ },
2417
+ shell: {
2418
+ declKinds: {
2419
+ function_definition: "function"
2420
+ },
2421
+ nameField: { function_definition: "name" },
2422
+ scopeNodes: /* @__PURE__ */ new Set(["program", "function_definition"])
2423
+ }
2424
+ };
2425
+ }
2426
+ });
2427
+
2428
+ // src/codebase-index/tree-sitter/util.ts
2429
+ function lineColAt2(offsets, index) {
2430
+ let low = 0;
2431
+ let high = offsets.length;
2432
+ while (low < high) {
2433
+ const mid = low + high >>> 1;
2434
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
2435
+ else high = mid;
2436
+ }
2437
+ const lastNl = low > 0 ? offsets[low - 1] ?? -1 : -1;
2438
+ return { line: low + 1, col: index - lastNl };
2439
+ }
2440
+ function newlineOffsets3(content) {
2441
+ const offsets = [];
2442
+ for (let i = 0; i < content.length; i++) {
2443
+ if (content.charCodeAt(i) === 10) offsets.push(i);
2444
+ }
2445
+ return offsets;
2446
+ }
2447
+ var TREE_SITTER_MAX_FILE_CHARS, TREE_SITTER_MAX_SYMBOLS;
2448
+ var init_util = __esm({
2449
+ "src/codebase-index/tree-sitter/util.ts"() {
2450
+ "use strict";
2451
+ TREE_SITTER_MAX_FILE_CHARS = 512 * 1024;
2452
+ TREE_SITTER_MAX_SYMBOLS = 500;
2453
+ }
2454
+ });
2455
+
2456
+ // src/codebase-index/tree-sitter/visitor.ts
2457
+ function visitTree(tree, content, file, lang, queries) {
2458
+ const boundedContent = content.length > TREE_SITTER_MAX_FILE_CHARS ? content.slice(0, TREE_SITTER_MAX_FILE_CHARS) : content;
2459
+ const nlOffsets = newlineOffsets3(boundedContent);
2460
+ const symbols = [];
2461
+ const scopeStack = [];
2462
+ function visit(node, depth) {
2463
+ if (symbols.length >= TREE_SITTER_MAX_SYMBOLS) return;
2464
+ if (node.isMissing || node.isError) {
2465
+ } else {
2466
+ const kind = queries.declKinds[node.type];
2467
+ if (kind) {
2468
+ const emitted = emitSymbol(
2469
+ node,
2470
+ kind,
2471
+ file,
2472
+ lang,
2473
+ scopeStack,
2474
+ boundedContent,
2475
+ nlOffsets,
2476
+ queries
2477
+ );
2478
+ if (emitted) symbols.push(emitted);
2479
+ }
2480
+ }
2481
+ const pushesScope = queries.scopeNodes?.has(node.type) ?? false;
2482
+ const pushIdx = pushesScope ? pushScope(scopeStack, node, queries) : -1;
2483
+ if (queries.skipNamedChildren) {
2484
+ if (pushIdx !== -1) scopeStack.pop();
2485
+ return;
2486
+ }
2487
+ for (const child of node.namedChildren) {
2488
+ visit(child, depth + 1);
2489
+ if (symbols.length >= TREE_SITTER_MAX_SYMBOLS) {
2490
+ if (pushIdx !== -1) scopeStack.pop();
2491
+ return;
2492
+ }
2493
+ }
2494
+ if (pushIdx !== -1) scopeStack.pop();
2495
+ }
2496
+ visit(tree.rootNode, 0);
2497
+ return { symbols };
2498
+ }
2499
+ function pushScope(scopeStack, node, queries) {
2500
+ const name = extractName(node, queries);
2501
+ if (!name) return -1;
2502
+ scopeStack.push(name);
2503
+ return scopeStack.length - 1;
2504
+ }
2505
+ function extractName(node, queries) {
2506
+ if (queries.nameExtractor) {
2507
+ const extracted = queries.nameExtractor(node);
2508
+ if (extracted) return extracted;
2509
+ }
2510
+ const fieldName = queries.nameField?.[node.type] ?? "name";
2511
+ const field = node.childForFieldName(fieldName);
2512
+ if (field) {
2513
+ if (IDENTIFIER_NODE_TYPES.has(field.type)) {
2514
+ return field.text;
2515
+ }
2516
+ const inner = field.childForFieldName("name") ?? field.namedChild(0);
2517
+ if (inner && IDENTIFIER_NODE_TYPES.has(inner.type)) {
2518
+ return inner.text;
2519
+ }
2520
+ }
2521
+ for (let i = 0; i < node.namedChildCount; i++) {
2522
+ const child = node.namedChild(i);
2523
+ if (child && IDENTIFIER_NODE_TYPES.has(child.type)) {
2524
+ return child.text;
2525
+ }
2526
+ }
2527
+ return null;
2528
+ }
2529
+ function emitSymbol(node, kind, file, lang, scopeStack, content, nlOffsets, queries) {
2530
+ const name = extractName(node, queries);
2531
+ if (!name) return null;
2532
+ const pos = node.startIndex;
2533
+ const { line, col } = lineColAt2(nlOffsets, pos);
2534
+ const end = Math.min(node.endIndex, content.length);
2535
+ const signature = content.slice(pos, end).replace(/\s+/g, " ").trim().slice(0, 500);
2536
+ const scope = scopeStack.join(".");
2537
+ const text = [name, signature].filter(Boolean).join(" | ").trim().slice(0, 1e3);
2538
+ return {
2539
+ id: 0,
2540
+ // caller assigns during bulk insertion
2541
+ lang,
2542
+ kind,
2543
+ name: name.slice(0, 200),
2544
+ file,
2545
+ line,
2546
+ col,
2547
+ signature,
2548
+ docComment: "",
2549
+ // doc-comment extraction lands with ref emission on Day 4
2550
+ scope,
2551
+ text
2552
+ };
2553
+ }
2554
+ var IDENTIFIER_NODE_TYPES;
2555
+ var init_visitor = __esm({
2556
+ "src/codebase-index/tree-sitter/visitor.ts"() {
2557
+ "use strict";
2558
+ init_util();
2559
+ IDENTIFIER_NODE_TYPES = /* @__PURE__ */ new Set([
2560
+ "identifier",
2561
+ "simple_identifier",
2562
+ "type_identifier",
2563
+ "field_identifier",
2564
+ "property_identifier",
2565
+ "name",
2566
+ "word",
2567
+ "variable_name",
2568
+ "constant",
2569
+ "sym"
2570
+ ]);
2571
+ }
2572
+ });
2573
+
2574
+ // src/codebase-index/tree-sitter-parser.ts
2575
+ var tree_sitter_parser_exports = {};
2576
+ __export(tree_sitter_parser_exports, {
2577
+ __smokeRootType: () => __smokeRootType,
2578
+ getGrammarWasmPath: () => getGrammarWasmPath,
2579
+ isTreeSitterSupported: () => isTreeSitterSupported,
2580
+ loadTreeSitterLanguage: () => loadTreeSitterLanguage,
2581
+ parseSymbols: () => parseSymbols8
2582
+ });
2583
+ import * as path12 from "node:path";
2584
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2585
+ function optInEnabled(env) {
2586
+ return process.env[env] === "1" || process.env[env] === "true";
2587
+ }
2588
+ function getRuntime() {
2589
+ if (!runtimePromise) {
2590
+ runtimePromise = (async () => {
2591
+ const mod = await import("web-tree-sitter");
2592
+ const init = () => mod.Parser.init({ locateFile: () => RUNTIME_WASM });
2593
+ return { Parser: mod.Parser, Language: mod.Language, init };
2594
+ })();
2595
+ }
2596
+ return runtimePromise;
2597
+ }
2598
+ async function loadLanguage(lang) {
2599
+ const existing = languageCache.get(lang);
2600
+ if (existing) return existing;
2601
+ const promise = (async () => {
2602
+ const grammarName = resolveGrammarName(lang);
2603
+ if (!grammarName) {
2604
+ throw new Error(`tree-sitter: no grammar registered for lang "${lang}"`);
2605
+ }
2606
+ const wasmPath = path12.join(WASM_DIR, grammarName, `tree-sitter-${grammarName}.wasm`);
2607
+ const { Language, init } = await getRuntime();
2608
+ await init();
2609
+ const languageObj = await Language.load(wasmPath);
2610
+ return { lang, Language: languageObj };
2611
+ })();
2612
+ languageCache.set(lang, promise);
2613
+ return promise;
2614
+ }
2615
+ function resolveGrammarName(lang) {
2616
+ if (lang === "go" && optInEnabled(GO_OPT_IN)) return "go";
2617
+ if (lang === "py" && optInEnabled(PY_OPT_IN)) return "python";
2618
+ if (lang === "rs" && optInEnabled(RS_OPT_IN)) return "rust";
2619
+ return LANG_TO_GRAMMAR[lang];
2620
+ }
2621
+ function isTreeSitterSupported(lang) {
2622
+ return resolveGrammarName(lang) !== void 0;
2623
+ }
2624
+ function getGrammarWasmPath(lang) {
2625
+ const name = resolveGrammarName(lang);
2626
+ if (!name) return void 0;
2627
+ return path12.join(WASM_DIR, name, `tree-sitter-${name}.wasm`);
2628
+ }
2629
+ async function parseSymbols8(opts) {
2630
+ const { file, content, lang } = opts;
2631
+ if (!isTreeSitterSupported(lang)) {
2632
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
2633
+ }
2634
+ try {
2635
+ const { Parser } = await getRuntime();
2636
+ const cached = await loadLanguage(lang);
2637
+ const parser = new Parser();
2638
+ parser.setLanguage(cached.Language);
2639
+ const tree = parser.parse(content);
2640
+ if (!tree) {
2641
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
2642
+ }
2643
+ const { symbols } = visitTree(tree, content, file, lang, getQueries(lang));
2644
+ parser.delete();
2645
+ tree.delete();
2646
+ return { file, lang, symbols, refs: [], mtimeMs: Date.now() };
2647
+ } catch {
2648
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
2649
+ }
2650
+ }
2651
+ async function loadTreeSitterLanguage(lang) {
2652
+ const cached = await loadLanguage(lang);
2653
+ return cached.Language;
2654
+ }
2655
+ async function __smokeRootType(opts) {
2656
+ if (!isTreeSitterSupported(opts.lang)) {
2657
+ throw new Error(`tree-sitter: no grammar registered for lang "${opts.lang}"`);
2658
+ }
2659
+ const { Parser } = await getRuntime();
2660
+ const cached = await loadLanguage(opts.lang);
2661
+ const parser = new Parser();
2662
+ parser.setLanguage(cached.Language);
2663
+ let tree = null;
2664
+ try {
2665
+ tree = parser.parse(opts.content);
2666
+ if (!tree) throw new Error("tree-sitter: parser.parse returned null");
2667
+ return tree.rootNode.type;
2668
+ } finally {
2669
+ tree?.delete();
2670
+ parser.delete();
2671
+ }
2672
+ }
2673
+ var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
2674
+ var init_tree_sitter_parser = __esm({
2675
+ "src/codebase-index/tree-sitter-parser.ts"() {
2676
+ "use strict";
2677
+ init_queries();
2678
+ init_visitor();
2679
+ WASM_DIR = fileURLToPath3(new URL("./wasm/", import.meta.url));
2680
+ RUNTIME_WASM = path12.join(WASM_DIR, "tree-sitter-runtime.wasm");
2681
+ LANG_TO_GRAMMAR = {
2682
+ c: "c",
2683
+ cpp: "cpp",
2684
+ java: "java",
2685
+ csharp: "c_sharp",
2686
+ // tree-sitter directory uses underscore
2687
+ php: "php",
2688
+ ruby: "ruby",
2689
+ swift: "swift",
2690
+ kotlin: "kotlin",
2691
+ shell: "bash",
2692
+ // we treat `.sh` / `.bash` / `.zsh` via the bash grammar
2693
+ // Go/Python/Rust are opt-in only (see goOptIn / pyOptIn / rsOptIn below)
2694
+ elixir: "elixir"
2695
+ };
2696
+ GO_OPT_IN = "WRONGSTACK_USE_TS_GO";
2697
+ PY_OPT_IN = "WRONGSTACK_USE_TS_PY";
2698
+ RS_OPT_IN = "WRONGSTACK_USE_TS_RS";
2699
+ runtimePromise = null;
2700
+ languageCache = /* @__PURE__ */ new Map();
2701
+ }
2702
+ });
2703
+
2133
2704
  // src/codebase-index/project-server-client.ts
2134
2705
  import { spawn } from "node:child_process";
2135
2706
  import * as fs5 from "node:fs";
2136
2707
  import * as net from "node:net";
2708
+ import { StringDecoder } from "node:string_decoder";
2137
2709
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2138
2710
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
2139
2711
 
@@ -2220,6 +2792,23 @@ function resetIndexCircuitBreaker() {
2220
2792
  indexCircuitBreaker.reset();
2221
2793
  }
2222
2794
 
2795
+ // src/codebase-index/binary-frame.ts
2796
+ import { decode, encode } from "@msgpack/msgpack";
2797
+ var BINARY_FRAME_MAGIC = 87;
2798
+ function isBinaryFrame(firstByte) {
2799
+ return firstByte === BINARY_FRAME_MAGIC;
2800
+ }
2801
+ function encodeBinaryFrame(message) {
2802
+ const payload = encode(message);
2803
+ const header = Buffer.allocUnsafe(5);
2804
+ header[0] = BINARY_FRAME_MAGIC;
2805
+ header.writeUInt32BE(payload.length, 1);
2806
+ return Buffer.concat([header, payload], 5 + payload.length);
2807
+ }
2808
+ function decodeBinaryFrame(payload) {
2809
+ return decode(payload);
2810
+ }
2811
+
2223
2812
  // src/codebase-index/project-server-endpoint.ts
2224
2813
  import { createHash } from "node:crypto";
2225
2814
  import * as fs4 from "node:fs";
@@ -2495,7 +3084,7 @@ function getMetadataWithStatement(stmt, key) {
2495
3084
  }
2496
3085
  function getFileMetaWithStatement(stmt, file) {
2497
3086
  const rows = stmt(
2498
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files WHERE file = ?"
3087
+ "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
2499
3088
  ).all(file);
2500
3089
  const r = rows[0];
2501
3090
  if (!r) return null;
@@ -2504,16 +3093,20 @@ function getFileMetaWithStatement(stmt, file) {
2504
3093
  lang: r.lang,
2505
3094
  mtimeMs: r.mtime_ms,
2506
3095
  symbolCount: r.symbol_count,
2507
- lastIndexed: r.last_indexed
3096
+ lastIndexed: r.last_indexed,
3097
+ contentHash: r.content_hash
2508
3098
  };
2509
3099
  }
2510
3100
  function getAllFileMetasWithStatement(stmt) {
2511
- return stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
3101
+ return stmt(
3102
+ "SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files"
3103
+ ).all().map((r) => ({
2512
3104
  file: r.file,
2513
3105
  lang: r.lang,
2514
3106
  mtimeMs: r.mtime_ms,
2515
3107
  symbolCount: r.symbol_count,
2516
- lastIndexed: r.last_indexed
3108
+ lastIndexed: r.last_indexed,
3109
+ contentHash: r.content_hash
2517
3110
  }));
2518
3111
  }
2519
3112
  function getIndexDbSizeBytes(indexDir) {
@@ -2567,6 +3160,18 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
2567
3160
  insert.run(...binds);
2568
3161
  }
2569
3162
  }
3163
+ function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
3164
+ if (rows.length === 0) return;
3165
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 2));
3166
+ for (let i = 0; i < rows.length; i += chunkSize) {
3167
+ const chunk = rows.slice(i, i + chunkSize);
3168
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
3169
+ const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders}`);
3170
+ const binds = [];
3171
+ for (const r of chunk) binds.push(r.id, r.vector);
3172
+ insert.run(...binds);
3173
+ }
3174
+ }
2570
3175
  function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2571
3176
  if (refs.length === 0) return;
2572
3177
  const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
@@ -3173,6 +3778,171 @@ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
3173
3778
  const calls = rows.map(mapCallSiteRow).slice(0, limit);
3174
3779
  return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
3175
3780
  }
3781
+ function runCteWithSeeds(stmt, seedIds, buildSql) {
3782
+ if (seedIds.length <= 900) {
3783
+ const ph = seedIds.map(() => "?").join(",");
3784
+ return stmt(buildSql(ph)).all(...seedIds);
3785
+ }
3786
+ stmt("DROP TABLE IF EXISTS _cte_seeds").run();
3787
+ try {
3788
+ stmt("CREATE TEMP TABLE _cte_seeds (id INTEGER PRIMARY KEY)").run();
3789
+ for (let i = 0; i < seedIds.length; i += 500) {
3790
+ const chunk = seedIds.slice(i, i + 500);
3791
+ const ph = chunk.map(() => "(?)").join(",");
3792
+ stmt(`INSERT OR IGNORE INTO _cte_seeds (id) VALUES ${ph}`).run(...chunk);
3793
+ }
3794
+ return stmt(buildSql("SELECT id FROM _cte_seeds")).all();
3795
+ } finally {
3796
+ stmt("DROP TABLE IF EXISTS _cte_seeds").run();
3797
+ }
3798
+ }
3799
+ function findTransitiveIncomingCallsByName(stmt, symbolName, file, limit) {
3800
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
3801
+ if (targetIds.length === 0)
3802
+ return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
3803
+ let matchIds = targetIds;
3804
+ let ambiguous = false;
3805
+ if (file !== void 0) {
3806
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
3807
+ if (allNamedIds.length > targetIds.length) {
3808
+ matchIds = allNamedIds;
3809
+ ambiguous = true;
3810
+ }
3811
+ }
3812
+ const cteSql = (seedSource) => `WITH RECURSIVE incoming_tree(from_id) AS (
3813
+ SELECT r.from_id
3814
+ FROM refs r
3815
+ WHERE r.to_id IN (${seedSource})
3816
+
3817
+ UNION
3818
+
3819
+ SELECT r.from_id
3820
+ FROM refs r
3821
+ JOIN incoming_tree it ON r.to_id = it.from_id
3822
+ )
3823
+ SELECT
3824
+ s.id AS sym_id,
3825
+ s.name AS sym_name,
3826
+ s.kind AS sym_kind,
3827
+ s.lang AS sym_lang,
3828
+ s.file AS sym_file,
3829
+ s.line AS sym_line,
3830
+ s.signature AS sym_signature,
3831
+ '' AS call_type,
3832
+ 0 AS ref_line
3833
+ FROM incoming_tree it
3834
+ JOIN symbols s ON s.id = it.from_id
3835
+ GROUP BY s.id
3836
+ ORDER BY s.file, s.line`;
3837
+ const rows = runCteWithSeeds(stmt, matchIds, cteSql);
3838
+ if (!file) {
3839
+ const fallbackRows = stmt(
3840
+ `SELECT
3841
+ s.id AS sym_id,
3842
+ s.name AS sym_name,
3843
+ s.kind AS sym_kind,
3844
+ s.lang AS sym_lang,
3845
+ s.file AS sym_file,
3846
+ s.line AS sym_line,
3847
+ s.signature AS sym_signature,
3848
+ r.call_type,
3849
+ r.line AS ref_line
3850
+ FROM refs r
3851
+ JOIN symbols s ON s.id = r.from_id
3852
+ WHERE r.to_id IS NULL AND r.to_name = ?
3853
+ ORDER BY r.line, r.id`
3854
+ ).all(symbolName);
3855
+ rows.push(...fallbackRows);
3856
+ }
3857
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
3858
+ const allCalls = rows.map(mapCallSiteRow);
3859
+ return {
3860
+ calls: allCalls.slice(0, limit),
3861
+ symbolFound: true,
3862
+ ambiguous,
3863
+ totalMatches: allCalls.length
3864
+ };
3865
+ }
3866
+ function findTransitiveOutgoingCallsByName(stmt, symbolName, file, limit) {
3867
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
3868
+ if (sourceIds.length === 0)
3869
+ return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
3870
+ const unresolvedCount = chunkedIdScalar(
3871
+ stmt,
3872
+ sourceIds,
3873
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
3874
+ );
3875
+ const cteSql = (seedSource) => `WITH RECURSIVE outgoing_tree(to_id) AS (
3876
+ SELECT r.to_id
3877
+ FROM refs r
3878
+ WHERE r.from_id IN (${seedSource}) AND r.to_id IS NOT NULL
3879
+
3880
+ UNION
3881
+
3882
+ SELECT r.to_id
3883
+ FROM refs r
3884
+ JOIN outgoing_tree ot ON r.from_id = ot.to_id
3885
+ WHERE r.to_id IS NOT NULL
3886
+ )
3887
+ SELECT
3888
+ s.id AS sym_id,
3889
+ s.name AS sym_name,
3890
+ s.kind AS sym_kind,
3891
+ s.lang AS sym_lang,
3892
+ s.file AS sym_file,
3893
+ s.line AS sym_line,
3894
+ s.signature AS sym_signature,
3895
+ '' AS call_type,
3896
+ 0 AS ref_line
3897
+ FROM outgoing_tree ot
3898
+ JOIN symbols s ON s.id = ot.to_id
3899
+ GROUP BY s.id
3900
+ ORDER BY s.file, s.line`;
3901
+ const rows = runCteWithSeeds(stmt, sourceIds, cteSql);
3902
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
3903
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
3904
+ }
3905
+ function findReachableSymbolIds(stmt, seedIds) {
3906
+ if (seedIds.length === 0) return /* @__PURE__ */ new Set();
3907
+ if (seedIds.length > 900) {
3908
+ stmt("DROP TABLE IF EXISTS _seeds").run();
3909
+ try {
3910
+ stmt("CREATE TEMP TABLE _seeds (id INTEGER PRIMARY KEY)").run();
3911
+ for (let i = 0; i < seedIds.length; i += 500) {
3912
+ const chunk = seedIds.slice(i, i + 500);
3913
+ const ph2 = chunk.map(() => "(?)").join(",");
3914
+ stmt(`INSERT OR IGNORE INTO _seeds (id) VALUES ${ph2}`).run(...chunk);
3915
+ }
3916
+ const rows2 = stmt(
3917
+ `WITH RECURSIVE reachable(id) AS (
3918
+ SELECT id FROM _seeds
3919
+ UNION
3920
+ SELECT r.to_id
3921
+ FROM refs r
3922
+ JOIN reachable ON r.from_id = reachable.id
3923
+ WHERE r.to_id IS NOT NULL
3924
+ )
3925
+ SELECT DISTINCT id FROM reachable`
3926
+ ).all();
3927
+ return new Set(rows2.map((r) => r.id));
3928
+ } finally {
3929
+ stmt("DROP TABLE IF EXISTS _seeds").run();
3930
+ }
3931
+ }
3932
+ const ph = seedIds.map(() => "?").join(",");
3933
+ const rows = stmt(
3934
+ `WITH RECURSIVE reachable(id) AS (
3935
+ SELECT id FROM symbols WHERE id IN (${ph})
3936
+ UNION
3937
+ SELECT r.to_id
3938
+ FROM refs r
3939
+ JOIN reachable ON r.from_id = reachable.id
3940
+ WHERE r.to_id IS NOT NULL
3941
+ )
3942
+ SELECT DISTINCT id FROM reachable`
3943
+ ).all(...seedIds);
3944
+ return new Set(rows.map((r) => r.id));
3945
+ }
3176
3946
  function findRefsToWithStatement(stmt, symbolId) {
3177
3947
  return stmt(
3178
3948
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
@@ -3416,6 +4186,12 @@ var CORE_TABLES_SQL = `
3416
4186
  file TEXT PRIMARY KEY,
3417
4187
  lang TEXT NOT NULL,
3418
4188
  mtime_ms INTEGER NOT NULL,
4189
+ -- Phase 2: xxHash64 of the file's UTF-8 bytes. Empty string when the
4190
+ -- indexer hasn't populated it yet (legacy rows, schema repaired by
4191
+ -- repairMissingColumns). Compared on incremental re-index so that a
4192
+ -- touch or branch-switch that leaves content byte-identical skips the
4193
+ -- expensive parse phase entirely (refactoring proposal Phase 2).
4194
+ content_hash TEXT NOT NULL DEFAULT '',
3419
4195
  symbol_count INTEGER NOT NULL DEFAULT 0,
3420
4196
  last_indexed INTEGER NOT NULL,
3421
4197
  -- Code Atlas grouping label, computed at index time from the ecosystem's
@@ -3483,7 +4259,14 @@ var LANG_FAMILY_TABLE_SQL = `
3483
4259
  );
3484
4260
  `;
3485
4261
  var LANG_FAMILY_WILDCARD = "*";
3486
- var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
4262
+ var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'trigram')";
4263
+ var SYMBOL_VECTORS_TABLE_SQL = `
4264
+ CREATE TABLE IF NOT EXISTS symbol_vectors (
4265
+ symbol_id INTEGER PRIMARY KEY,
4266
+ vector BLOB NOT NULL,
4267
+ FOREIGN KEY (symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
4268
+ );
4269
+ `;
3487
4270
 
3488
4271
  // src/codebase-index/writer-search-helpers.ts
3489
4272
  var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
@@ -3624,6 +4407,85 @@ var StorePool = class {
3624
4407
  }
3625
4408
  };
3626
4409
 
4410
+ // src/codebase-index/vector-search.ts
4411
+ var RRF_K = 60;
4412
+ var VECTOR_DIMENSIONS = 384;
4413
+ var NGRAM_SIZE = 3;
4414
+ function embedText(text) {
4415
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
4416
+ const normalized = text.toLowerCase().trim();
4417
+ if (normalized.length < NGRAM_SIZE) {
4418
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
4419
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
4420
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
4421
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4422
+ vec[bucket] += 1;
4423
+ }
4424
+ } else {
4425
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
4426
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
4427
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4428
+ vec[bucket] += 1;
4429
+ }
4430
+ }
4431
+ let norm = 0;
4432
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4433
+ norm += vec[i] * vec[i];
4434
+ }
4435
+ norm = Math.sqrt(norm);
4436
+ if (norm > 0) {
4437
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4438
+ vec[i] /= norm;
4439
+ }
4440
+ }
4441
+ return vec;
4442
+ }
4443
+ function hashNgram(str) {
4444
+ let hash = 2166136261;
4445
+ for (let i = 0; i < str.length; i++) {
4446
+ hash ^= str.charCodeAt(i);
4447
+ hash = Math.imul(hash, 16777619);
4448
+ }
4449
+ return hash >>> 0;
4450
+ }
4451
+ function cosineSimilarity(a, b) {
4452
+ let dot = 0;
4453
+ const len = Math.min(a.length, b.length);
4454
+ for (let i = 0; i < len; i++) {
4455
+ dot += a[i] * b[i];
4456
+ }
4457
+ return dot;
4458
+ }
4459
+ function encodeVector(vec) {
4460
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
4461
+ }
4462
+ function decodeVector(buf) {
4463
+ const view = new DataView(
4464
+ buf.buffer,
4465
+ buf.byteOffset,
4466
+ buf.byteLength
4467
+ );
4468
+ const copy = new Float32Array(buf.byteLength / 4);
4469
+ for (let i = 0; i < copy.length; i++) {
4470
+ copy[i] = view.getFloat32(i * 4, true);
4471
+ }
4472
+ return copy;
4473
+ }
4474
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
4475
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
4476
+ const scored = [];
4477
+ for (const id of allIds) {
4478
+ const bm25Rank = bm25Ranks.get(id);
4479
+ const vecRank = vectorRanks.get(id);
4480
+ let score = 0;
4481
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
4482
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
4483
+ scored.push([id, score]);
4484
+ }
4485
+ scored.sort((a, b) => b[1] - a[1]);
4486
+ return scored;
4487
+ }
4488
+
3627
4489
  // src/codebase-index/writer.ts
3628
4490
  var DB_FILE2 = "index.db";
3629
4491
  var MAX_STATEMENT_CACHE = 128;
@@ -3636,6 +4498,12 @@ var IndexStore = class _IndexStore {
3636
4498
  * When false, ranked search falls back to the LIKE + in-process BM25 path.
3637
4499
  */
3638
4500
  ftsAvailable = false;
4501
+ /**
4502
+ * Phase 3: true when the `symbol_vectors` table was created successfully.
4503
+ * When false, hybrid search skips the vector pass and falls back to FTS5
4504
+ * (or LIKE) only.
4505
+ */
4506
+ vectorsAvailable = false;
3639
4507
  /**
3640
4508
  * Cache of prepared statements keyed by their SQL text. `DatabaseSync`
3641
4509
  * compiles SQL on every `.prepare()` call; for the fixed-SQL methods
@@ -3734,7 +4602,13 @@ var IndexStore = class _IndexStore {
3734
4602
  */
3735
4603
  repairMissingColumns() {
3736
4604
  const expected = [
3737
- { table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
4605
+ {
4606
+ table: "files",
4607
+ columns: [
4608
+ ["package", "TEXT NOT NULL DEFAULT ''"],
4609
+ ["content_hash", "TEXT NOT NULL DEFAULT ''"]
4610
+ ]
4611
+ },
3738
4612
  {
3739
4613
  table: "refs",
3740
4614
  columns: [
@@ -3766,6 +4640,7 @@ var IndexStore = class _IndexStore {
3766
4640
  DROP TABLE IF EXISTS symbols;
3767
4641
  DROP TABLE IF EXISTS files;
3768
4642
  DROP TABLE IF EXISTS refs;
4643
+ DROP TABLE IF EXISTS symbol_vectors;
3769
4644
  `);
3770
4645
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
3771
4646
  this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(
@@ -3787,6 +4662,12 @@ var IndexStore = class _IndexStore {
3787
4662
  this.db.exec(LANG_FAMILY_TABLE_SQL);
3788
4663
  this.seedLangFamilies();
3789
4664
  try {
4665
+ const ftsSchema = this.stmt(
4666
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
4667
+ ).get();
4668
+ if (ftsSchema?.sql?.includes("unicode61")) {
4669
+ this.db.exec("DROP TABLE IF EXISTS symbols_fts");
4670
+ }
3790
4671
  this.db.exec(SYMBOLS_FTS_SQL);
3791
4672
  this.ftsAvailable = true;
3792
4673
  const symbolCount = Number(
@@ -3797,6 +4678,7 @@ var IndexStore = class _IndexStore {
3797
4678
  );
3798
4679
  if (symbolCount !== ftsCount) {
3799
4680
  this.db.exec("DELETE FROM symbols_fts");
4681
+ if (this.vectorsAvailable) this.db.exec("DELETE FROM symbol_vectors");
3800
4682
  const rows = this.stmt(
3801
4683
  "SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
3802
4684
  ).all();
@@ -3814,6 +4696,12 @@ var IndexStore = class _IndexStore {
3814
4696
  } catch {
3815
4697
  this.ftsAvailable = false;
3816
4698
  }
4699
+ try {
4700
+ this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
4701
+ this.vectorsAvailable = true;
4702
+ } catch {
4703
+ this.vectorsAvailable = false;
4704
+ }
3817
4705
  this.ensureNextSymbolIdSeeded();
3818
4706
  }
3819
4707
  // ─── ID allocation & bulk helpers ────────────────────────────────────────────
@@ -3923,6 +4811,7 @@ var IndexStore = class _IndexStore {
3923
4811
  const result = [];
3924
4812
  const bulk = [];
3925
4813
  const ftsRows = [];
4814
+ const vectorRows = [];
3926
4815
  for (const s of symbols) {
3927
4816
  const id = nextId++;
3928
4817
  bulk.push({
@@ -3941,6 +4830,10 @@ var IndexStore = class _IndexStore {
3941
4830
  if (this.ftsAvailable) {
3942
4831
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
3943
4832
  }
4833
+ vectorRows.push({
4834
+ id,
4835
+ vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
4836
+ });
3944
4837
  result.push({ ...s, id });
3945
4838
  }
3946
4839
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -3950,6 +4843,13 @@ var IndexStore = class _IndexStore {
3950
4843
  this.ftsAvailable,
3951
4844
  ftsRows
3952
4845
  );
4846
+ if (this.vectorsAvailable) {
4847
+ bulkInsertVectorsWithStatement(
4848
+ (sql) => this.stmt(sql),
4849
+ _IndexStore.MAX_SQL_VARS,
4850
+ vectorRows
4851
+ );
4852
+ }
3953
4853
  this.db.exec("COMMIT");
3954
4854
  return result;
3955
4855
  } catch (err) {
@@ -3969,6 +4869,11 @@ var IndexStore = class _IndexStore {
3969
4869
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
3970
4870
  ).run(file);
3971
4871
  }
4872
+ if (this.vectorsAvailable) {
4873
+ this.stmt(
4874
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
4875
+ ).run(file);
4876
+ }
3972
4877
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
3973
4878
  this.resolveRefsForNamesUnsafe(affectedNames);
3974
4879
  this.db.exec("COMMIT");
@@ -3994,6 +4899,11 @@ var IndexStore = class _IndexStore {
3994
4899
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
3995
4900
  ).run(file);
3996
4901
  }
4902
+ if (this.vectorsAvailable) {
4903
+ this.stmt(
4904
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
4905
+ ).run(file);
4906
+ }
3997
4907
  this.stmt(
3998
4908
  "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
3999
4909
  ).run(file);
@@ -4011,14 +4921,22 @@ var IndexStore = class _IndexStore {
4011
4921
  upsertFile(meta) {
4012
4922
  this.runWithRetry(() => {
4013
4923
  this.stmt(
4014
- `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
4015
- VALUES (?, ?, ?, ?, ?)
4924
+ `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
4925
+ VALUES (?, ?, ?, ?, ?, ?)
4016
4926
  ON CONFLICT(file) DO UPDATE SET
4017
4927
  lang = excluded.lang,
4018
4928
  mtime_ms = excluded.mtime_ms,
4929
+ content_hash = excluded.content_hash,
4019
4930
  symbol_count = excluded.symbol_count,
4020
4931
  last_indexed = excluded.last_indexed`
4021
- ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
4932
+ ).run(
4933
+ meta.file,
4934
+ meta.lang,
4935
+ meta.mtimeMs,
4936
+ meta.contentHash ?? "",
4937
+ meta.symbolCount,
4938
+ meta.lastIndexed
4939
+ );
4022
4940
  });
4023
4941
  }
4024
4942
  getFileMeta(file) {
@@ -4185,9 +5103,18 @@ var IndexStore = class _IndexStore {
4185
5103
  if (mapped === null) return { results: [], total: 0 };
4186
5104
  effectiveKind = mapped;
4187
5105
  }
4188
- const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
5106
+ const longTokens = tokens.filter((t) => t.length >= 3);
5107
+ const shortTokens = tokens.filter((t) => t.length < 3);
5108
+ if (longTokens.length === 0) {
5109
+ return this.searchRankedFallback(query, filter, safeLimit);
5110
+ }
5111
+ const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
4189
5112
  const conditions = ["symbols_fts MATCH ?"];
4190
5113
  const values = [match];
5114
+ for (const shortTok of shortTokens) {
5115
+ conditions.push("s.text LIKE ? ESCAPE '\\'");
5116
+ values.push(`%${escapeLike(shortTok)}%`);
5117
+ }
4191
5118
  if (effectiveKind) {
4192
5119
  conditions.push("s.kind = ?");
4193
5120
  values.push(effectiveKind);
@@ -4206,7 +5133,7 @@ var IndexStore = class _IndexStore {
4206
5133
  ).all(...values);
4207
5134
  const total = countRows[0] ? Number(countRows[0].n) : 0;
4208
5135
  if (total === 0) return { results: [], total: 0 };
4209
- const rows = this.stmt(
5136
+ const bm25Rows = this.stmt(
4210
5137
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
4211
5138
  -bm25(symbols_fts) AS score,
4212
5139
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -4219,8 +5146,39 @@ var IndexStore = class _IndexStore {
4219
5146
  bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
4220
5147
  LIMIT ?`
4221
5148
  ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5149
+ if (this.vectorsAvailable && bm25Rows.length > 0) {
5150
+ const queryVec = embedText(query);
5151
+ const candidateIds = bm25Rows.map((r) => r.id);
5152
+ const placeholders = candidateIds.map(() => "?").join(",");
5153
+ const vecRows = this.stmt(
5154
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
5155
+ ).all(...candidateIds);
5156
+ const vecScores = vecRows.map((r) => ({
5157
+ id: r.symbol_id,
5158
+ sim: cosineSimilarity(queryVec, decodeVector(r.vector))
5159
+ })).sort((a, b) => b.sim - a.sim);
5160
+ const bm25Rank = /* @__PURE__ */ new Map();
5161
+ bm25Rows.forEach((r, i) => {
5162
+ bm25Rank.set(r.id, i);
5163
+ });
5164
+ const vecRank = /* @__PURE__ */ new Map();
5165
+ vecScores.forEach((r, i) => {
5166
+ vecRank.set(r.id, i);
5167
+ });
5168
+ const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
5169
+ const fusedScore = new Map(fused);
5170
+ const sorted = [...bm25Rows].sort(
5171
+ (a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
5172
+ );
5173
+ return {
5174
+ results: sorted.map(
5175
+ (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5176
+ ),
5177
+ total
5178
+ };
5179
+ }
4222
5180
  return {
4223
- results: rows.map(
5181
+ results: bm25Rows.map(
4224
5182
  (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
4225
5183
  ),
4226
5184
  total
@@ -4348,6 +5306,7 @@ var IndexStore = class _IndexStore {
4348
5306
  this.db.exec("DROP TABLE IF EXISTS files");
4349
5307
  this.db.exec("DROP TABLE IF EXISTS metadata");
4350
5308
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
5309
+ this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
4351
5310
  this.db.exec("COMMIT");
4352
5311
  this.stmtCache.clear();
4353
5312
  this.initSchema();
@@ -4435,6 +5394,11 @@ var IndexStore = class _IndexStore {
4435
5394
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
4436
5395
  ).run(...options.deleteForFiles);
4437
5396
  }
5397
+ if (this.vectorsAvailable) {
5398
+ this.stmt(
5399
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5400
+ ).run(...options.deleteForFiles);
5401
+ }
4438
5402
  this.stmt(
4439
5403
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
4440
5404
  ).run(...options.deleteForFiles);
@@ -4448,6 +5412,7 @@ var IndexStore = class _IndexStore {
4448
5412
  const refsToInsert = [];
4449
5413
  const bulkSyms = [];
4450
5414
  const ftsRows = [];
5415
+ const vectorRows = [];
4451
5416
  for (const entry of entries) {
4452
5417
  const insertedForEntry = [];
4453
5418
  for (const s of entry.symbols) {
@@ -4471,6 +5436,10 @@ var IndexStore = class _IndexStore {
4471
5436
  text: buildIndexableText(s.name, s.signature, s.docComment)
4472
5437
  });
4473
5438
  }
5439
+ vectorRows.push({
5440
+ id,
5441
+ vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
5442
+ });
4474
5443
  const inserted = { ...s, id };
4475
5444
  allInserted.push(inserted);
4476
5445
  insertedForEntry.push(inserted);
@@ -4484,19 +5453,34 @@ var IndexStore = class _IndexStore {
4484
5453
  this.ftsAvailable,
4485
5454
  ftsRows
4486
5455
  );
5456
+ if (this.vectorsAvailable) {
5457
+ bulkInsertVectorsWithStatement(
5458
+ (sql) => this.stmt(sql),
5459
+ _IndexStore.MAX_SQL_VARS,
5460
+ vectorRows
5461
+ );
5462
+ }
4487
5463
  bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
4488
5464
  const upsertStmt = this.stmt(
4489
- `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
4490
- VALUES (?, ?, ?, ?, ?)
5465
+ `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
5466
+ VALUES (?, ?, ?, ?, ?, ?)
4491
5467
  ON CONFLICT(file) DO UPDATE SET
4492
5468
  lang = excluded.lang,
4493
5469
  mtime_ms = excluded.mtime_ms,
5470
+ content_hash = excluded.content_hash,
4494
5471
  symbol_count = excluded.symbol_count,
4495
5472
  last_indexed = excluded.last_indexed`
4496
5473
  );
4497
5474
  const now = Date.now();
4498
5475
  for (const entry of entries) {
4499
- upsertStmt.run(entry.file, entry.lang, entry.mtimeMs, entry.symbolCount, now);
5476
+ upsertStmt.run(
5477
+ entry.file,
5478
+ entry.lang,
5479
+ entry.mtimeMs,
5480
+ entry.contentHash ?? "",
5481
+ entry.symbolCount,
5482
+ now
5483
+ );
4500
5484
  }
4501
5485
  this.resolveRefsForNamesUnsafe(affectedNames);
4502
5486
  this.db.exec("COMMIT");
@@ -4587,19 +5571,32 @@ var IndexStore = class _IndexStore {
4587
5571
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
4588
5572
  ).run(meta.file);
4589
5573
  }
5574
+ if (this.vectorsAvailable) {
5575
+ this.stmt(
5576
+ "DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
5577
+ ).run(meta.file);
5578
+ }
4590
5579
  this.stmt(
4591
5580
  "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
4592
5581
  ).run(meta.file);
4593
5582
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
4594
5583
  this.stmt(
4595
- `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
4596
- VALUES (?, ?, ?, ?, ?)
5584
+ `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
5585
+ VALUES (?, ?, ?, ?, ?, ?)
4597
5586
  ON CONFLICT(file) DO UPDATE SET
4598
5587
  lang = excluded.lang,
4599
5588
  mtime_ms = excluded.mtime_ms,
5589
+ content_hash = excluded.content_hash,
4600
5590
  symbol_count = excluded.symbol_count,
4601
5591
  last_indexed = excluded.last_indexed`
4602
- ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
5592
+ ).run(
5593
+ meta.file,
5594
+ meta.lang,
5595
+ meta.mtimeMs,
5596
+ meta.contentHash ?? "",
5597
+ meta.symbolCount,
5598
+ meta.lastIndexed
5599
+ );
4603
5600
  this.resolveRefsForNamesUnsafe(affectedNames);
4604
5601
  this.db.exec("COMMIT");
4605
5602
  } catch (err) {
@@ -4662,6 +5659,31 @@ var IndexStore = class _IndexStore {
4662
5659
  findOutgoingCallsByName(symbolName, file, limit = 100) {
4663
5660
  return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4664
5661
  }
5662
+ /**
5663
+ * Transitive incoming-call tree: all symbols that transitively call the
5664
+ * target, to an unbounded depth (cycle-safe via SQL UNION deduplication).
5665
+ * Used by `codebase-incoming-calls` when the caller wants the full call
5666
+ * chain rather than just direct callers.
5667
+ */
5668
+ findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
5669
+ return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
5670
+ }
5671
+ /**
5672
+ * Transitive outgoing-call tree: all symbols the target transitively calls.
5673
+ * Used by `codebase-outgoing-calls` when the caller wants the full
5674
+ * dependency chain rather than just direct callees.
5675
+ */
5676
+ findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
5677
+ return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
5678
+ }
5679
+ /**
5680
+ * Compute the set of symbol IDs reachable from the given seed IDs using a
5681
+ * native SQLite recursive CTE. Used by dead-code detection to replace the
5682
+ * in-memory BFS.
5683
+ */
5684
+ findReachableSymbolIds(seedIds) {
5685
+ return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
5686
+ }
4665
5687
  /**
4666
5688
  * Find all references TO a given symbol (who calls / uses this symbol?).
4667
5689
  */
@@ -4924,6 +5946,12 @@ var ProjectServerConnection = class {
4924
5946
  endpoint;
4925
5947
  socket = null;
4926
5948
  buffer = "";
5949
+ /** P6: binary frame buffer — accumulates raw bytes when in binary mode. */
5950
+ binaryBuffer = [];
5951
+ /** P6: StringDecoder for safe UTF-8 multibyte handling in JSON mode. */
5952
+ textDecoder = null;
5953
+ /** P6: true once the server advertises binary support and client accepts. */
5954
+ useBinary = false;
4927
5955
  info = null;
4928
5956
  activity = null;
4929
5957
  health = null;
@@ -5062,6 +6090,8 @@ var ProjectServerConnection = class {
5062
6090
  this.info = null;
5063
6091
  this.activity = null;
5064
6092
  this.health = null;
6093
+ this.useBinary = false;
6094
+ this.binaryBuffer = [];
5065
6095
  this.connectReject?.(new Error("codebase-index client disconnected"));
5066
6096
  this.connectResolve = null;
5067
6097
  this.connectReject = null;
@@ -5190,10 +6220,12 @@ var ProjectServerConnection = class {
5190
6220
  this.activity = null;
5191
6221
  this.health = null;
5192
6222
  this.buffer = "";
6223
+ this.binaryBuffer = [];
6224
+ this.useBinary = false;
6225
+ this.textDecoder = null;
5193
6226
  return new Promise((resolve4, reject) => {
5194
6227
  const socket = net.createConnection(this.endpoint);
5195
6228
  this.socket = socket;
5196
- socket.setEncoding("utf8");
5197
6229
  const timer = setTimeout(() => {
5198
6230
  reject(new Error("codebase-index server handshake timed out"));
5199
6231
  socket.destroy();
@@ -5222,7 +6254,12 @@ var ProjectServerConnection = class {
5222
6254
  }
5223
6255
  onData(socket, chunk) {
5224
6256
  if (socket !== this.socket) return;
5225
- this.buffer += chunk;
6257
+ if (this.useBinary) {
6258
+ this.onBinaryData(socket, chunk);
6259
+ return;
6260
+ }
6261
+ if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
6262
+ this.buffer += this.textDecoder.write(chunk);
5226
6263
  while (true) {
5227
6264
  const newline = this.buffer.indexOf("\n");
5228
6265
  if (newline < 0) {
@@ -5248,6 +6285,44 @@ var ProjectServerConnection = class {
5248
6285
  this.onMessage(message);
5249
6286
  }
5250
6287
  }
6288
+ /**
6289
+ * P6: Parse binary frames from the raw buffer.
6290
+ *
6291
+ * Each frame is: [1 byte magic 0x57] [4 bytes uint32 BE length] [payload].
6292
+ * The magic byte distinguishes binary from JSON — a JSON frame's first byte
6293
+ * is `{` (0x7B), so there is no ambiguity even in a mixed-mode buffer.
6294
+ */
6295
+ onBinaryData(socket, chunk) {
6296
+ this.binaryBuffer.push(chunk);
6297
+ const all = Buffer.concat(this.binaryBuffer);
6298
+ let offset = 0;
6299
+ while (offset + 5 <= all.length) {
6300
+ if (!isBinaryFrame(all[offset])) {
6301
+ this.useBinary = false;
6302
+ this.buffer += all.subarray(offset).toString("utf8");
6303
+ this.binaryBuffer = [];
6304
+ return;
6305
+ }
6306
+ const frameLen = all.readUInt32BE(offset + 1);
6307
+ if (frameLen > 256 * 1024 * 1024) {
6308
+ socket.destroy();
6309
+ this.transition("offline", { error: "binary frame length exceeds 256MB limit" });
6310
+ return;
6311
+ }
6312
+ const totalLen = 5 + frameLen;
6313
+ if (offset + totalLen > all.length) break;
6314
+ const payload = all.subarray(offset + 5, offset + 5 + frameLen);
6315
+ try {
6316
+ const message = decodeBinaryFrame(payload);
6317
+ this.onMessage(message);
6318
+ } catch {
6319
+ socket.destroy(new Error("invalid binary codebase-index server response"));
6320
+ return;
6321
+ }
6322
+ offset += totalLen;
6323
+ }
6324
+ this.binaryBuffer = offset < all.length ? [all.subarray(offset)] : [];
6325
+ }
5251
6326
  onMessage(message) {
5252
6327
  if (message.type === "hello") {
5253
6328
  if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
@@ -5267,6 +6342,7 @@ var ProjectServerConnection = class {
5267
6342
  }
5268
6343
  this.info = message;
5269
6344
  this.markResponsive();
6345
+ if (message.binarySupported) this.useBinary = true;
5270
6346
  this.transition("connected", { pid: message.pid });
5271
6347
  ensureHeartbeatLoop();
5272
6348
  this.connectResolve?.();
@@ -5325,7 +6401,12 @@ var ProjectServerConnection = class {
5325
6401
  }
5326
6402
  write(message) {
5327
6403
  const socket = this.socket;
5328
- if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
6404
+ if (!socket || socket.destroyed) return;
6405
+ if (this.useBinary) {
6406
+ socket.write(encodeBinaryFrame(message));
6407
+ } else {
6408
+ socket.write(encodeProjectServerMessage(message));
6409
+ }
5329
6410
  }
5330
6411
  rejectStaleServer(message, reason) {
5331
6412
  const socket = this.socket;
@@ -5470,22 +6551,102 @@ function closeProjectIndexServerClients() {
5470
6551
  }
5471
6552
 
5472
6553
  // src/codebase-index/background-indexer.ts
5473
- import * as fs11 from "node:fs";
5474
- import { fileURLToPath as fileURLToPath3 } from "node:url";
5475
- import { Worker } from "node:worker_threads";
6554
+ import * as fs12 from "node:fs";
6555
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
6556
+ import { Worker as Worker2 } from "node:worker_threads";
5476
6557
 
5477
6558
  // src/codebase-index/indexer.ts
5478
6559
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
5479
6560
  import { execFile } from "node:child_process";
5480
- import * as fs10 from "node:fs/promises";
6561
+ import * as fs11 from "node:fs/promises";
5481
6562
  import { availableParallelism } from "node:os";
5482
- import * as path12 from "node:path";
6563
+ import * as path13 from "node:path";
5483
6564
  import {
5484
6565
  DEFAULT_WALK_IGNORE_DIRS,
5485
6566
  indexParallelBatchSize,
5486
6567
  isFrugalPerf
5487
6568
  } from "@wrongstack/core/utils";
5488
6569
 
6570
+ // src/codebase-index/content-hash.ts
6571
+ var PRIME64_1 = 0x9e3779b185ebca87n;
6572
+ var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
6573
+ var PRIME64_3 = 0x165667b19e3779f9n;
6574
+ var PRIME64_4 = 0x85ebca77c2b2ae63n;
6575
+ var PRIME64_5 = 0x27d4eb2f165667c5n;
6576
+ var MASK64 = 0xffffffffffffffffn;
6577
+ function mul64(a, b) {
6578
+ return (a & MASK64) * (b & MASK64) & MASK64;
6579
+ }
6580
+ function rotl64(x, n) {
6581
+ const v = x & MASK64;
6582
+ return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
6583
+ }
6584
+ function readU64LE(buf, off) {
6585
+ let v = 0n;
6586
+ for (let i = 7; i >= 0; i--) {
6587
+ v = v << 8n | BigInt(buf[off + i] ?? 0);
6588
+ }
6589
+ return v & MASK64;
6590
+ }
6591
+ function readU32LE(buf, off) {
6592
+ return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
6593
+ }
6594
+ function xxh64Round(acc, lane) {
6595
+ return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
6596
+ }
6597
+ function xxh64MergeRound(acc, val) {
6598
+ return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
6599
+ }
6600
+ function xxhash64Hex(buf, explicitLen) {
6601
+ const length = explicitLen ?? buf.length;
6602
+ let h;
6603
+ let off = 0;
6604
+ if (length >= 32) {
6605
+ let v1 = PRIME64_1 + PRIME64_2 & MASK64;
6606
+ let v2 = PRIME64_2;
6607
+ let v3 = 0n;
6608
+ let v4 = 0n - PRIME64_1 & MASK64;
6609
+ const end32 = length - 32;
6610
+ while (off <= end32) {
6611
+ v1 = xxh64Round(v1, readU64LE(buf, off));
6612
+ v2 = xxh64Round(v2, readU64LE(buf, off + 8));
6613
+ v3 = xxh64Round(v3, readU64LE(buf, off + 16));
6614
+ v4 = xxh64Round(v4, readU64LE(buf, off + 24));
6615
+ off += 32;
6616
+ }
6617
+ h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
6618
+ h = xxh64MergeRound(h, v1);
6619
+ h = xxh64MergeRound(h, v2);
6620
+ h = xxh64MergeRound(h, v3);
6621
+ h = xxh64MergeRound(h, v4);
6622
+ } else {
6623
+ h = PRIME64_5;
6624
+ }
6625
+ h = h + BigInt(length) & MASK64;
6626
+ while (off + 8 <= length) {
6627
+ const k1 = xxh64Round(0n, readU64LE(buf, off));
6628
+ h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
6629
+ off += 8;
6630
+ }
6631
+ if (off + 4 <= length) {
6632
+ h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
6633
+ off += 4;
6634
+ }
6635
+ while (off < length) {
6636
+ h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
6637
+ off += 1;
6638
+ }
6639
+ h = (h ^ h >> 33n) & MASK64;
6640
+ h = mul64(h, PRIME64_2);
6641
+ h = (h ^ h >> 29n) & MASK64;
6642
+ h = mul64(h, PRIME64_3);
6643
+ h = (h ^ h >> 32n) & MASK64;
6644
+ return h.toString(16).padStart(16, "0");
6645
+ }
6646
+ function xxhash64String(content) {
6647
+ return xxhash64Hex(new TextEncoder().encode(content));
6648
+ }
6649
+
5489
6650
  // src/codebase-index/gitignore.ts
5490
6651
  import * as fs6 from "node:fs/promises";
5491
6652
  import * as path6 from "node:path";
@@ -5977,32 +7138,56 @@ async function dispatch(file, content, lang) {
5977
7138
  case "tsx":
5978
7139
  case "js":
5979
7140
  case "jsx": {
5980
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
5981
- return parseSymbols8({ file, content, lang });
7141
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
7142
+ return parseSymbols9({ file, content, lang });
5982
7143
  }
5983
7144
  case "go": {
5984
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
5985
- return parseSymbols8({ file, content, lang: "go" });
7145
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
7146
+ return parseSymbols9({ file, content, lang: "go" });
5986
7147
  }
5987
7148
  case "py": {
5988
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
5989
- return parseSymbols8({ file, content, lang: "py" });
7149
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
7150
+ return parseSymbols9({ file, content, lang: "py" });
5990
7151
  }
5991
7152
  case "rs": {
5992
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
5993
- return parseSymbols8({ file, content, lang: "rs" });
7153
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
7154
+ return parseSymbols9({ file, content, lang: "rs" });
5994
7155
  }
5995
7156
  case "json": {
5996
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
5997
- return parseSymbols8({ file, content, lang: "json" });
7157
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
7158
+ return parseSymbols9({ file, content, lang: "json" });
5998
7159
  }
5999
7160
  case "yaml": {
6000
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
6001
- return parseSymbols8({ file, content, lang: "yaml" });
7161
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
7162
+ return parseSymbols9({ file, content, lang: "yaml" });
7163
+ }
7164
+ // Phase 1: ten languages now route through the Tree-Sitter WASM
7165
+ // universal parser (`tree-sitter-parser.ts`). The dispatch falls back to
7166
+ // the regex extractor in `generic-parser.ts` whenever WASM loading fails
7167
+ // or the parser returns zero symbols — preserving the indexable-file
7168
+ // contract that "missing a parser must never mean skipping the file".
7169
+ case "c":
7170
+ case "cpp":
7171
+ case "java":
7172
+ case "csharp":
7173
+ case "php":
7174
+ case "ruby":
7175
+ case "swift":
7176
+ case "kotlin":
7177
+ case "shell":
7178
+ case "elixir": {
7179
+ try {
7180
+ const { parseSymbols: parseSymbols10 } = await Promise.resolve().then(() => (init_tree_sitter_parser(), tree_sitter_parser_exports));
7181
+ const parsed = await parseSymbols10({ file, content, lang });
7182
+ if (parsed.symbols.length > 0) return parsed;
7183
+ } catch {
7184
+ }
7185
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
7186
+ return parseSymbols9({ file, content, lang });
6002
7187
  }
6003
7188
  default: {
6004
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
6005
- return parseSymbols8({ file, content, lang });
7189
+ const { parseSymbols: parseSymbols9 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
7190
+ return parseSymbols9({ file, content, lang });
6006
7191
  }
6007
7192
  }
6008
7193
  }
@@ -6014,11 +7199,189 @@ function withRelations(parsed, content, lang) {
6014
7199
  return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
6015
7200
  }
6016
7201
 
7202
+ // src/codebase-index/parser-worker-pool.ts
7203
+ import { Worker } from "node:worker_threads";
7204
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7205
+ import * as fs10 from "node:fs";
7206
+ var WORKER_POOL_THRESHOLD = 500;
7207
+ var ParserWorkerPool = class {
7208
+ constructor(maxWorkers = defaultWorkerCount()) {
7209
+ this.maxWorkers = maxWorkers;
7210
+ }
7211
+ maxWorkers;
7212
+ workers = [];
7213
+ nextBatchId = 1;
7214
+ pending = /* @__PURE__ */ new Map();
7215
+ creating = false;
7216
+ unavailable = false;
7217
+ /**
7218
+ * True if the pool is available for use. Returns false when:
7219
+ * - Worker threads aren't supported (sandbox, exotic runtime)
7220
+ * - The built worker script can't be found
7221
+ * - Pool creation was attempted and failed
7222
+ */
7223
+ isAvailable() {
7224
+ return !this.unavailable && this.workers.length > 0;
7225
+ }
7226
+ /**
7227
+ * Lazily create the worker pool. Returns true if the pool is ready, false
7228
+ * if it's unavailable (caller should fall back to inline parsing).
7229
+ */
7230
+ async ensureReady() {
7231
+ if (this.isAvailable()) return true;
7232
+ if (this.unavailable) return false;
7233
+ if (this.creating) {
7234
+ await new Promise((r) => setTimeout(r, 50));
7235
+ return this.isAvailable();
7236
+ }
7237
+ this.creating = true;
7238
+ try {
7239
+ const url = resolveWorkerScriptUrl();
7240
+ if (!url) {
7241
+ this.unavailable = true;
7242
+ return false;
7243
+ }
7244
+ for (let i = 0; i < this.maxWorkers; i++) {
7245
+ try {
7246
+ const w = new Worker(url, { name: `wstack-parser-${i}` });
7247
+ w.unref();
7248
+ w.on("message", (msg) => this.handleMessage(msg));
7249
+ w.on("error", (err) => this.handleError(err, w));
7250
+ this.workers.push({ worker: w, busy: false });
7251
+ } catch {
7252
+ if (this.workers.length === 0) {
7253
+ this.unavailable = true;
7254
+ return false;
7255
+ }
7256
+ break;
7257
+ }
7258
+ }
7259
+ return this.workers.length > 0;
7260
+ } finally {
7261
+ this.creating = false;
7262
+ }
7263
+ }
7264
+ /**
7265
+ * Parse files in parallel across the worker pool. Returns a flat
7266
+ * `FileSymbols[]` in completion order (caller sorts if needed).
7267
+ *
7268
+ * Content is pre-read by the main thread (for the content-hash check)
7269
+ * and passed to workers to avoid a second disk read. Files are
7270
+ * distributed round-robin across workers.
7271
+ */
7272
+ async parseFiles(files) {
7273
+ if (!this.isAvailable()) {
7274
+ throw new Error("ParserWorkerPool.parseFiles called before ensureReady() succeeded");
7275
+ }
7276
+ if (files.length === 0) return [];
7277
+ const batchId = this.nextBatchId++;
7278
+ const workerCount = Math.min(this.workers.length, files.length);
7279
+ const chunks = Array.from(
7280
+ { length: workerCount },
7281
+ () => []
7282
+ );
7283
+ for (let i = 0; i < files.length; i++) {
7284
+ chunks[i % workerCount].push(files[i]);
7285
+ }
7286
+ return new Promise((resolve4, reject) => {
7287
+ this.pending.set(batchId, {
7288
+ resolve: resolve4,
7289
+ reject,
7290
+ accumulated: [],
7291
+ expectedWorkers: workerCount,
7292
+ completedWorkers: 0
7293
+ });
7294
+ for (let i = 0; i < workerCount; i++) {
7295
+ const pw = this.workers[i];
7296
+ pw.busy = true;
7297
+ pw.worker.postMessage({
7298
+ type: "parse",
7299
+ id: batchId,
7300
+ files: chunks[i]
7301
+ });
7302
+ }
7303
+ });
7304
+ }
7305
+ /** Shut down all workers. Safe to call multiple times. */
7306
+ async shutdown() {
7307
+ const workers = this.workers.map((w) => w.worker);
7308
+ this.workers = [];
7309
+ this.unavailable = false;
7310
+ for (const w of workers) {
7311
+ try {
7312
+ w.postMessage({ type: "shutdown" });
7313
+ } catch {
7314
+ }
7315
+ }
7316
+ await Promise.allSettled(
7317
+ workers.map(
7318
+ (w) => Promise.race([
7319
+ new Promise((resolve4) => {
7320
+ w.once("exit", () => resolve4());
7321
+ }),
7322
+ new Promise((resolve4) => setTimeout(() => resolve4(), 2e3))
7323
+ ]).then(() => {
7324
+ if (!w.threadId) return;
7325
+ return w.terminate().catch(() => {
7326
+ });
7327
+ })
7328
+ )
7329
+ );
7330
+ for (const [, p] of this.pending) p.reject(new Error("ParserWorkerPool shut down"));
7331
+ this.pending.clear();
7332
+ }
7333
+ handleMessage(msg) {
7334
+ const batch = this.pending.get(msg.id);
7335
+ if (!batch) return;
7336
+ batch.accumulated.push(...msg.results);
7337
+ batch.completedWorkers++;
7338
+ const freeWorker = this.workers.find((w) => w.busy);
7339
+ if (freeWorker) freeWorker.busy = false;
7340
+ if (batch.completedWorkers >= batch.expectedWorkers) {
7341
+ this.pending.delete(msg.id);
7342
+ batch.resolve(batch.accumulated);
7343
+ }
7344
+ }
7345
+ handleError(err, source) {
7346
+ this.workers = this.workers.filter((w) => w.worker !== source);
7347
+ if (this.workers.length === 0) {
7348
+ for (const [, p] of this.pending) p.reject(err);
7349
+ this.pending.clear();
7350
+ this.unavailable = true;
7351
+ }
7352
+ }
7353
+ };
7354
+ function defaultWorkerCount() {
7355
+ const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
7356
+ return Math.max(1, Math.min(4, cores - 1));
7357
+ }
7358
+ function resolveWorkerScriptUrl() {
7359
+ for (const rel of [
7360
+ "./parser-worker-script.js",
7361
+ "./codebase-index/parser-worker-script.js"
7362
+ ]) {
7363
+ try {
7364
+ const url = new URL(rel, import.meta.url);
7365
+ if (url.protocol === "file:" && fs10.existsSync(fileURLToPath4(url))) return url;
7366
+ } catch {
7367
+ }
7368
+ }
7369
+ return null;
7370
+ }
7371
+ var _pool = null;
7372
+ function getParserPool() {
7373
+ _pool ??= new ParserWorkerPool();
7374
+ return _pool;
7375
+ }
7376
+
6017
7377
  // src/codebase-index/indexer.ts
6018
7378
  var YIELD_EVERY_N = 50;
6019
7379
  function resolveParallelBatch() {
6020
7380
  return indexParallelBatchSize(availableParallelism());
6021
7381
  }
7382
+ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
7383
+ return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
7384
+ }
6022
7385
  function yieldEventLoop() {
6023
7386
  return new Promise((resolve4) => setImmediate(resolve4));
6024
7387
  }
@@ -6036,15 +7399,15 @@ var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
6036
7399
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
6037
7400
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
6038
7401
  function isWithinProject(projectRoot, file) {
6039
- const rel = path12.relative(projectRoot, file);
6040
- return rel !== "" && !rel.startsWith(`..${path12.sep}`) && rel !== ".." && !path12.isAbsolute(rel);
7402
+ const rel = path13.relative(projectRoot, file);
7403
+ return rel !== "" && !rel.startsWith(`..${path13.sep}`) && rel !== ".." && !path13.isAbsolute(rel);
6041
7404
  }
6042
7405
  function isMissingPathError(err) {
6043
7406
  const code = err?.code;
6044
7407
  return code === "ENOENT" || code === "ENOTDIR";
6045
7408
  }
6046
7409
  function normalizeComparablePath(value) {
6047
- const resolved = path12.resolve(value);
7410
+ const resolved = path13.resolve(value);
6048
7411
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6049
7412
  }
6050
7413
  function gitOutput(projectRoot, args) {
@@ -6089,24 +7452,24 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6089
7452
  const record = statusRecords[i];
6090
7453
  if (!record) continue;
6091
7454
  const status = record.slice(0, 2);
6092
- const changedPath = path12.resolve(projectRoot, record.slice(3));
7455
+ const changedPath = path13.resolve(projectRoot, record.slice(3));
6093
7456
  dirty.add(changedPath);
6094
7457
  if (status.includes("D")) deleted.add(changedPath);
6095
7458
  if (status.includes("R") || status.includes("C")) {
6096
7459
  const source = statusRecords[++i];
6097
- if (source) dirty.add(path12.resolve(projectRoot, source));
7460
+ if (source) dirty.add(path13.resolve(projectRoot, source));
6098
7461
  }
6099
7462
  }
6100
7463
  const files = [];
6101
7464
  for (const relative2 of output.toString("utf8").split("\0")) {
6102
7465
  if (!relative2) continue;
6103
7466
  const portable = relative2.replace(/\\/g, "/");
6104
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path12.posix.basename(portable))) {
7467
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path13.posix.basename(portable))) {
6105
7468
  continue;
6106
7469
  }
6107
- const full = path12.resolve(projectRoot, relative2);
7470
+ const full = path13.resolve(projectRoot, relative2);
6108
7471
  if (deleted.has(full)) continue;
6109
- const ext = path12.extname(relative2).toLowerCase();
7472
+ const ext = path13.extname(relative2).toLowerCase();
6110
7473
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6111
7474
  }
6112
7475
  return {
@@ -6141,7 +7504,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6141
7504
  }
6142
7505
  let entries;
6143
7506
  try {
6144
- entries = await fs10.readdir(dir, { withFileTypes: true });
7507
+ entries = await fs11.readdir(dir, { withFileTypes: true });
6145
7508
  } catch (err) {
6146
7509
  complete = false;
6147
7510
  errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6150,14 +7513,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6150
7513
  dirCount++;
6151
7514
  for (const e of entries) {
6152
7515
  if (ignoreSet.has(e.name)) continue;
6153
- const full = path12.join(dir, e.name);
6154
- const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
7516
+ const full = path13.join(dir, e.name);
7517
+ const rel = path13.relative(projectRoot, full).replace(/\\/g, "/");
6155
7518
  if (e.isDirectory()) {
6156
7519
  if (isGitIgnored(rel, true)) continue;
6157
7520
  await walk(full);
6158
7521
  } else if (e.isFile()) {
6159
7522
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
6160
- const ext = path12.extname(e.name).toLowerCase();
7523
+ const ext = path13.extname(e.name).toLowerCase();
6161
7524
  if (indexableExts.has(ext) || detectLang(full) !== null) {
6162
7525
  results.push(full);
6163
7526
  }
@@ -6195,11 +7558,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
6195
7558
  const structure = await detectModuleRoots(projectRoot, indexedFiles);
6196
7559
  if (opts.signal?.aborted) return;
6197
7560
  store.setFilePackages(assignPackageLabels(structure, indexedFiles));
6198
- const resolver = new ModuleResolver(
6199
- structure,
6200
- indexedFiles,
6201
- store.getNamespaceDeclarations()
6202
- );
7561
+ const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
6203
7562
  const pending2 = store.getUnresolvedImports(opts.onlyFiles);
6204
7563
  const resolutions = [];
6205
7564
  for (const entry of pending2) {
@@ -6235,6 +7594,10 @@ async function runIndexerWithStore(store, opts) {
6235
7594
  const errors = [];
6236
7595
  const langStats = {};
6237
7596
  let filesIndexed = 0;
7597
+ let filesParsed = 0;
7598
+ let filesSkipped = 0;
7599
+ let filesEmpty = 0;
7600
+ let filesFailed = 0;
6238
7601
  let symbolsIndexed = 0;
6239
7602
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
6240
7603
  let files;
@@ -6242,10 +7605,10 @@ async function runIndexerWithStore(store, opts) {
6242
7605
  let discoveryComplete = true;
6243
7606
  let trustedUnchanged;
6244
7607
  if (opts.files && opts.files.length > 0) {
6245
- files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => {
7608
+ files = opts.files.map((f) => path13.resolve(projectRoot, f)).filter((f) => {
6246
7609
  if (!isWithinProject(projectRoot, f)) return false;
6247
- const rel = path12.relative(projectRoot, f).replace(/\\/g, "/");
6248
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path12.basename(f)) && !isGitIgnored(rel, false);
7610
+ const rel = path13.relative(projectRoot, f).replace(/\\/g, "/");
7611
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path13.basename(f)) && !isGitIgnored(rel, false);
6249
7612
  });
6250
7613
  } else {
6251
7614
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -6276,12 +7639,14 @@ async function runIndexerWithStore(store, opts) {
6276
7639
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
6277
7640
  symbolsIndexed += meta.symbolCount;
6278
7641
  filesIndexed++;
7642
+ filesSkipped++;
6279
7643
  filesPreSkipped++;
6280
7644
  return false;
6281
7645
  });
6282
7646
  if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
6283
7647
  }
6284
7648
  const parallelBatch = resolveParallelBatch();
7649
+ const parserPoolCandidateCount = files.length;
6285
7650
  let filesSinceLastYield = 0;
6286
7651
  for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
6287
7652
  const batchEnd = Math.min(batchStart + parallelBatch, files.length);
@@ -6302,7 +7667,7 @@ async function runIndexerWithStore(store, opts) {
6302
7667
  async (file) => {
6303
7668
  let stat2;
6304
7669
  try {
6305
- stat2 = await fs10.stat(file, statOpts);
7670
+ stat2 = await fs11.stat(file, statOpts);
6306
7671
  } catch (e) {
6307
7672
  if (isAbortError(e)) throw e;
6308
7673
  return {
@@ -6332,7 +7697,7 @@ async function runIndexerWithStore(store, opts) {
6332
7697
  }
6333
7698
  let content;
6334
7699
  try {
6335
- content = await fs10.readFile(file, { encoding: "utf8", signal });
7700
+ content = await fs11.readFile(file, { encoding: "utf8", signal });
6336
7701
  } catch (e) {
6337
7702
  if (isAbortError(e)) throw e;
6338
7703
  return {
@@ -6343,22 +7708,78 @@ async function runIndexerWithStore(store, opts) {
6343
7708
  error: `read error: ${e instanceof Error ? e.message : String(e)}`
6344
7709
  };
6345
7710
  }
6346
- let parsed;
6347
- try {
6348
- parsed = await parseFileContent(file, content, lang);
6349
- } catch (e) {
7711
+ const contentHash = xxhash64String(content);
7712
+ if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
6350
7713
  return {
6351
7714
  file,
6352
7715
  stat: stat2,
6353
7716
  lang,
6354
7717
  parsed: null,
6355
- error: `parse error: ${e instanceof Error ? e.message : String(e)}`
7718
+ content,
7719
+ contentHash,
7720
+ skippedMeta: { ...meta, mtimeMs: Math.floor(stat2.mtimeMs) }
6356
7721
  };
6357
7722
  }
6358
- return { file, stat: stat2, lang, parsed, content };
7723
+ return { file, stat: stat2, lang, parsed: null, content, contentHash };
6359
7724
  }
6360
7725
  )
6361
7726
  );
7727
+ const toParse = [];
7728
+ for (let pi = 0; pi < statReadParse.length; pi++) {
7729
+ const s = statReadParse[pi];
7730
+ if (s.status !== "fulfilled") continue;
7731
+ const r = s.value;
7732
+ if (r.error || r.skippedMeta || !r.lang || r.parsed) continue;
7733
+ if (r.content === void 0) continue;
7734
+ toParse.push({
7735
+ index: pi,
7736
+ file: batchFiles[pi],
7737
+ content: r.content,
7738
+ lang: r.lang
7739
+ });
7740
+ }
7741
+ if (toParse.length > 0) {
7742
+ let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
7743
+ if (pool) {
7744
+ try {
7745
+ await pool.ensureReady();
7746
+ const parsedResults = await pool.parseFiles(
7747
+ toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
7748
+ );
7749
+ const byFile = new Map(parsedResults.map((r) => [r.file, r]));
7750
+ for (const item of toParse) {
7751
+ const parsed = byFile.get(item.file);
7752
+ const settled = statReadParse[item.index];
7753
+ if (settled.status !== "fulfilled") continue;
7754
+ if (parsed) {
7755
+ settled.value.parsed = parsed;
7756
+ } else {
7757
+ settled.value.error = `parse error: worker returned no result for ${item.file}`;
7758
+ }
7759
+ }
7760
+ } catch {
7761
+ pool = null;
7762
+ }
7763
+ }
7764
+ if (!pool) {
7765
+ await Promise.all(
7766
+ toParse.map(async (item) => {
7767
+ try {
7768
+ const parsed = await parseFileContent(item.file, item.content, item.lang);
7769
+ const settled = statReadParse[item.index];
7770
+ if (settled.status === "fulfilled") {
7771
+ settled.value.parsed = parsed;
7772
+ }
7773
+ } catch (e) {
7774
+ const settled = statReadParse[item.index];
7775
+ if (settled.status === "fulfilled") {
7776
+ settled.value.error = `parse error: ${e instanceof Error ? e.message : String(e)}`;
7777
+ }
7778
+ }
7779
+ })
7780
+ );
7781
+ }
7782
+ }
6362
7783
  const batchEntries = [];
6363
7784
  const deleteForFiles = [];
6364
7785
  for (let fi = 0; fi < statReadParse.length; fi++) {
@@ -6368,12 +7789,14 @@ async function runIndexerWithStore(store, opts) {
6368
7789
  const err = settled.reason;
6369
7790
  if (err instanceof Error && isAbortError(err)) throw err;
6370
7791
  errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
7792
+ filesFailed++;
6371
7793
  continue;
6372
7794
  }
6373
7795
  const result = settled.value;
6374
7796
  if (result.error) {
6375
7797
  if (result.missing) store.deleteFile(file);
6376
7798
  errors.push(`${file}: ${result.error}`);
7799
+ filesFailed++;
6377
7800
  continue;
6378
7801
  }
6379
7802
  const { stat: stat2, lang, parsed } = result;
@@ -6381,6 +7804,18 @@ async function runIndexerWithStore(store, opts) {
6381
7804
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
6382
7805
  symbolsIndexed += result.skippedMeta.symbolCount;
6383
7806
  filesIndexed++;
7807
+ filesSkipped++;
7808
+ const stored = existingMeta.get(file);
7809
+ if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
7810
+ store.upsertFile({
7811
+ file,
7812
+ lang,
7813
+ mtimeMs: result.skippedMeta.mtimeMs,
7814
+ symbolCount: result.skippedMeta.symbolCount,
7815
+ lastIndexed: Date.now(),
7816
+ contentHash: result.skippedMeta.contentHash
7817
+ });
7818
+ }
6384
7819
  continue;
6385
7820
  }
6386
7821
  if (!lang || !parsed) {
@@ -6390,9 +7825,11 @@ async function runIndexerWithStore(store, opts) {
6390
7825
  lang,
6391
7826
  mtimeMs: Math.floor(stat2.mtimeMs),
6392
7827
  symbolCount: 0,
6393
- lastIndexed: Date.now()
7828
+ lastIndexed: Date.now(),
7829
+ contentHash: result.contentHash ?? ""
6394
7830
  });
6395
7831
  filesIndexed++;
7832
+ filesEmpty++;
6396
7833
  }
6397
7834
  continue;
6398
7835
  }
@@ -6402,9 +7839,11 @@ async function runIndexerWithStore(store, opts) {
6402
7839
  lang,
6403
7840
  mtimeMs: Math.floor(stat2.mtimeMs),
6404
7841
  symbolCount: 0,
6405
- lastIndexed: Date.now()
7842
+ lastIndexed: Date.now(),
7843
+ contentHash: result.contentHash ?? ""
6406
7844
  });
6407
7845
  filesIndexed++;
7846
+ filesEmpty++;
6408
7847
  continue;
6409
7848
  }
6410
7849
  batchEntries.push({
@@ -6413,7 +7852,8 @@ async function runIndexerWithStore(store, opts) {
6413
7852
  symbols: parsed.symbols,
6414
7853
  refs: parsed.refs ?? [],
6415
7854
  mtimeMs: Math.floor(stat2.mtimeMs),
6416
- symbolCount: parsed.symbols.length
7855
+ symbolCount: parsed.symbols.length,
7856
+ contentHash: result.contentHash ?? ""
6417
7857
  });
6418
7858
  deleteForFiles.push(file);
6419
7859
  }
@@ -6425,6 +7865,7 @@ async function runIndexerWithStore(store, opts) {
6425
7865
  symbolsIndexed += count;
6426
7866
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
6427
7867
  filesIndexed++;
7868
+ filesParsed++;
6428
7869
  }
6429
7870
  } catch (err) {
6430
7871
  const message = err instanceof Error ? err.message : String(err);
@@ -6437,6 +7878,7 @@ async function runIndexerWithStore(store, opts) {
6437
7878
  symbolsIndexed += symbolsWithIds.length;
6438
7879
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
6439
7880
  filesIndexed++;
7881
+ filesParsed++;
6440
7882
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
6441
7883
  const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
6442
7884
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
@@ -6450,9 +7892,11 @@ async function runIndexerWithStore(store, opts) {
6450
7892
  lang: entry.lang,
6451
7893
  mtimeMs: entry.mtimeMs,
6452
7894
  symbolCount: entry.symbolCount,
6453
- lastIndexed: Date.now()
7895
+ lastIndexed: Date.now(),
7896
+ contentHash: entry.contentHash
6454
7897
  });
6455
7898
  } catch (innerErr) {
7899
+ filesFailed++;
6456
7900
  errors.push(
6457
7901
  `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
6458
7902
  );
@@ -6485,6 +7929,12 @@ async function runIndexerWithStore(store, opts) {
6485
7929
  const durationMs = Date.now() - startMs;
6486
7930
  return {
6487
7931
  filesIndexed,
7932
+ fileOutcomes: {
7933
+ parsed: filesParsed,
7934
+ skipped: filesSkipped,
7935
+ empty: filesEmpty,
7936
+ failed: filesFailed
7937
+ },
6488
7938
  symbolsIndexed,
6489
7939
  langStats,
6490
7940
  durationMs,
@@ -6562,6 +8012,9 @@ function symbolGraphService(args) {
6562
8012
  function incomingCallsService(args) {
6563
8013
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
6564
8014
  try {
8015
+ if (args.transitive) {
8016
+ return store.findTransitiveIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
8017
+ }
6565
8018
  return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
6566
8019
  } finally {
6567
8020
  indexStorePool.release(store);
@@ -6570,6 +8023,9 @@ function incomingCallsService(args) {
6570
8023
  function outgoingCallsService(args) {
6571
8024
  const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
6572
8025
  try {
8026
+ if (args.transitive) {
8027
+ return store.findTransitiveOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
8028
+ }
6573
8029
  return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
6574
8030
  } finally {
6575
8031
  indexStorePool.release(store);
@@ -6632,7 +8088,7 @@ function resolveWorkerUrl() {
6632
8088
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
6633
8089
  try {
6634
8090
  const url = new URL(rel, import.meta.url);
6635
- if (url.protocol === "file:" && fs11.existsSync(fileURLToPath3(url))) return url;
8091
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath5(url))) return url;
6636
8092
  } catch {
6637
8093
  }
6638
8094
  }
@@ -6652,7 +8108,7 @@ function ensureWorker() {
6652
8108
  return null;
6653
8109
  }
6654
8110
  try {
6655
- const w = new Worker(url, { name: "wstack-codebase-index" });
8111
+ const w = new Worker2(url, { name: "wstack-codebase-index" });
6656
8112
  w.unref();
6657
8113
  w.on("message", (msg) => {
6658
8114
  if (msg.type === "progress") {
@@ -7125,6 +8581,11 @@ var codebaseIncomingCallsTool = {
7125
8581
  description: "Maximum call sites to return (default 50, max 200)",
7126
8582
  minimum: 1,
7127
8583
  maximum: 200
8584
+ },
8585
+ transitive: {
8586
+ type: "boolean",
8587
+ description: "When true, traverse the full transitive call chain (callers of callers, to unlimited depth). Default false: return only direct callers. Cycle-safe via SQL recursive CTE.",
8588
+ default: false
7128
8589
  }
7129
8590
  },
7130
8591
  required: ["symbol"]
@@ -7150,13 +8611,15 @@ var codebaseIncomingCallsTool = {
7150
8611
  };
7151
8612
  }
7152
8613
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
8614
+ const transitive = input.transitive === true;
7153
8615
  const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
7154
8616
  {
7155
8617
  projectRoot: ctx.projectRoot,
7156
8618
  indexDir: codebaseIndexDirOverride(ctx),
7157
8619
  symbol: input.symbol,
7158
8620
  file: input.file,
7159
- limit
8621
+ limit,
8622
+ transitive
7160
8623
  }
7161
8624
  );
7162
8625
  if (!symbolFound) {
@@ -7229,6 +8692,11 @@ var codebaseOutgoingCallsTool = {
7229
8692
  description: "Maximum call sites to return (default 50, max 200)",
7230
8693
  minimum: 1,
7231
8694
  maximum: 200
8695
+ },
8696
+ transitive: {
8697
+ type: "boolean",
8698
+ description: "When true, traverse the full transitive dependency chain (callees of callees, to unlimited depth). Default false: return only direct callees. Cycle-safe via SQL recursive CTE.",
8699
+ default: false
7232
8700
  }
7233
8701
  },
7234
8702
  required: ["symbol"]
@@ -7254,13 +8722,15 @@ var codebaseOutgoingCallsTool = {
7254
8722
  };
7255
8723
  }
7256
8724
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
8725
+ const transitive = input.transitive === true;
7257
8726
  const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
7258
8727
  {
7259
8728
  projectRoot: ctx.projectRoot,
7260
8729
  indexDir: codebaseIndexDirOverride(ctx),
7261
8730
  symbol: input.symbol,
7262
8731
  file: input.file,
7263
- limit
8732
+ limit,
8733
+ transitive
7264
8734
  }
7265
8735
  );
7266
8736
  if (!symbolFound) {
@@ -7520,8 +8990,8 @@ var codebaseStatsTool = {
7520
8990
 
7521
8991
  // src/codebase-index/dead-code-scan.ts
7522
8992
  init_languages();
7523
- import * as fs12 from "node:fs";
7524
- import * as path13 from "node:path";
8993
+ import * as fs13 from "node:fs";
8994
+ import * as path14 from "node:path";
7525
8995
  var deadCodeScanTool = {
7526
8996
  name: "dead-code-scan",
7527
8997
  category: "Project",
@@ -7566,25 +9036,25 @@ var deadCodeScanTool = {
7566
9036
  };
7567
9037
  function tryReadJson(filePath) {
7568
9038
  try {
7569
- const raw = fs12.readFileSync(filePath, "utf8");
9039
+ const raw = fs13.readFileSync(filePath, "utf8");
7570
9040
  return JSON.parse(raw);
7571
9041
  } catch {
7572
9042
  return null;
7573
9043
  }
7574
9044
  }
7575
9045
  function resolveAgainst(base, relative2) {
7576
- if (path13.isAbsolute(relative2)) return relative2;
7577
- return path13.resolve(base, relative2);
9046
+ if (path14.isAbsolute(relative2)) return relative2;
9047
+ return path14.resolve(base, relative2);
7578
9048
  }
7579
9049
  function discoverEntryPoints(projectRoot, userEntryPoints) {
7580
9050
  const entries = /* @__PURE__ */ new Set();
7581
9051
  if (userEntryPoints) {
7582
9052
  for (const ep of userEntryPoints) {
7583
9053
  const resolved = resolveAgainst(projectRoot, ep);
7584
- if (fs12.existsSync(resolved)) entries.add(resolved);
9054
+ if (fs13.existsSync(resolved)) entries.add(resolved);
7585
9055
  }
7586
9056
  }
7587
- const rootPkg = tryReadJson(path13.join(projectRoot, "package.json"));
9057
+ const rootPkg = tryReadJson(path14.join(projectRoot, "package.json"));
7588
9058
  if (rootPkg) {
7589
9059
  addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
7590
9060
  }
@@ -7598,45 +9068,45 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
7598
9068
  workspaces = [];
7599
9069
  }
7600
9070
  for (const wsDir of workspaces) {
7601
- const pkgJsonPath = path13.join(wsDir, "package.json");
9071
+ const pkgJsonPath = path14.join(wsDir, "package.json");
7602
9072
  const pkg = tryReadJson(pkgJsonPath);
7603
9073
  if (pkg) {
7604
9074
  addPkgJsonEntryPoints(wsDir, pkg, entries);
7605
- const convention1 = path13.join(wsDir, "src", "index.ts");
7606
- if (fs12.existsSync(convention1)) entries.add(convention1);
7607
- const convention2 = path13.join(wsDir, "src", "main.ts");
7608
- if (fs12.existsSync(convention2)) entries.add(convention2);
7609
- const convention3 = path13.join(wsDir, "index.ts");
7610
- if (fs12.existsSync(convention3)) entries.add(convention3);
9075
+ const convention1 = path14.join(wsDir, "src", "index.ts");
9076
+ if (fs13.existsSync(convention1)) entries.add(convention1);
9077
+ const convention2 = path14.join(wsDir, "src", "main.ts");
9078
+ if (fs13.existsSync(convention2)) entries.add(convention2);
9079
+ const convention3 = path14.join(wsDir, "index.ts");
9080
+ if (fs13.existsSync(convention3)) entries.add(convention3);
7611
9081
  }
7612
9082
  }
7613
9083
  if (!rootPkg || !workspaces.length) {
7614
9084
  for (const name of ["src/index.ts", "src/main.ts", "index.ts"]) {
7615
- const convention = path13.join(projectRoot, name);
7616
- if (fs12.existsSync(convention)) entries.add(convention);
9085
+ const convention = path14.join(projectRoot, name);
9086
+ if (fs13.existsSync(convention)) entries.add(convention);
7617
9087
  }
7618
9088
  }
7619
9089
  return [...entries];
7620
9090
  }
7621
9091
  var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
7622
- var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path13.sep}${d}${path13.sep}`);
9092
+ var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path14.sep}${d}${path14.sep}`);
7623
9093
  function trySourceEquivalent(resolved) {
7624
- resolved = resolved.replace(/[/\\]/g, path13.sep);
9094
+ resolved = resolved.replace(/[/\\]/g, path14.sep);
7625
9095
  for (const marker of BUILD_OUTPUT_DIR_NAMES) {
7626
9096
  const idx = resolved.indexOf(marker);
7627
9097
  if (idx === -1) continue;
7628
- const base = resolved.replace(marker, `${path13.sep}src${path13.sep}`);
9098
+ const base = resolved.replace(marker, `${path14.sep}src${path14.sep}`);
7629
9099
  const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
7630
- if (candidate !== base && fs12.existsSync(candidate)) {
9100
+ if (candidate !== base && fs13.existsSync(candidate)) {
7631
9101
  return candidate;
7632
9102
  }
7633
9103
  const dtsStripped = base.replace(/\.d\.ts$/, "");
7634
9104
  const candidateDts = dtsStripped + ".ts";
7635
- if (candidateDts !== base && candidateDts !== candidate && fs12.existsSync(candidateDts)) {
9105
+ if (candidateDts !== base && candidateDts !== candidate && fs13.existsSync(candidateDts)) {
7636
9106
  return candidateDts;
7637
9107
  }
7638
9108
  const candidateNoExt = base + ".ts";
7639
- if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs12.existsSync(candidateNoExt)) {
9109
+ if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs13.existsSync(candidateNoExt)) {
7640
9110
  return candidateNoExt;
7641
9111
  }
7642
9112
  }
@@ -7644,9 +9114,9 @@ function trySourceEquivalent(resolved) {
7644
9114
  }
7645
9115
  function tryAddEntryPath(pkgDir, rawPath, entries) {
7646
9116
  const resolved = resolveAgainst(pkgDir, rawPath);
7647
- if (fs12.existsSync(resolved)) entries.add(resolved);
9117
+ if (fs13.existsSync(resolved)) entries.add(resolved);
7648
9118
  const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
7649
- if (tsResolved !== resolved && fs12.existsSync(tsResolved)) {
9119
+ if (tsResolved !== resolved && fs13.existsSync(tsResolved)) {
7650
9120
  entries.add(tsResolved);
7651
9121
  }
7652
9122
  const srcAlt = trySourceEquivalent(resolved);
@@ -7690,18 +9160,18 @@ function expandGlobPattern(entry, projectRoot) {
7690
9160
  const dirs = [];
7691
9161
  if (entry.includes("*")) {
7692
9162
  const base = entry.replace(/\/\*+$/, "");
7693
- const baseDir = path13.resolve(projectRoot, base);
9163
+ const baseDir = path14.resolve(projectRoot, base);
7694
9164
  try {
7695
- const children = fs12.readdirSync(baseDir, { withFileTypes: true });
9165
+ const children = fs13.readdirSync(baseDir, { withFileTypes: true });
7696
9166
  for (const child of children) {
7697
9167
  if (child.isDirectory()) {
7698
- dirs.push(path13.join(baseDir, child.name));
9168
+ dirs.push(path14.join(baseDir, child.name));
7699
9169
  }
7700
9170
  }
7701
9171
  } catch {
7702
9172
  }
7703
9173
  } else {
7704
- dirs.push(path13.resolve(projectRoot, entry));
9174
+ dirs.push(path14.resolve(projectRoot, entry));
7705
9175
  }
7706
9176
  return dirs;
7707
9177
  }
@@ -7718,10 +9188,10 @@ function extractWorkspaceGlobs(pkg, projectRoot) {
7718
9188
  return dirs;
7719
9189
  }
7720
9190
  function extractPnpmWorkspaceDirs(projectRoot) {
7721
- const yamlPath = path13.join(projectRoot, "pnpm-workspace.yaml");
7722
- if (!fs12.existsSync(yamlPath)) return [];
9191
+ const yamlPath = path14.join(projectRoot, "pnpm-workspace.yaml");
9192
+ if (!fs13.existsSync(yamlPath)) return [];
7723
9193
  try {
7724
- const content = fs12.readFileSync(yamlPath, "utf8");
9194
+ const content = fs13.readFileSync(yamlPath, "utf8");
7725
9195
  const dirs = [];
7726
9196
  let inPackages = false;
7727
9197
  const lines = content.split("\n");
@@ -7755,8 +9225,8 @@ function extractPnpmWorkspaceDirs(projectRoot) {
7755
9225
  }
7756
9226
  function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
7757
9227
  if (!moduleSpecifier.startsWith(".")) return [];
7758
- const dir = path13.dirname(importerPath);
7759
- const base = path13.resolve(dir, moduleSpecifier);
9228
+ const dir = path14.dirname(importerPath);
9229
+ const base = path14.resolve(dir, moduleSpecifier);
7760
9230
  const results = [];
7761
9231
  const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
7762
9232
  const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
@@ -7768,18 +9238,18 @@ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
7768
9238
  if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
7769
9239
  if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
7770
9240
  if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
7771
- if (indexedFiles.has(path13.join(candidate, "index.ts")))
7772
- results.push(path13.join(candidate, "index.ts"));
7773
- if (indexedFiles.has(path13.join(candidate, "index.tsx")))
7774
- results.push(path13.join(candidate, "index.tsx"));
7775
- if (indexedFiles.has(path13.join(candidate, "index.js")))
7776
- results.push(path13.join(candidate, "index.js"));
7777
- if (indexedFiles.has(path13.join(candidate, "index.jsx")))
7778
- results.push(path13.join(candidate, "index.jsx"));
7779
- if (indexedFiles.has(path13.join(candidate, "index.mjs")))
7780
- results.push(path13.join(candidate, "index.mjs"));
7781
- if (indexedFiles.has(path13.join(candidate, "index.cjs")))
7782
- results.push(path13.join(candidate, "index.cjs"));
9241
+ if (indexedFiles.has(path14.join(candidate, "index.ts")))
9242
+ results.push(path14.join(candidate, "index.ts"));
9243
+ if (indexedFiles.has(path14.join(candidate, "index.tsx")))
9244
+ results.push(path14.join(candidate, "index.tsx"));
9245
+ if (indexedFiles.has(path14.join(candidate, "index.js")))
9246
+ results.push(path14.join(candidate, "index.js"));
9247
+ if (indexedFiles.has(path14.join(candidate, "index.jsx")))
9248
+ results.push(path14.join(candidate, "index.jsx"));
9249
+ if (indexedFiles.has(path14.join(candidate, "index.mjs")))
9250
+ results.push(path14.join(candidate, "index.mjs"));
9251
+ if (indexedFiles.has(path14.join(candidate, "index.cjs")))
9252
+ results.push(path14.join(candidate, "index.cjs"));
7783
9253
  }
7784
9254
  return [...new Set(results)];
7785
9255
  }
@@ -7806,22 +9276,12 @@ function runDeadCodeScan(projectRoot, opts = {}) {
7806
9276
  const store = opts.store ?? indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
7807
9277
  try {
7808
9278
  const allSymbols = store.getAllSymbols();
7809
- const allRefs = store.getAllResolvedRefs();
7810
9279
  const symbolById = /* @__PURE__ */ new Map();
7811
9280
  for (const s of allSymbols) {
7812
9281
  symbolById.set(s.id, s);
7813
9282
  }
7814
- const references = /* @__PURE__ */ new Map();
7815
- for (const ref of allRefs) {
7816
- let set = references.get(ref.fromId);
7817
- if (!set) {
7818
- set = /* @__PURE__ */ new Set();
7819
- references.set(ref.fromId, set);
7820
- }
7821
- set.add(ref.toId);
7822
- }
7823
9283
  const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
7824
- const entryFileSet = new Set(discoveredFiles.map((f) => path13.resolve(f)));
9284
+ const entryFileSet = new Set(discoveredFiles.map((f) => path14.resolve(f)));
7825
9285
  const indexedFiles = /* @__PURE__ */ new Set();
7826
9286
  for (const s of allSymbols) indexedFiles.add(s.file);
7827
9287
  for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
@@ -7847,7 +9307,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
7847
9307
  if (scannedBarrels.has(epFile)) continue;
7848
9308
  scannedBarrels.add(epFile);
7849
9309
  try {
7850
- const content = fs12.readFileSync(epFile, "utf8");
9310
+ const content = fs13.readFileSync(epFile, "utf8");
7851
9311
  const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
7852
9312
  const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
7853
9313
  for (; ; ) {
@@ -7888,23 +9348,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
7888
9348
  }
7889
9349
  }
7890
9350
  }
7891
- const alive = new Set(seedIds);
7892
- const frontier = [...seedIds];
7893
- const visitedEdges = /* @__PURE__ */ new Set();
7894
- while (frontier.length > 0) {
7895
- const current = frontier.pop();
7896
- const outgoing = references.get(current);
7897
- if (!outgoing) continue;
7898
- for (const toId of outgoing) {
7899
- const edgeKey = `${current}->${toId}`;
7900
- if (visitedEdges.has(edgeKey)) continue;
7901
- visitedEdges.add(edgeKey);
7902
- if (!alive.has(toId)) {
7903
- alive.add(toId);
7904
- frontier.push(toId);
7905
- }
7906
- }
7907
- }
9351
+ const alive = store.findReachableSymbolIds([...seedIds]);
7908
9352
  const dead = [];
7909
9353
  const symbolsByFile = /* @__PURE__ */ new Map();
7910
9354
  const usedFiles = /* @__PURE__ */ new Set();
@@ -7953,7 +9397,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
7953
9397
  const deadPackages = [];
7954
9398
  const pkgEntries = findPackageEntries(projectRoot);
7955
9399
  for (const [pkgName, pkgDir] of pkgEntries) {
7956
- const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path13.sep));
9400
+ const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path14.sep));
7957
9401
  if (pkgFiles.length === 0) continue;
7958
9402
  const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
7959
9403
  if (pkgUsed.length === 0) {
@@ -7989,7 +9433,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
7989
9433
  }
7990
9434
  function findPackageEntries(projectRoot) {
7991
9435
  const pkgMap = /* @__PURE__ */ new Map();
7992
- const rootPkg = tryReadJson(path13.join(projectRoot, "package.json"));
9436
+ const rootPkg = tryReadJson(path14.join(projectRoot, "package.json"));
7993
9437
  if (rootPkg && typeof rootPkg.name === "string") {
7994
9438
  pkgMap.set(rootPkg.name, projectRoot);
7995
9439
  }
@@ -7999,7 +9443,7 @@ function findPackageEntries(projectRoot) {
7999
9443
  wsDirs = extractPnpmWorkspaceDirs(projectRoot);
8000
9444
  }
8001
9445
  for (const wsDir of wsDirs) {
8002
- const wsPkg = tryReadJson(path13.join(wsDir, "package.json"));
9446
+ const wsPkg = tryReadJson(path14.join(wsDir, "package.json"));
8003
9447
  if (wsPkg && typeof wsPkg.name === "string") {
8004
9448
  pkgMap.set(wsPkg.name, wsDir);
8005
9449
  }