@optave/codegraph 3.9.6 → 3.11.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 (186) hide show
  1. package/README.md +26 -12
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +1 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +50 -8
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/audit.js +1 -1
  12. package/dist/cli/commands/audit.js.map +1 -1
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +2 -0
  15. package/dist/cli/commands/build.js.map +1 -1
  16. package/dist/cli/commands/check.js +1 -1
  17. package/dist/cli/commands/check.js.map +1 -1
  18. package/dist/cli/commands/children.js +1 -1
  19. package/dist/cli/commands/children.js.map +1 -1
  20. package/dist/cli/commands/diff-impact.js +1 -1
  21. package/dist/cli/commands/diff-impact.js.map +1 -1
  22. package/dist/cli/commands/roles.js +1 -1
  23. package/dist/cli/commands/roles.js.map +1 -1
  24. package/dist/cli/commands/structure.js +1 -1
  25. package/dist/cli/commands/structure.js.map +1 -1
  26. package/dist/cli/shared/options.js +1 -1
  27. package/dist/cli/shared/options.js.map +1 -1
  28. package/dist/db/connection.d.ts.map +1 -1
  29. package/dist/db/connection.js +8 -0
  30. package/dist/db/connection.js.map +1 -1
  31. package/dist/domain/graph/builder/context.d.ts +10 -0
  32. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/context.js +10 -0
  34. package/dist/domain/graph/builder/context.js.map +1 -1
  35. package/dist/domain/graph/builder/helpers.d.ts +7 -2
  36. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/helpers.js +7 -2
  38. package/dist/domain/graph/builder/helpers.js.map +1 -1
  39. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +6 -23
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  44. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/pipeline.js +348 -42
  46. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/stages/collect-files.js +8 -0
  52. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
  54. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  55. package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
  56. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  57. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  58. package/dist/domain/graph/builder/stages/finalize.js +9 -6
  59. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  60. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
  61. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
  63. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  66. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  67. package/dist/domain/graph/watcher.d.ts.map +1 -1
  68. package/dist/domain/graph/watcher.js +23 -18
  69. package/dist/domain/graph/watcher.js.map +1 -1
  70. package/dist/domain/parser.d.ts +14 -1
  71. package/dist/domain/parser.d.ts.map +1 -1
  72. package/dist/domain/parser.js +104 -11
  73. package/dist/domain/parser.js.map +1 -1
  74. package/dist/domain/search/models.d.ts +16 -0
  75. package/dist/domain/search/models.d.ts.map +1 -1
  76. package/dist/domain/search/models.js +36 -2
  77. package/dist/domain/search/models.js.map +1 -1
  78. package/dist/domain/wasm-worker-entry.js +20 -13
  79. package/dist/domain/wasm-worker-entry.js.map +1 -1
  80. package/dist/extractors/c.js +25 -6
  81. package/dist/extractors/c.js.map +1 -1
  82. package/dist/extractors/cpp.js +47 -6
  83. package/dist/extractors/cpp.js.map +1 -1
  84. package/dist/extractors/cuda.js +90 -14
  85. package/dist/extractors/cuda.js.map +1 -1
  86. package/dist/extractors/elixir.js +83 -3
  87. package/dist/extractors/elixir.js.map +1 -1
  88. package/dist/extractors/erlang.js +56 -20
  89. package/dist/extractors/erlang.js.map +1 -1
  90. package/dist/extractors/fsharp.d.ts +7 -0
  91. package/dist/extractors/fsharp.d.ts.map +1 -1
  92. package/dist/extractors/fsharp.js +94 -0
  93. package/dist/extractors/fsharp.js.map +1 -1
  94. package/dist/extractors/gleam.js +6 -2
  95. package/dist/extractors/gleam.js.map +1 -1
  96. package/dist/extractors/groovy.js +41 -1
  97. package/dist/extractors/groovy.js.map +1 -1
  98. package/dist/extractors/haskell.js +48 -4
  99. package/dist/extractors/haskell.js.map +1 -1
  100. package/dist/extractors/julia.js +172 -41
  101. package/dist/extractors/julia.js.map +1 -1
  102. package/dist/extractors/kotlin.js +4 -0
  103. package/dist/extractors/kotlin.js.map +1 -1
  104. package/dist/extractors/objc.js +184 -47
  105. package/dist/extractors/objc.js.map +1 -1
  106. package/dist/extractors/python.js +7 -4
  107. package/dist/extractors/python.js.map +1 -1
  108. package/dist/extractors/r.js +93 -52
  109. package/dist/extractors/r.js.map +1 -1
  110. package/dist/extractors/scala.d.ts.map +1 -1
  111. package/dist/extractors/scala.js +18 -32
  112. package/dist/extractors/scala.js.map +1 -1
  113. package/dist/extractors/solidity.js +18 -9
  114. package/dist/extractors/solidity.js.map +1 -1
  115. package/dist/extractors/verilog.js +80 -15
  116. package/dist/extractors/verilog.js.map +1 -1
  117. package/dist/infrastructure/config.d.ts +1 -0
  118. package/dist/infrastructure/config.d.ts.map +1 -1
  119. package/dist/infrastructure/config.js +1 -0
  120. package/dist/infrastructure/config.js.map +1 -1
  121. package/dist/mcp/server.d.ts.map +1 -1
  122. package/dist/mcp/server.js +14 -8
  123. package/dist/mcp/server.js.map +1 -1
  124. package/dist/mcp/tool-registry.d.ts +1 -1
  125. package/dist/mcp/tool-registry.d.ts.map +1 -1
  126. package/dist/mcp/tool-registry.js +23 -5
  127. package/dist/mcp/tool-registry.js.map +1 -1
  128. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  129. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  130. package/dist/mcp/tools/semantic-search.js +1 -0
  131. package/dist/mcp/tools/semantic-search.js.map +1 -1
  132. package/dist/types.d.ts +16 -1
  133. package/dist/types.d.ts.map +1 -1
  134. package/grammars/tree-sitter-erlang.wasm +0 -0
  135. package/grammars/tree-sitter-fsharp.wasm +0 -0
  136. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  137. package/grammars/tree-sitter-gleam.wasm +0 -0
  138. package/package.json +11 -10
  139. package/src/ast-analysis/engine.ts +3 -1
  140. package/src/ast-analysis/rules/index.ts +87 -0
  141. package/src/ast-analysis/visitors/ast-store-visitor.ts +45 -9
  142. package/src/cli/commands/audit.ts +1 -1
  143. package/src/cli/commands/build.ts +2 -0
  144. package/src/cli/commands/check.ts +1 -1
  145. package/src/cli/commands/children.ts +1 -1
  146. package/src/cli/commands/diff-impact.ts +1 -1
  147. package/src/cli/commands/roles.ts +1 -1
  148. package/src/cli/commands/structure.ts +1 -1
  149. package/src/cli/shared/options.ts +1 -1
  150. package/src/db/connection.ts +8 -0
  151. package/src/domain/graph/builder/context.ts +10 -0
  152. package/src/domain/graph/builder/helpers.ts +8 -3
  153. package/src/domain/graph/builder/incremental.ts +6 -41
  154. package/src/domain/graph/builder/pipeline.ts +404 -41
  155. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  156. package/src/domain/graph/builder/stages/collect-files.ts +9 -0
  157. package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
  158. package/src/domain/graph/builder/stages/finalize.ts +9 -6
  159. package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
  160. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  161. package/src/domain/graph/watcher.ts +21 -23
  162. package/src/domain/parser.ts +110 -10
  163. package/src/domain/search/models.ts +37 -2
  164. package/src/domain/wasm-worker-entry.ts +20 -13
  165. package/src/extractors/c.ts +27 -8
  166. package/src/extractors/cpp.ts +50 -8
  167. package/src/extractors/cuda.ts +90 -16
  168. package/src/extractors/elixir.ts +75 -3
  169. package/src/extractors/erlang.ts +63 -20
  170. package/src/extractors/fsharp.ts +104 -0
  171. package/src/extractors/gleam.ts +7 -2
  172. package/src/extractors/groovy.ts +45 -1
  173. package/src/extractors/haskell.ts +45 -4
  174. package/src/extractors/julia.ts +164 -43
  175. package/src/extractors/kotlin.ts +4 -0
  176. package/src/extractors/objc.ts +171 -47
  177. package/src/extractors/python.ts +5 -3
  178. package/src/extractors/r.ts +88 -48
  179. package/src/extractors/scala.ts +24 -36
  180. package/src/extractors/solidity.ts +17 -8
  181. package/src/extractors/verilog.ts +83 -15
  182. package/src/infrastructure/config.ts +1 -0
  183. package/src/mcp/server.ts +16 -9
  184. package/src/mcp/tool-registry.ts +28 -5
  185. package/src/mcp/tools/semantic-search.ts +2 -0
  186. package/src/types.ts +16 -0
@@ -316,16 +316,23 @@ export function getParser(parsers: Map<string, Parser | null>, filePath: string)
316
316
  *
317
317
  * Name is preserved for caller compatibility; the function now ensures
318
318
  * *analysis data* rather than *trees*.
319
+ *
320
+ * `needsFn` (optional): when provided, only files for which it returns true are
321
+ * re-parsed. Without it the function falls back to "any WASM-parseable file
322
+ * without _tree", which was the source of #1036 — a single file missing one
323
+ * analysis triggered a full-build re-parse of every WASM-parseable file.
319
324
  */
320
325
  export async function ensureWasmTrees(
321
326
  fileSymbols: Map<string, any>,
322
327
  rootDir: string,
328
+ needsFn?: (relPath: string, symbols: any) => boolean,
323
329
  ): Promise<void> {
324
330
  // Collect files that still need analysis data and are parseable by WASM.
325
331
  const pending: Array<{ relPath: string; absPath: string; symbols: any }> = [];
326
332
  for (const [relPath, symbols] of fileSymbols) {
327
333
  if (symbols._tree) continue; // legacy path — leave existing trees alone
328
334
  if (!_extToLang.has(path.extname(relPath).toLowerCase())) continue;
335
+ if (needsFn && !needsFn(relPath, symbols)) continue;
329
336
  pending.push({ relPath, absPath: path.join(rootDir, relPath), symbols });
330
337
  }
331
338
  if (pending.length === 0) return;
@@ -450,6 +457,8 @@ export const NATIVE_SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
450
457
  '.cc',
451
458
  '.cxx',
452
459
  '.hpp',
460
+ '.cu',
461
+ '.cuh',
453
462
  '.kt',
454
463
  '.kts',
455
464
  '.swift',
@@ -464,6 +473,23 @@ export const NATIVE_SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
464
473
  '.hs',
465
474
  '.ml',
466
475
  '.mli',
476
+ '.fs',
477
+ '.fsx',
478
+ '.fsi',
479
+ '.m',
480
+ '.gleam',
481
+ '.jl',
482
+ '.clj',
483
+ '.cljs',
484
+ '.cljc',
485
+ '.erl',
486
+ '.hrl',
487
+ '.groovy',
488
+ '.gvy',
489
+ '.r',
490
+ '.sol',
491
+ '.v',
492
+ '.sv',
467
493
  ]);
468
494
 
469
495
  /**
@@ -805,11 +831,18 @@ export const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
805
831
  },
806
832
  {
807
833
  id: 'fsharp',
808
- extensions: ['.fs', '.fsx', '.fsi'],
834
+ extensions: ['.fs', '.fsx'],
809
835
  grammarFile: 'tree-sitter-fsharp.wasm',
810
836
  extractor: extractFSharpSymbols,
811
837
  required: false,
812
838
  },
839
+ {
840
+ id: 'fsharp-signature',
841
+ extensions: ['.fsi'],
842
+ grammarFile: 'tree-sitter-fsharp_signature.wasm',
843
+ extractor: extractFSharpSymbols,
844
+ required: false,
845
+ },
813
846
  {
814
847
  id: 'gleam',
815
848
  extensions: ['.gleam'],
@@ -1060,6 +1093,71 @@ async function parseFilesWasm(
1060
1093
  return result;
1061
1094
  }
1062
1095
 
1096
+ /**
1097
+ * Files at or below this count use the inline parse path (no worker spawn).
1098
+ *
1099
+ * Sized for typical engine-parity drops: a handful of fixture files in one
1100
+ * or two languages (the recurring HCL case is 4 files). Above this, the
1101
+ * worker-pool's IPC + crash-isolation cost (#965) is amortized over enough
1102
+ * parse work to be worth paying; below it, the ~1–2s cold-start dominates.
1103
+ */
1104
+ const INLINE_BACKFILL_THRESHOLD = 16;
1105
+
1106
+ /**
1107
+ * Inline WASM parse (no worker) for small file batches.
1108
+ *
1109
+ * Used by the engine-parity backfill path when the native engine drops a
1110
+ * handful of files (typically test fixtures). The worker pool's per-call
1111
+ * IPC + grammar-init overhead can cost 1–2s on slow CI runners — for a
1112
+ * 4-file backfill, that dwarfs the ~10ms of actual parse work.
1113
+ *
1114
+ * Returns symbols with `_tree` set so `runAnalyses` can run AST/CFG/dataflow
1115
+ * visitors via the unified walker (mirrors how WASM-engine results behaved
1116
+ * before the worker pool was introduced).
1117
+ */
1118
+ async function parseFilesWasmInline(
1119
+ filePaths: string[],
1120
+ rootDir: string,
1121
+ ): Promise<Map<string, ExtractorOutput>> {
1122
+ const result = new Map<string, ExtractorOutput>();
1123
+ if (filePaths.length === 0) return result;
1124
+ const parsers = await ensureParsersForFiles(filePaths);
1125
+ for (const filePath of filePaths) {
1126
+ if (!_extToLang.has(path.extname(filePath).toLowerCase())) continue;
1127
+ let code: string;
1128
+ try {
1129
+ code = fs.readFileSync(filePath, 'utf-8');
1130
+ } catch (err: unknown) {
1131
+ warn(`Skipping ${path.relative(rootDir, filePath)}: ${(err as Error).message}`);
1132
+ continue;
1133
+ }
1134
+ const extracted = wasmExtractSymbols(parsers, filePath, code);
1135
+ if (!extracted) continue;
1136
+ const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
1137
+ const symbols = extracted.symbols as ExtractorOutput & { _tree?: unknown; _langId?: string };
1138
+ symbols._tree = extracted.tree;
1139
+ symbols._langId = extracted.langId;
1140
+ result.set(relPath, symbols);
1141
+ }
1142
+ return result;
1143
+ }
1144
+
1145
+ /**
1146
+ * Backfill helper: small batches use the inline (main-thread) path; larger
1147
+ * batches keep the worker-pool isolation against tree-sitter WASM crashes
1148
+ * (#965). Threshold matches typical engine-parity drop sizes (a few fixture
1149
+ * files in one or two languages).
1150
+ */
1151
+ export async function parseFilesWasmForBackfill(
1152
+ filePaths: string[],
1153
+ rootDir: string,
1154
+ ): Promise<Map<string, ExtractorOutput>> {
1155
+ if (filePaths.length <= INLINE_BACKFILL_THRESHOLD) {
1156
+ return parseFilesWasmInline(filePaths, rootDir);
1157
+ }
1158
+ return parseFilesWasm(filePaths, rootDir);
1159
+ }
1160
+
1063
1161
  /**
1064
1162
  * Parse multiple files in bulk and return a Map<relPath, symbols>.
1065
1163
  */
@@ -1110,7 +1208,7 @@ export async function parseFilesAuto(
1110
1208
  );
1111
1209
  if (dropped.length > 0) {
1112
1210
  warn(`Native engine dropped ${dropped.length} file(s); falling back to WASM for parity`);
1113
- const wasmResults = await parseFilesWasm(dropped, rootDir);
1211
+ const wasmResults = await parseFilesWasmForBackfill(dropped, rootDir);
1114
1212
  for (const [relPath, symbols] of wasmResults) {
1115
1213
  result.set(relPath, symbols);
1116
1214
  }
@@ -1125,15 +1223,17 @@ export async function parseFilesAuto(
1125
1223
  export function getActiveEngine(opts: ParseEngineOpts = {}): {
1126
1224
  name: 'native' | 'wasm';
1127
1225
  version: string | null;
1226
+ binaryVersion: string | null;
1128
1227
  } {
1129
1228
  const { name, native } = resolveEngine(opts);
1130
- let version: string | null = native
1131
- ? typeof native.engineVersion === 'function'
1132
- ? native.engineVersion()
1133
- : null
1134
- : null;
1135
- // Prefer platform package.json version over binary-embedded version
1136
- // to handle stale binaries that weren't recompiled during a release
1229
+ const binaryVersion: string | null =
1230
+ native && typeof native.engineVersion === 'function' ? native.engineVersion() : null;
1231
+ // The display version prefers the platform package.json so the "Using native
1232
+ // engine (vX)" log matches the npm release the user installed. The Rust
1233
+ // orchestrator's check_version_mismatch compares against CARGO_PKG_VERSION
1234
+ // (the binary's own value), so build_meta writes must use `binaryVersion`,
1235
+ // not this display value see pipeline.ts and finalize.ts (#1066).
1236
+ let version: string | null = binaryVersion;
1137
1237
  if (native) {
1138
1238
  try {
1139
1239
  version = getNativePackageVersion() ?? version;
@@ -1141,7 +1241,7 @@ export function getActiveEngine(opts: ParseEngineOpts = {}): {
1141
1241
  debug(`getNativePackageVersion failed: ${(e as Error).message}`);
1142
1242
  }
1143
1243
  }
1144
- return { name, version };
1244
+ return { name, version, binaryVersion };
1145
1245
  }
1146
1246
 
1147
1247
  /**
@@ -1,8 +1,40 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
2
4
  import { createInterface } from 'node:readline';
3
5
  import { info } from '../../infrastructure/logger.js';
4
6
  import { ConfigError, EngineError } from '../../shared/errors.js';
5
7
 
8
+ const _require = createRequire(import.meta.url);
9
+
10
+ /**
11
+ * Resolve the directory where `npm install` should run so the installed
12
+ * package ends up reachable by `await import(pkg)` from inside this module.
13
+ *
14
+ * Without a `cwd`, `execFileSync('npm', ['install', ...])` operates on
15
+ * `process.cwd()` — when the user runs codegraph against a repo that is *not*
16
+ * the directory where codegraph itself is installed, npm installs into the
17
+ * wrong `node_modules`, the dynamic import still fails, and the user gets
18
+ * `ENGINE_UNAVAILABLE: ... installed but failed to load`.
19
+ *
20
+ * Pin cwd to the directory that contains @optave/codegraph's `node_modules`
21
+ * so the install lands where Node's resolution algorithm will find it.
22
+ *
23
+ * @internal Exported for unit tests; not part of the public barrel.
24
+ */
25
+ export function resolveNpmInstallCwd(): string | undefined {
26
+ try {
27
+ const pkgJsonPath = _require.resolve('@optave/codegraph/package.json');
28
+ // pkgJsonPath = <host>/node_modules/@optave/codegraph/package.json
29
+ // dirname x4: package.json → codegraph → @optave → node_modules → <host>
30
+ return path.dirname(path.dirname(path.dirname(path.dirname(pkgJsonPath))));
31
+ } catch {
32
+ // Source-of-truth checkout (no @optave/codegraph in node_modules) — fall back
33
+ // to process.cwd() so legacy behavior survives in tests.
34
+ return undefined;
35
+ }
36
+ }
37
+
6
38
  export interface ModelConfig {
7
39
  name: string;
8
40
  dim: number;
@@ -42,7 +74,7 @@ export const MODELS: Record<string, ModelConfig> = {
42
74
  quantized: false,
43
75
  },
44
76
  'jina-code': {
45
- name: 'Xenova/jina-embeddings-v2-base-code',
77
+ name: 'jinaai/jina-embeddings-v2-base-code',
46
78
  dim: 768,
47
79
  contextWindow: 8192,
48
80
  desc: 'Code-aware (~137MB). Trained on code+text, best for code search.',
@@ -104,12 +136,14 @@ export function getModelConfig(modelKey?: string): ModelConfig {
104
136
  * @internal Not part of the public barrel.
105
137
  */
106
138
  export function promptInstall(packageName: string): Promise<boolean> {
139
+ const installCwd = resolveNpmInstallCwd();
107
140
  if (!process.stdin.isTTY) {
108
141
  info(`Installing ${packageName} (optional dependency for semantic search)…`);
109
142
  try {
110
143
  execFileSync(NPM_BIN, ['install', '--no-save', packageName], {
111
144
  stdio: 'inherit',
112
145
  timeout: 300_000,
146
+ cwd: installCwd,
113
147
  });
114
148
  return Promise.resolve(true);
115
149
  } catch (err) {
@@ -128,9 +162,10 @@ export function promptInstall(packageName: string): Promise<boolean> {
128
162
  rl.close();
129
163
  if (answer.trim().toLowerCase() !== 'y') return resolve(false);
130
164
  try {
131
- execFileSync(NPM_BIN, ['install', packageName], {
165
+ execFileSync(NPM_BIN, ['install', '--no-save', packageName], {
132
166
  stdio: 'inherit',
133
167
  timeout: 300_000,
168
+ cwd: installCwd,
134
169
  });
135
170
  resolve(true);
136
171
  } catch (err) {
@@ -306,11 +306,18 @@ const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
306
306
  },
307
307
  {
308
308
  id: 'fsharp',
309
- extensions: ['.fs', '.fsx', '.fsi'],
309
+ extensions: ['.fs', '.fsx'],
310
310
  grammarFile: 'tree-sitter-fsharp.wasm',
311
311
  extractor: extractFSharpSymbols,
312
312
  required: false,
313
313
  },
314
+ {
315
+ id: 'fsharp-signature',
316
+ extensions: ['.fsi'],
317
+ grammarFile: 'tree-sitter-fsharp_signature.wasm',
318
+ extractor: extractFSharpSymbols,
319
+ required: false,
320
+ },
314
321
  {
315
322
  id: 'gleam',
316
323
  extensions: ['.gleam'],
@@ -708,18 +715,18 @@ async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractor
708
715
  file?: string;
709
716
  parentNodeId?: number | null;
710
717
  }>;
711
- if (astRows.length > 0) {
712
- // Strip `file` and `parentNodeId` main thread re-resolves parent IDs
713
- // against its DB in features/ast.ts::collectFileAstRows, and `file` is
714
- // known from the map key.
715
- serializedAstNodes = astRows.map((n) => ({
716
- line: n.line,
717
- kind: n.kind,
718
- name: n.name ?? '',
719
- text: n.text ?? undefined,
720
- receiver: n.receiver ?? undefined,
721
- }));
722
- }
718
+ // Always set an array (even empty) — leaving astNodes undefined makes
719
+ // engine.ts::fileNeedsWasmTree treat the file as un-walked and trigger
720
+ // a full ensureWasmTrees re-parse of every WASM-parseable file (#1036).
721
+ // Strip `file` and `parentNodeId` — main thread re-resolves both in
722
+ // features/ast.ts::collectFileAstRows.
723
+ serializedAstNodes = astRows.map((n) => ({
724
+ line: n.line,
725
+ kind: n.kind,
726
+ name: n.name ?? '',
727
+ text: n.text ?? undefined,
728
+ receiver: n.receiver ?? undefined,
729
+ }));
723
730
  }
724
731
 
725
732
  if (complexityVisitor) storeComplexityResults(results, defs, entry.id);
@@ -159,6 +159,31 @@ function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void
159
159
 
160
160
  // ── Child extraction helpers ────────────────────────────────────────────────
161
161
 
162
+ const C_DECLARATOR_WRAPPERS = new Set([
163
+ 'pointer_declarator',
164
+ 'array_declarator',
165
+ 'parenthesized_declarator',
166
+ 'function_declarator',
167
+ ]);
168
+
169
+ /**
170
+ * Drill through pointer/array/parenthesized/function declarator wrappers to
171
+ * recover the bare identifier. Mirrors `unwrap_declarator` in the native C
172
+ * extractor so both engines agree on the name for parameters such as
173
+ * `void process(int callback(int))` (function-type parameter → `callback`) or
174
+ * `int *func(int)` (pointer-returning function → `func`).
175
+ */
176
+ function unwrapCDeclaratorName(node: TreeSitterNode): string {
177
+ let current: TreeSitterNode | null = node;
178
+ while (current && C_DECLARATOR_WRAPPERS.has(current.type)) {
179
+ current = current.childForFieldName('declarator');
180
+ }
181
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
182
+ return current.text;
183
+ }
184
+ return current?.text ?? node.text;
185
+ }
186
+
162
187
  function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
163
188
  const params: SubDeclaration[] = [];
164
189
  if (!paramListNode) return params;
@@ -167,10 +192,7 @@ function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaratio
167
192
  if (!param || param.type !== 'parameter_declaration') continue;
168
193
  const nameNode = param.childForFieldName('declarator');
169
194
  if (nameNode) {
170
- const name =
171
- nameNode.type === 'identifier'
172
- ? nameNode.text
173
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
195
+ const name = unwrapCDeclaratorName(nameNode);
174
196
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
175
197
  }
176
198
  }
@@ -186,10 +208,7 @@ function extractStructFields(structNode: TreeSitterNode): SubDeclaration[] {
186
208
  if (!member || member.type !== 'field_declaration') continue;
187
209
  const nameNode = member.childForFieldName('declarator');
188
210
  if (nameNode) {
189
- const name =
190
- nameNode.type === 'identifier'
191
- ? nameNode.text
192
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
211
+ const name = unwrapCDeclaratorName(nameNode);
193
212
  fields.push({ name, kind: 'property', line: member.startPosition.row + 1 });
194
213
  }
195
214
  }
@@ -239,6 +239,54 @@ function findCppParentClass(node: TreeSitterNode): string | null {
239
239
  return null;
240
240
  }
241
241
 
242
+ const CPP_DECLARATOR_WRAPPERS = new Set([
243
+ 'pointer_declarator',
244
+ 'reference_declarator',
245
+ 'array_declarator',
246
+ 'parenthesized_declarator',
247
+ 'function_declarator',
248
+ ]);
249
+
250
+ /**
251
+ * Drill through pointer/reference/array/parenthesized/function declarator
252
+ * wrappers to recover the bare identifier. Mirrors `unwrap_cpp_declarator` in
253
+ * the native C++ extractor. tree-sitter-cpp's `reference_declarator` does not
254
+ * expose a `declarator` field, so the loop falls back to scanning children
255
+ * for the next nested declarator or identifier.
256
+ */
257
+ function unwrapCppDeclaratorName(node: TreeSitterNode): string {
258
+ let current: TreeSitterNode | null = node;
259
+ while (current && CPP_DECLARATOR_WRAPPERS.has(current.type)) {
260
+ const named = current.childForFieldName('declarator');
261
+ if (named) {
262
+ current = named;
263
+ continue;
264
+ }
265
+ const fallback = nextCppDeclaratorChild(current);
266
+ if (!fallback) break;
267
+ current = fallback;
268
+ }
269
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
270
+ return current.text;
271
+ }
272
+ return current?.text ?? node.text;
273
+ }
274
+
275
+ function nextCppDeclaratorChild(node: TreeSitterNode): TreeSitterNode | null {
276
+ for (let i = 0; i < node.childCount; i++) {
277
+ const child = node.child(i);
278
+ if (!child) continue;
279
+ if (
280
+ child.type === 'identifier' ||
281
+ child.type === 'field_identifier' ||
282
+ CPP_DECLARATOR_WRAPPERS.has(child.type)
283
+ ) {
284
+ return child;
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
242
290
  function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
243
291
  const params: SubDeclaration[] = [];
244
292
  if (!paramListNode) return params;
@@ -247,10 +295,7 @@ function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclarat
247
295
  if (!param || param.type !== 'parameter_declaration') continue;
248
296
  const nameNode = param.childForFieldName('declarator');
249
297
  if (nameNode) {
250
- const name =
251
- nameNode.type === 'identifier'
252
- ? nameNode.text
253
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
298
+ const name = unwrapCppDeclaratorName(nameNode);
254
299
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
255
300
  }
256
301
  }
@@ -267,10 +312,7 @@ function extractCppClassFields(classNode: TreeSitterNode): SubDeclaration[] {
267
312
  if (!member || member.type !== 'field_declaration') continue;
268
313
  const nameNode = member.childForFieldName('declarator');
269
314
  if (nameNode) {
270
- const name =
271
- nameNode.type === 'identifier'
272
- ? nameNode.text
273
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
315
+ const name = unwrapCppDeclaratorName(nameNode);
274
316
  fields.push({
275
317
  name,
276
318
  kind: 'property',
@@ -265,10 +265,10 @@ function extractCudaParameters(paramListNode: TreeSitterNode | null): SubDeclara
265
265
  if (!param || param.type !== 'parameter_declaration') continue;
266
266
  const nameNode = param.childForFieldName('declarator');
267
267
  if (nameNode) {
268
- const name =
269
- nameNode.type === 'identifier'
270
- ? nameNode.text
271
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
268
+ // Reuse the field-name drill helper so function-type parameters like
269
+ // `void process(int callback(int))` yield the bare name `callback`
270
+ // instead of the raw declarator text, matching the native unwrap path.
271
+ const name = extractCudaFieldName(nameNode);
272
272
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
273
273
  }
274
274
  }
@@ -284,22 +284,96 @@ function extractCudaClassFields(classNode: TreeSitterNode): SubDeclaration[] {
284
284
  const member = body.child(i);
285
285
  if (!member || member.type !== 'field_declaration') continue;
286
286
  const nameNode = member.childForFieldName('declarator');
287
- if (nameNode) {
288
- const name =
289
- nameNode.type === 'identifier'
290
- ? nameNode.text
291
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
292
- fields.push({
293
- name,
294
- kind: 'property',
295
- line: member.startPosition.row + 1,
296
- visibility: extractModifierVisibility(member),
297
- });
298
- }
287
+ if (!nameNode) continue;
288
+ // Skip method declarations — a `field_declaration` whose declarator
289
+ // (after unwrapping pointer/reference/array) is a `function_declarator`
290
+ // is a method signature in a header, not a data field. Native and WASM
291
+ // previously diverged on how to format these (native stripped the `*`
292
+ // from pointer-return types, WASM kept it), and both produced
293
+ // method-signature-shaped "property" entries that are not real fields.
294
+ if (isCudaMethodDeclarator(nameNode)) continue;
295
+ const name = extractCudaFieldName(nameNode);
296
+ fields.push({
297
+ name,
298
+ kind: 'property',
299
+ line: member.startPosition.row + 1,
300
+ visibility: extractModifierVisibility(member),
301
+ });
299
302
  }
300
303
  return fields;
301
304
  }
302
305
 
306
+ const CUDA_DECLARATOR_WRAPPERS = new Set([
307
+ 'pointer_declarator',
308
+ 'reference_declarator',
309
+ 'array_declarator',
310
+ 'parenthesized_declarator',
311
+ ]);
312
+
313
+ function isCudaMethodDeclarator(node: TreeSitterNode): boolean {
314
+ let current: TreeSitterNode | null = node;
315
+ while (current && CUDA_DECLARATOR_WRAPPERS.has(current.type)) {
316
+ current = current.childForFieldName('declarator');
317
+ }
318
+ if (current?.type !== 'function_declarator') return false;
319
+ // A `function_declarator` whose inner declarator is a `parenthesized_declarator`
320
+ // is a function-pointer (or function-reference) field — e.g. `void (*cb)(int)`
321
+ // parses as function_declarator > parenthesized_declarator > pointer_declarator >
322
+ // field_identifier. Those are real data fields, not method declarations.
323
+ const inner = current.childForFieldName('declarator');
324
+ return inner?.type !== 'parenthesized_declarator';
325
+ }
326
+
327
+ /**
328
+ * Resolve the identifier of a declarator by walking through any combination of
329
+ * pointer/reference/array/parenthesized wrappers and `function_declarator`
330
+ * nodes. Used by both class-field extraction (where `function_declarator`
331
+ * indicates a function-pointer field after method declarations have been
332
+ * filtered out) and parameter extraction (where `function_declarator` wraps a
333
+ * bare function-type parameter name like `callback` in
334
+ * `void process(int callback(int))`).
335
+ */
336
+ function extractCudaFieldName(decl: TreeSitterNode): string {
337
+ let current: TreeSitterNode | null = decl;
338
+ while (current) {
339
+ if (current.type === 'identifier' || current.type === 'field_identifier') {
340
+ return current.text;
341
+ }
342
+ if (CUDA_DECLARATOR_WRAPPERS.has(current.type) || current.type === 'function_declarator') {
343
+ const next = innerCudaDeclarator(current);
344
+ if (!next) break;
345
+ current = next;
346
+ continue;
347
+ }
348
+ break;
349
+ }
350
+ return decl.text;
351
+ }
352
+
353
+ /**
354
+ * Find the inner declarator of a wrapper node. Most C++ declarator wrappers
355
+ * expose it via the `declarator` field, but some (e.g. `parenthesized_declarator`
356
+ * and `reference_declarator` in tree-sitter-cuda) have unnamed children — so
357
+ * fall back to scanning children for a declarator-shaped node.
358
+ */
359
+ function innerCudaDeclarator(node: TreeSitterNode): TreeSitterNode | null {
360
+ const named = node.childForFieldName('declarator');
361
+ if (named) return named;
362
+ for (let i = 0; i < node.childCount; i++) {
363
+ const child = node.child(i);
364
+ if (!child) continue;
365
+ if (
366
+ child.type === 'identifier' ||
367
+ child.type === 'field_identifier' ||
368
+ child.type === 'function_declarator' ||
369
+ CUDA_DECLARATOR_WRAPPERS.has(child.type)
370
+ ) {
371
+ return child;
372
+ }
373
+ }
374
+ return null;
375
+ }
376
+
303
377
  function extractCudaEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] {
304
378
  const entries: SubDeclaration[] = [];
305
379
  const body = findChild(enumNode, 'enumerator_list');
@@ -190,14 +190,86 @@ function extractElixirParams(defCallNode: TreeSitterNode): SubDeclaration[] {
190
190
  for (let j = 0; j < innerArgs.childCount; j++) {
191
191
  const param = innerArgs.child(j);
192
192
  if (!param) continue;
193
- if (param.type === 'identifier') {
194
- params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
195
- }
193
+ collectElixirParamIdentifiers(param, params);
196
194
  }
197
195
  }
198
196
  return params;
199
197
  }
200
198
 
199
+ /**
200
+ * Recursively walk a parameter pattern and emit each bound identifier as a
201
+ * `parameter` child. Handles bare identifiers, default-value `a \\ default`,
202
+ * list-cons `[head | tail]`, list `[a, b, c]`, tuple `{x, y}`, and
203
+ * map / struct destructuring (`%{k: v}`, `%Foo{k: v}`).
204
+ */
205
+ function collectElixirParamIdentifiers(node: TreeSitterNode, out: SubDeclaration[]): void {
206
+ switch (node.type) {
207
+ case 'identifier':
208
+ out.push({ name: node.text, kind: 'parameter', line: node.startPosition.row + 1 });
209
+ return;
210
+ case 'binary_operator': {
211
+ // `name \\ default` (default-value) binds the left operand only.
212
+ // `head | tail` (list-cons, appears inside a `list` pattern) binds both operands.
213
+ const op = node.child(1);
214
+ if (!op) return;
215
+ if (op.type === '\\\\') {
216
+ const left = node.child(0);
217
+ if (left) collectElixirParamIdentifiers(left, out);
218
+ return;
219
+ }
220
+ if (op.type === '|') {
221
+ const left = node.child(0);
222
+ const right = node.child(2);
223
+ if (left) collectElixirParamIdentifiers(left, out);
224
+ if (right) collectElixirParamIdentifiers(right, out);
225
+ return;
226
+ }
227
+ return;
228
+ }
229
+ case 'list':
230
+ // `[a, b, c]` or `[head | tail]` — walk children, skipping punctuation. The
231
+ // `|` cons case is handled by the `binary_operator` arm when we recurse.
232
+ for (let i = 0; i < node.childCount; i++) {
233
+ const c = node.child(i);
234
+ if (!c || c.type === '[' || c.type === ']' || c.type === ',') continue;
235
+ collectElixirParamIdentifiers(c, out);
236
+ }
237
+ return;
238
+ case 'tuple':
239
+ for (let i = 0; i < node.childCount; i++) {
240
+ const c = node.child(i);
241
+ if (!c || c.type === '{' || c.type === '}' || c.type === ',') continue;
242
+ collectElixirParamIdentifiers(c, out);
243
+ }
244
+ return;
245
+ case 'map':
246
+ // `%{k: v}` or `%Foo{k: v}` — walk map_content > keywords > pair and emit each
247
+ // pair's value side (the bound name). The struct alias (`Foo`) is a type, not a
248
+ // bound identifier, so the leading `struct` child is intentionally skipped.
249
+ for (let i = 0; i < node.childCount; i++) {
250
+ const c = node.child(i);
251
+ if (c && c.type === 'map_content') collectElixirMapBindings(c, out);
252
+ }
253
+ return;
254
+ }
255
+ }
256
+
257
+ function collectElixirMapBindings(content: TreeSitterNode, out: SubDeclaration[]): void {
258
+ for (let i = 0; i < content.childCount; i++) {
259
+ const kws = content.child(i);
260
+ if (!kws || kws.type !== 'keywords') continue;
261
+ for (let j = 0; j < kws.childCount; j++) {
262
+ const pair = kws.child(j);
263
+ if (!pair || pair.type !== 'pair') continue;
264
+ for (let k = 0; k < pair.childCount; k++) {
265
+ const part = pair.child(k);
266
+ if (!part || part.type === 'keyword') continue;
267
+ collectElixirParamIdentifiers(part, out);
268
+ }
269
+ }
270
+ }
271
+ }
272
+
201
273
  function handleDefprotocol(node: TreeSitterNode, ctx: ExtractorOutput): void {
202
274
  const args = findChild(node, 'arguments');
203
275
  if (!args) return;