@optave/codegraph 3.12.0 → 3.13.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 (144) hide show
  1. package/README.md +71 -35
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +2 -1
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/batch.d.ts.map +1 -1
  6. package/dist/cli/commands/batch.js +1 -0
  7. package/dist/cli/commands/batch.js.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/build.js +6 -1
  10. package/dist/cli/commands/build.js.map +1 -1
  11. package/dist/cli/commands/config.d.ts +3 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +272 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/triage.js +1 -1
  16. package/dist/cli/commands/triage.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +10 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/shared/options.d.ts +2 -1
  21. package/dist/cli/shared/options.d.ts.map +1 -1
  22. package/dist/cli/shared/options.js +11 -1
  23. package/dist/cli/shared/options.js.map +1 -1
  24. package/dist/cli/types.d.ts +2 -0
  25. package/dist/cli/types.d.ts.map +1 -1
  26. package/dist/db/migrations.js +1 -1
  27. package/dist/db/migrations.js.map +1 -1
  28. package/dist/domain/graph/builder/call-resolver.d.ts +12 -8
  29. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/call-resolver.js +93 -38
  31. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  32. package/dist/domain/graph/builder/cha.d.ts +9 -1
  33. package/dist/domain/graph/builder/cha.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/cha.js +17 -2
  35. package/dist/domain/graph/builder/cha.js.map +1 -1
  36. package/dist/domain/graph/builder/helpers.d.ts +8 -0
  37. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  38. package/dist/domain/graph/builder/helpers.js +22 -3
  39. package/dist/domain/graph/builder/helpers.js.map +1 -1
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +1 -1
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/pipeline.js +37 -2
  45. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +88 -318
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +1 -1
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/native-orchestrator.js +341 -82
  57. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
  59. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  60. package/dist/domain/parser.d.ts +4 -5
  61. package/dist/domain/parser.d.ts.map +1 -1
  62. package/dist/domain/parser.js +46 -15
  63. package/dist/domain/parser.js.map +1 -1
  64. package/dist/domain/wasm-worker-entry.js +10 -2
  65. package/dist/domain/wasm-worker-entry.js.map +1 -1
  66. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  67. package/dist/domain/wasm-worker-pool.js +2 -0
  68. package/dist/domain/wasm-worker-pool.js.map +1 -1
  69. package/dist/domain/wasm-worker-protocol.d.ts +1 -0
  70. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  71. package/dist/extractors/cpp.d.ts.map +1 -1
  72. package/dist/extractors/cpp.js +42 -1
  73. package/dist/extractors/cpp.js.map +1 -1
  74. package/dist/extractors/cuda.d.ts.map +1 -1
  75. package/dist/extractors/cuda.js +42 -1
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/helpers.d.ts +11 -0
  78. package/dist/extractors/helpers.d.ts.map +1 -1
  79. package/dist/extractors/helpers.js +40 -0
  80. package/dist/extractors/helpers.js.map +1 -1
  81. package/dist/extractors/java.d.ts.map +1 -1
  82. package/dist/extractors/java.js +8 -7
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.js +137 -6
  85. package/dist/extractors/javascript.js.map +1 -1
  86. package/dist/features/structure-query.d.ts +1 -1
  87. package/dist/features/structure-query.d.ts.map +1 -1
  88. package/dist/features/structure-query.js +6 -6
  89. package/dist/features/structure-query.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/infrastructure/config.d.ts +77 -4
  95. package/dist/infrastructure/config.d.ts.map +1 -1
  96. package/dist/infrastructure/config.js +395 -21
  97. package/dist/infrastructure/config.js.map +1 -1
  98. package/dist/infrastructure/registry.d.ts +27 -0
  99. package/dist/infrastructure/registry.d.ts.map +1 -1
  100. package/dist/infrastructure/registry.js +59 -1
  101. package/dist/infrastructure/registry.js.map +1 -1
  102. package/dist/presentation/structure.d.ts +1 -1
  103. package/dist/presentation/structure.d.ts.map +1 -1
  104. package/dist/presentation/structure.js +2 -2
  105. package/dist/presentation/structure.js.map +1 -1
  106. package/dist/types.d.ts +37 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/grammars/tree-sitter-gleam.wasm +0 -0
  109. package/package.json +7 -8
  110. package/src/cli/commands/audit.ts +2 -1
  111. package/src/cli/commands/batch.ts +1 -0
  112. package/src/cli/commands/build.ts +6 -1
  113. package/src/cli/commands/config.ts +353 -0
  114. package/src/cli/commands/triage.ts +1 -1
  115. package/src/cli/index.ts +10 -0
  116. package/src/cli/shared/options.ts +11 -1
  117. package/src/cli/types.ts +2 -0
  118. package/src/db/migrations.ts +1 -1
  119. package/src/domain/graph/builder/call-resolver.ts +99 -41
  120. package/src/domain/graph/builder/cha.ts +18 -1
  121. package/src/domain/graph/builder/helpers.ts +24 -4
  122. package/src/domain/graph/builder/incremental.ts +1 -0
  123. package/src/domain/graph/builder/pipeline.ts +49 -2
  124. package/src/domain/graph/builder/stages/build-edges.ts +130 -399
  125. package/src/domain/graph/builder/stages/detect-changes.ts +1 -1
  126. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  127. package/src/domain/graph/builder/stages/native-orchestrator.ts +396 -92
  128. package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
  129. package/src/domain/parser.ts +45 -14
  130. package/src/domain/wasm-worker-entry.ts +10 -2
  131. package/src/domain/wasm-worker-pool.ts +1 -0
  132. package/src/domain/wasm-worker-protocol.ts +1 -0
  133. package/src/extractors/cpp.ts +44 -1
  134. package/src/extractors/cuda.ts +44 -1
  135. package/src/extractors/helpers.ts +43 -0
  136. package/src/extractors/java.ts +8 -7
  137. package/src/extractors/javascript.ts +127 -6
  138. package/src/features/structure-query.ts +7 -7
  139. package/src/index.ts +5 -1
  140. package/src/infrastructure/config.ts +481 -22
  141. package/src/infrastructure/registry.ts +82 -1
  142. package/src/presentation/structure.ts +3 -3
  143. package/src/types.ts +41 -0
  144. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -135,7 +135,7 @@ async function reparseBarrelFiles(
135
135
  // which only runs on the original (changed + reverse-dep) fileSymbols. Barrel
136
136
  // candidates are merged here *after* insertNodes, so wiping those kinds
137
137
  // would permanently drop them (mirrors the Rust orchestrator's Stage 6b
138
- // delete in build_pipeline.rs).
138
+ // delete in domain/graph/builder/pipeline.rs).
139
139
  const deleteOutgoingEdges = db.prepare(
140
140
  `DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)
141
141
  AND kind NOT IN ('contains', 'parameter_of')`,
@@ -158,6 +158,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
158
158
  '(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
159
159
  '(method_definition name: (property_identifier) @meth_name) @meth_node',
160
160
  '(method_definition name: (private_property_identifier) @meth_name) @meth_node',
161
+ '(method_definition name: (computed_property_name) @meth_name) @meth_node',
161
162
  '(import_statement source: (string) @imp_source) @imp_node',
162
163
  '(export_statement) @exp_node',
163
164
  '(call_expression function: (identifier) @callfn_name) @callfn_node',
@@ -221,7 +222,9 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
221
222
  file: entry.grammarFile,
222
223
  cause: e as Error,
223
224
  });
224
- warn(
225
+ const isEnoent = (e as NodeJS.ErrnoException).code === 'ENOENT';
226
+ const log = isEnoent ? debug : warn;
227
+ log(
225
228
  `${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
226
229
  );
227
230
  _cachedParsers!.set(entry.id, null);
@@ -465,7 +468,7 @@ export function getInstalledWasmExtensions(): Set<string> {
465
468
  * Lowercase file extensions covered by the native Rust addon.
466
469
  *
467
470
  * Mirrors `LanguageKind::from_extension` in
468
- * `crates/codegraph-core/src/parser_registry.rs`. Used to classify why the
471
+ * `crates/codegraph-core/src/domain/parser.rs`. Used to classify why the
469
472
  * native orchestrator dropped a file: extensions outside this set are a
470
473
  * legitimate parser limit (no Rust extractor exists), while extensions inside
471
474
  * it indicate a real native bug (parse/read/extract failure).
@@ -1181,12 +1184,25 @@ async function parseFilesWasm(
1181
1184
  /**
1182
1185
  * Files at or below this count use the inline parse path (no worker spawn).
1183
1186
  *
1184
- * Sized for typical engine-parity drops: a handful of fixture files in one
1185
- * or two languages (the recurring HCL case is 4 files). Above this, the
1186
- * worker-pool's IPC + crash-isolation cost (#965) is amortized over enough
1187
- * parse work to be worth paying; below it, the ~1–2s cold-start dominates.
1187
+ * The worker pool exists for crash safety (#965): exotic (non-required) WASM
1188
+ * grammars can trigger uncatchable V8 fatal errors that would kill the main
1189
+ * process. Running them in a worker means only the worker dies; the pool
1190
+ * detects the exit, skips the file, respawns, and continues.
1191
+ *
1192
+ * JS/TS/TSX are required-tier grammars — they have never triggered the V8
1193
+ * fatal crash class and are safe to run inline. The primary hot caller
1194
+ * (this/super dispatch post-pass) exclusively handles JS/TS/TSX files and
1195
+ * measured ~55–64ms/file through the pool vs ~8–10ms/file inline (#1435);
1196
+ * IPC overhead scales linearly with file count, not amortised.
1197
+ *
1198
+ * The threshold is set high enough to keep typical this-dispatch batches
1199
+ * (≤ 18 files on the codegraph corpus) on the inline path, while still
1200
+ * routing truly large exotic-language drops (rare; typical HCL case is 4
1201
+ * files) through the pool for crash isolation. Exotic-language drops are
1202
+ * almost always well under this limit anyway, so they benefit from the
1203
+ * inline fast path too without meaningful crash risk increase.
1188
1204
  */
1189
- const INLINE_BACKFILL_THRESHOLD = 16;
1205
+ const INLINE_BACKFILL_THRESHOLD = 32;
1190
1206
 
1191
1207
  /**
1192
1208
  * Inline WASM parse (no worker) for small file batches.
@@ -1198,11 +1214,16 @@ const INLINE_BACKFILL_THRESHOLD = 16;
1198
1214
  *
1199
1215
  * Returns symbols with `_tree` set so `runAnalyses` can run AST/CFG/dataflow
1200
1216
  * visitors via the unified walker (mirrors how WASM-engine results behaved
1201
- * before the worker pool was introduced).
1217
+ * before the worker pool was introduced), unless `symbolsOnly` is true — in
1218
+ * that case `_tree` is not set, skipping all analysis visitor walks. Use
1219
+ * `symbolsOnly` when only definitions/calls/typeMap are needed (e.g. the
1220
+ * this/super dispatch post-pass) to avoid the analysis overhead on the inline
1221
+ * path, matching the optimization already applied to the worker-pool path.
1202
1222
  */
1203
1223
  async function parseFilesWasmInline(
1204
1224
  filePaths: string[],
1205
1225
  rootDir: string,
1226
+ symbolsOnly = false,
1206
1227
  ): Promise<Map<string, ExtractorOutput>> {
1207
1228
  const result = new Map<string, ExtractorOutput>();
1208
1229
  if (filePaths.length === 0) return result;
@@ -1220,7 +1241,18 @@ async function parseFilesWasmInline(
1220
1241
  if (!extracted) continue;
1221
1242
  const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
1222
1243
  const symbols = extracted.symbols as ExtractorOutput & { _tree?: unknown; _langId?: string };
1223
- symbols._tree = extracted.tree;
1244
+ // When symbolsOnly=true, skip setting _tree so runAnalyses does not run
1245
+ // AST/complexity/CFG/dataflow visitor walks — only definitions/calls/typeMap
1246
+ // are needed by callers like the this/super dispatch post-pass.
1247
+ if (!symbolsOnly) {
1248
+ symbols._tree = extracted.tree;
1249
+ } else if (typeof (extracted.tree as any)?.delete === 'function') {
1250
+ // Free the WASM-backed tree immediately — web-tree-sitter trees are backed
1251
+ // by WASM linear memory and require explicit disposal. When symbolsOnly is
1252
+ // true the tree is never stored anywhere, so we must delete it here to
1253
+ // avoid leaking WASM heap on every incremental rebuild.
1254
+ (extracted.tree as any).delete();
1255
+ }
1224
1256
  symbols._langId = extracted.langId;
1225
1257
  result.set(relPath, symbols);
1226
1258
  }
@@ -1230,14 +1262,13 @@ async function parseFilesWasmInline(
1230
1262
  /**
1231
1263
  * Backfill helper: small batches use the inline (main-thread) path; larger
1232
1264
  * batches keep the worker-pool isolation against tree-sitter WASM crashes
1233
- * (#965). Threshold matches typical engine-parity drop sizes (a few fixture
1234
- * files in one or two languages).
1265
+ * (#965). See INLINE_BACKFILL_THRESHOLD for threshold rationale.
1235
1266
  *
1236
1267
  * `opts.symbolsOnly` skips the AST/complexity/CFG/dataflow visitors in the
1237
1268
  * worker (and their result serialization across the thread boundary) for
1238
1269
  * callers that only consume definitions/calls/typeMap — the native
1239
- * orchestrator's prototype-methods and this-dispatch post-passes. Callers
1240
- * that ingest the files into the DB (dropped-language backfill) must keep
1270
+ * orchestrator's this-dispatch post-pass. Callers that ingest the files into
1271
+ * the DB (dropped-language backfill) must keep
1241
1272
  * the default full analysis.
1242
1273
  */
1243
1274
  export async function parseFilesWasmForBackfill(
@@ -1246,7 +1277,7 @@ export async function parseFilesWasmForBackfill(
1246
1277
  opts: { symbolsOnly?: boolean } = {},
1247
1278
  ): Promise<Map<string, ExtractorOutput>> {
1248
1279
  if (filePaths.length <= INLINE_BACKFILL_THRESHOLD) {
1249
- return parseFilesWasmInline(filePaths, rootDir);
1280
+ return parseFilesWasmInline(filePaths, rootDir, opts.symbolsOnly);
1250
1281
  }
1251
1282
  return parseFilesWasm(filePaths, rootDir, opts.symbolsOnly ? EXTRACT_ONLY : FULL_ANALYSIS);
1252
1283
  }
@@ -115,6 +115,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
115
115
  '(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
116
116
  '(method_definition name: (property_identifier) @meth_name) @meth_node',
117
117
  '(method_definition name: (private_property_identifier) @meth_name) @meth_node',
118
+ '(method_definition name: (computed_property_name) @meth_name) @meth_node',
118
119
  '(import_statement source: (string) @imp_source) @imp_node',
119
120
  '(export_statement) @exp_node',
120
121
  '(call_expression function: (identifier) @callfn_name) @callfn_node',
@@ -125,11 +126,17 @@ const COMMON_QUERY_PATTERNS: string[] = [
125
126
  '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
126
127
  ];
127
128
 
128
- const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';
129
+ const JS_CLASS_PATTERNS: string[] = [
130
+ '(class_declaration name: (identifier) @cls_name) @cls_node',
131
+ // class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
132
+ '(class name: (identifier) @cls_name) @cls_node',
133
+ ];
129
134
 
130
135
  const TS_EXTRA_PATTERNS: string[] = [
131
136
  '(class_declaration name: (type_identifier) @cls_name) @cls_node',
132
137
  '(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node',
138
+ // class expressions: `return class Foo extends Bar { ... }`
139
+ '(class name: (type_identifier) @cls_name) @cls_node',
133
140
  '(interface_declaration name: (type_identifier) @iface_name) @iface_node',
134
141
  '(type_alias_declaration name: (type_identifier) @type_name) @type_node',
135
142
  ];
@@ -433,7 +440,7 @@ async function loadLanguageLazy(entry: LanguageRegistryEntry): Promise<Parser |
433
440
  const isTS = entry.id === 'typescript' || entry.id === 'tsx';
434
441
  const patterns = isTS
435
442
  ? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
436
- : [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
443
+ : [...COMMON_QUERY_PATTERNS, ...JS_CLASS_PATTERNS];
437
444
  _queries.set(entry.id, new Query(lang, patterns.join('\n')));
438
445
  }
439
446
  return parser;
@@ -818,6 +825,7 @@ function serializeExtractorOutput(
818
825
  ...(symbols.objectPropBindings?.length
819
826
  ? { objectPropBindings: symbols.objectPropBindings }
820
827
  : {}),
828
+ ...(symbols.thisCallBindings?.length ? { thisCallBindings: symbols.thisCallBindings } : {}),
821
829
  ...(symbols.newExpressions?.length ? { newExpressions: symbols.newExpressions } : {}),
822
830
  ...(symbols.definePropertyReceivers?.size
823
831
  ? { definePropertyReceivers: Array.from(symbols.definePropertyReceivers.entries()) }
@@ -115,6 +115,7 @@ function deserializeResult(ser: SerializedExtractorOutput | null): ExtractorOutp
115
115
  if (ser.objectRestParamBindings?.length)
116
116
  out.objectRestParamBindings = ser.objectRestParamBindings;
117
117
  if (ser.objectPropBindings?.length) out.objectPropBindings = ser.objectPropBindings;
118
+ if (ser.thisCallBindings?.length) out.thisCallBindings = ser.thisCallBindings;
118
119
  if (ser.newExpressions?.length) out.newExpressions = ser.newExpressions;
119
120
  if (ser.definePropertyReceivers?.length) {
120
121
  const m = new Map<string, string>();
@@ -71,6 +71,7 @@ export interface SerializedExtractorOutput {
71
71
  objectRestParamBindings?: import('../types.js').ObjectRestParamBinding[];
72
72
  objectPropBindings?: import('../types.js').ObjectPropBinding[];
73
73
  paramBindings?: import('../types.js').ParamBinding[];
74
+ thisCallBindings?: import('../types.js').ThisCallBinding[];
74
75
  newExpressions?: readonly string[];
75
76
  /** Serialized definePropertyReceivers map (funcName → receiverVarName) as tuple array. */
76
77
  definePropertyReceivers?: Array<[string, string]>;
@@ -5,7 +5,13 @@ import type {
5
5
  TreeSitterNode,
6
6
  TreeSitterTree,
7
7
  } from '../types.js';
8
- import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
8
+ import {
9
+ extractModifierVisibility,
10
+ findChild,
11
+ isCPrimitiveType,
12
+ nodeEndLine,
13
+ setTypeMapEntry,
14
+ } from './helpers.js';
9
15
 
10
16
  /**
11
17
  * Extract symbols from C++ files.
@@ -50,6 +56,9 @@ function walkCppNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
50
56
  case 'call_expression':
51
57
  handleCppCallExpression(node, ctx);
52
58
  break;
59
+ case 'declaration':
60
+ handleCppDeclaration(node, ctx);
61
+ break;
53
62
  }
54
63
 
55
64
  for (let i = 0; i < node.childCount; i++) {
@@ -204,6 +213,40 @@ function handleCppInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
204
213
  });
205
214
  }
206
215
 
216
+ /**
217
+ * Seed typeMap for declaration-typed locals: `UserService svc;` and
218
+ * `UserService svc = makeService();` both yield typeMap["svc"] = "UserService"
219
+ * at confidence 0.9. Mirrors `match_c_family_type_map` ("declaration" branch)
220
+ * in the native Rust C++ extractor.
221
+ */
222
+ function handleCppDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void {
223
+ const typeNode = node.childForFieldName('type');
224
+ if (!typeNode) return;
225
+ const typeName = typeNode.text;
226
+ // Skip primitive types — they are never class/struct receivers
227
+ if (isCPrimitiveType(typeName)) return;
228
+ for (let i = 0; i < node.childCount; i++) {
229
+ const child = node.child(i);
230
+ if (!child) continue;
231
+ const kind = child.type;
232
+ let nameNode: TreeSitterNode | null = null;
233
+ if (kind === 'init_declarator') {
234
+ nameNode = child.childForFieldName('declarator') ?? null;
235
+ } else if (kind === 'identifier') {
236
+ nameNode = child;
237
+ }
238
+ // Note: pointer_declarator / reference_declarator children (e.g. `UserService *svc;`)
239
+ // are intentionally skipped here — they are also skipped by the native Rust
240
+ // match_c_family_type_map helper, which only handles 'init_declarator' and
241
+ // 'identifier' children. Both engines have the same scope for this case.
242
+ if (!nameNode) continue;
243
+ const varName = unwrapCppDeclaratorName(nameNode);
244
+ if (varName) {
245
+ setTypeMapEntry(ctx.typeMap, varName, typeName, 0.9);
246
+ }
247
+ }
248
+ }
249
+
207
250
  function handleCppCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
208
251
  const funcNode = node.childForFieldName('function');
209
252
  if (!funcNode) return;
@@ -5,7 +5,13 @@ import type {
5
5
  TreeSitterNode,
6
6
  TreeSitterTree,
7
7
  } from '../types.js';
8
- import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
8
+ import {
9
+ extractModifierVisibility,
10
+ findChild,
11
+ isCPrimitiveType,
12
+ nodeEndLine,
13
+ setTypeMapEntry,
14
+ } from './helpers.js';
9
15
 
10
16
  /**
11
17
  * Extract symbols from CUDA files.
@@ -63,6 +69,9 @@ function walkCudaNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
63
69
  case 'call_expression':
64
70
  handleCudaCallExpression(node, ctx);
65
71
  break;
72
+ case 'declaration':
73
+ handleCudaDeclaration(node, ctx);
74
+ break;
66
75
  }
67
76
 
68
77
  for (let i = 0; i < node.childCount; i++) {
@@ -204,6 +213,40 @@ function handleCudaInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
204
213
  });
205
214
  }
206
215
 
216
+ /**
217
+ * Seed typeMap for declaration-typed locals: `UserService svc;` and
218
+ * `UserService svc = make();` both yield typeMap["svc"] = "UserService"
219
+ * at confidence 0.9. Mirrors `match_c_family_type_map` ("declaration" branch)
220
+ * in the native Rust CUDA extractor.
221
+ */
222
+ function handleCudaDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void {
223
+ const typeNode = node.childForFieldName('type');
224
+ if (!typeNode) return;
225
+ const typeName = typeNode.text;
226
+ // Skip primitive types — they are never class/struct receivers
227
+ if (isCPrimitiveType(typeName)) return;
228
+ for (let i = 0; i < node.childCount; i++) {
229
+ const child = node.child(i);
230
+ if (!child) continue;
231
+ const kind = child.type;
232
+ let nameNode: TreeSitterNode | null = null;
233
+ if (kind === 'init_declarator') {
234
+ nameNode = child.childForFieldName('declarator') ?? null;
235
+ } else if (kind === 'identifier') {
236
+ nameNode = child;
237
+ }
238
+ // Note: pointer_declarator / reference_declarator children (e.g. `UserService *svc;`)
239
+ // are intentionally skipped here — they are also skipped by the native Rust
240
+ // match_c_family_type_map helper, which only handles 'init_declarator' and
241
+ // 'identifier' children. Both engines have the same scope for this case.
242
+ if (!nameNode) continue;
243
+ const varName = extractCudaFieldName(nameNode);
244
+ if (varName) {
245
+ setTypeMapEntry(ctx.typeMap, varName, typeName, 0.9);
246
+ }
247
+ }
248
+ }
249
+
207
250
  function handleCudaCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
208
251
  const funcNode = node.childForFieldName('function');
209
252
  if (!funcNode) return;
@@ -305,6 +305,49 @@ export function pushImport(
305
305
  ctx.imports.push(entry);
306
306
  }
307
307
 
308
+ // ── C-family primitive types ───────────────────────────────────────────────
309
+
310
+ /**
311
+ * Primitive C/C++/CUDA types that are never class/struct receivers. Seeding
312
+ * these into typeMap would produce spurious receiver edges (e.g. `int x` → `int`).
313
+ * Shared between the C++ and CUDA extractors to prevent divergence.
314
+ */
315
+ export const C_PRIMITIVE_TYPES: ReadonlySet<string> = new Set([
316
+ 'int',
317
+ 'long',
318
+ 'short',
319
+ 'unsigned',
320
+ 'signed',
321
+ 'float',
322
+ 'double',
323
+ 'char',
324
+ 'bool',
325
+ 'void',
326
+ 'wchar_t',
327
+ 'auto',
328
+ 'size_t',
329
+ 'uint8_t',
330
+ 'uint16_t',
331
+ 'uint32_t',
332
+ 'uint64_t',
333
+ 'int8_t',
334
+ 'int16_t',
335
+ 'int32_t',
336
+ 'int64_t',
337
+ 'ptrdiff_t',
338
+ 'intptr_t',
339
+ 'uintptr_t',
340
+ ]);
341
+
342
+ /**
343
+ * Return true when `typeName` is a primitive C/C++/CUDA type.
344
+ * Strips leading qualifiers (`const int` → `int`) before checking.
345
+ */
346
+ export function isCPrimitiveType(typeName: string): boolean {
347
+ const base = typeName.split(/\s+/).pop() ?? typeName;
348
+ return C_PRIMITIVE_TYPES.has(base) || C_PRIMITIVE_TYPES.has(typeName);
349
+ }
350
+
308
351
  // ── Parameter extraction ───────────────────────────────────────────────────
309
352
 
310
353
  /**
@@ -16,6 +16,7 @@ import {
16
16
  nodeStartLine,
17
17
  pushCall,
18
18
  pushImport,
19
+ setTypeMapEntry,
19
20
  } from './helpers.js';
20
21
 
21
22
  /**
@@ -273,13 +274,13 @@ function handleJavaLocalVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): voi
273
274
  const child = node.child(i);
274
275
  if (child?.type === 'variable_declarator') {
275
276
  const nameNode = child.childForFieldName('name');
276
- // Use direct Map.set (last-wins) for local variable declarations.
277
- // Local variable types are method-scoped and should override any
278
- // prior entry (e.g. a same-named constructor parameter). Using
279
- // setTypeMapEntry (first-wins on tie) would let a constructor
280
- // parameter type block a local variable's more-specific concrete type.
281
- if (nameNode && ctx.typeMap)
282
- ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
277
+ // Use setTypeMapEntry (first-wins on tie) to match Rust extractor semantics.
278
+ // The typeMap is flat per-file without method scoping, so a local variable
279
+ // in one method (e.g. `InMemoryUserRepository repo` in `createDefault()`) must
280
+ // not override a parameter binding set by an earlier method
281
+ // (e.g. `UserRepository repo` constructor param). First-wins preserves the
282
+ // interface/abstract type annotation that drives correct CHA dispatch.
283
+ if (nameNode && ctx.typeMap) setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9);
283
284
  }
284
285
  }
285
286
  }
@@ -169,7 +169,19 @@ function handleClassCapture(
169
169
 
170
170
  /** Handle method_definition capture. */
171
171
  function handleMethodCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
172
- const methName = c.meth_name!.text;
172
+ const methNameNode = c.meth_name!;
173
+ let methName: string;
174
+ if (methNameNode.type === 'computed_property_name') {
175
+ // Extract the inner string literal from `['methodName']` or `["methodName"]`.
176
+ // Non-string computed keys (e.g. `[Symbol.iterator]`) cannot be resolved at
177
+ // dot-notation call sites, so skip them entirely.
178
+ const inner = methNameNode.child(1); // child(0)='[', child(1)=string, child(2)=']'
179
+ if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) return;
180
+ methName = inner.text.replace(/^['"]|['"]$/g, '');
181
+ if (!methName) return;
182
+ } else {
183
+ methName = methNameNode.text;
184
+ }
173
185
  const parentClass = findParentClass(c.meth_node!);
174
186
  const fullName = parentClass ? `${parentClass}.${methName}` : methName;
175
187
  const methChildren = extractParameters(c.meth_node!);
@@ -507,6 +519,15 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
507
519
  nodeEndLine(declNode),
508
520
  definitions,
509
521
  );
522
+ } else if (nameN && nameN.type === 'array_pattern') {
523
+ // `const [x, y] = ...` — emit a single constant node whose name is the
524
+ // full array pattern text (e.g. `[x, y]`), matching native engine behaviour.
525
+ definitions.push({
526
+ name: nameN.text,
527
+ kind: 'constant',
528
+ line: nodeStartLine(declNode),
529
+ endLine: nodeEndLine(declNode),
530
+ });
510
531
  }
511
532
  }
512
533
  }
@@ -816,8 +837,20 @@ function handleClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
816
837
  function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
817
838
  const nameNode = node.childForFieldName('name');
818
839
  if (nameNode) {
840
+ let methName: string;
841
+ if (nameNode.type === 'computed_property_name') {
842
+ // Extract the inner string literal from `['methodName']` or `["methodName"]`.
843
+ // Non-string computed keys (e.g. `[Symbol.iterator]`) cannot be resolved at
844
+ // dot-notation call sites, so skip them entirely.
845
+ const inner = nameNode.child(1); // child(0)='[', child(1)=string, child(2)=']'
846
+ if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) return;
847
+ methName = inner.text.replace(/^['"]|['"]$/g, '');
848
+ if (!methName) return;
849
+ } else {
850
+ methName = nameNode.text;
851
+ }
819
852
  const parentClass = findParentClass(node);
820
- const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
853
+ const fullName = parentClass ? `${parentClass}.${methName}` : methName;
821
854
  const methChildren = extractParameters(node);
822
855
  const methVis = extractVisibility(node);
823
856
  ctx.definitions.push({
@@ -1017,6 +1050,16 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
1017
1050
  nodeEndLine(node),
1018
1051
  ctx.definitions,
1019
1052
  );
1053
+ } else if (isConst && nameN.type === 'array_pattern' && !hasFunctionScopeAncestor(node)) {
1054
+ // Array destructuring: `const [x, y] = ...` — emit a single constant node
1055
+ // whose name is the full array pattern text (e.g. `[x, y]`), matching
1056
+ // native engine behaviour. Scope guard mirrors the object_pattern branch above.
1057
+ ctx.definitions.push({
1058
+ name: nameN.text,
1059
+ kind: 'constant',
1060
+ line: nodeStartLine(node),
1061
+ endLine: nodeEndLine(node),
1062
+ });
1020
1063
  }
1021
1064
  }
1022
1065
  }
@@ -1065,8 +1108,19 @@ function extractObjectLiteralFunctions(
1065
1108
  } else if (child.type === 'method_definition') {
1066
1109
  const nameNode = child.childForFieldName('name');
1067
1110
  if (nameNode) {
1111
+ let methodName: string;
1112
+ if (nameNode.type === 'computed_property_name') {
1113
+ // Strip brackets+quotes from `['methodName']` to get a resolvable name.
1114
+ // Skip non-string computed keys (e.g. [Symbol.iterator]).
1115
+ const inner = nameNode.child(1);
1116
+ if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) continue;
1117
+ methodName = inner.text.replace(/^['"]|['"]$/g, '');
1118
+ if (!methodName) continue;
1119
+ } else {
1120
+ methodName = nameNode.text;
1121
+ }
1068
1122
  definitions.push({
1069
- name: `${varName}.${nameNode.text}`,
1123
+ name: `${varName}.${methodName}`,
1070
1124
  kind: 'function',
1071
1125
  line: nodeStartLine(child),
1072
1126
  endLine: nodeEndLine(child),
@@ -1813,9 +1867,23 @@ function runContextCollectorWalk(rootNode: TreeSitterNode, out: ContextCollector
1813
1867
  // Qualify with the enclosing class name so the PTS key matches
1814
1868
  // callerName from findCaller (which uses def.name = 'ClassName.method').
1815
1869
  const enclosingClass = classStack.length > 0 ? classStack[classStack.length - 1] : null;
1816
- const qualifiedName = enclosingClass ? `${enclosingClass}.${nameNode.text}` : nameNode.text;
1817
- funcStack.push(qualifiedName);
1818
- pushedFunc = true;
1870
+ let rawName: string;
1871
+ if (nameNode.type === 'computed_property_name') {
1872
+ const inner = nameNode.child(1);
1873
+ if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) {
1874
+ // Non-string computed key — skip adding to funcStack (no resolvable name).
1875
+ rawName = '';
1876
+ } else {
1877
+ rawName = inner.text.replace(/^['"]|['"]$/g, '');
1878
+ }
1879
+ } else {
1880
+ rawName = nameNode.text;
1881
+ }
1882
+ if (rawName) {
1883
+ const qualifiedName = enclosingClass ? `${enclosingClass}.${rawName}` : rawName;
1884
+ funcStack.push(qualifiedName);
1885
+ pushedFunc = true;
1886
+ }
1819
1887
  }
1820
1888
  } else if (t === 'variable_declarator') {
1821
1889
  // `const process = (arr) => { ... }` — arrow/expression functions assigned
@@ -1866,6 +1934,8 @@ function runContextCollectorWalk(rootNode: TreeSitterNode, out: ContextCollector
1866
1934
  collectCollectionWrapBinding(node, out.fnRefBindings);
1867
1935
  } else if (t === 'required_parameter' || t === 'optional_parameter') {
1868
1936
  handleParamTypeMap(node, out.typeMap);
1937
+ } else if (t === 'public_field_definition' || t === 'field_definition') {
1938
+ handleFieldDefTypeMap(node, out.typeMap, typeMapClass);
1869
1939
  } else if (t === 'assignment_expression') {
1870
1940
  handlePropWriteTypeMap(node, out.typeMap, typeMapClass);
1871
1941
  } else if (t === 'call_expression') {
@@ -2090,6 +2160,55 @@ function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEn
2090
2160
  }
2091
2161
  }
2092
2162
 
2163
+ /**
2164
+ * Extract type info from a class field declaration: `private repo: Repository<User>`.
2165
+ *
2166
+ * Seeds a class-scoped key `ClassName.field` (confidence 0.9) as the primary entry
2167
+ * so that two classes with identically-named fields don't overwrite each other's
2168
+ * typeMap entry (issue #1458). The resolver's `CallerClass.X` fallback (call-resolver.ts
2169
+ * line 110) looks up exactly this key.
2170
+ *
2171
+ * Bare `field` and `this.field` keys are kept at lower confidence (0.6) as fallbacks
2172
+ * for single-class files where the resolver may not have a callerClass context.
2173
+ *
2174
+ * Mirrors the field_definition branch of match_js_type_map in
2175
+ * crates/codegraph-core/src/extractors/javascript.rs.
2176
+ */
2177
+ function handleFieldDefTypeMap(
2178
+ node: TreeSitterNode,
2179
+ typeMap: Map<string, TypeMapEntry>,
2180
+ currentClass: string | null,
2181
+ ): void {
2182
+ const nameNode =
2183
+ node.childForFieldName('name') ||
2184
+ node.childForFieldName('property') ||
2185
+ findChild(node, 'property_identifier');
2186
+ if (!nameNode) return;
2187
+ const kind = nameNode.type;
2188
+ if (
2189
+ kind !== 'property_identifier' &&
2190
+ kind !== 'identifier' &&
2191
+ kind !== 'private_property_identifier'
2192
+ )
2193
+ return;
2194
+ const typeAnno = findChild(node, 'type_annotation');
2195
+ if (!typeAnno) return;
2196
+ const typeName = extractSimpleTypeName(typeAnno);
2197
+ if (!typeName) return;
2198
+ if (currentClass) {
2199
+ // Primary: class-scoped key prevents cross-class collision (issue #1458).
2200
+ setTypeMapEntry(typeMap, `${currentClass}.${nameNode.text}`, typeName, 0.9);
2201
+ // Fallback: bare keys at lower confidence for single-class files or when
2202
+ // the resolver does not have a callerClass in scope.
2203
+ setTypeMapEntry(typeMap, nameNode.text, typeName, 0.6);
2204
+ setTypeMapEntry(typeMap, `this.${nameNode.text}`, typeName, 0.6);
2205
+ } else {
2206
+ // No enclosing class declaration (e.g. class expression) — use bare keys only.
2207
+ setTypeMapEntry(typeMap, nameNode.text, typeName, 0.9);
2208
+ setTypeMapEntry(typeMap, `this.${nameNode.text}`, typeName, 0.9);
2209
+ }
2210
+ }
2211
+
2093
2212
  /**
2094
2213
  * Phase 8.3d: seed the pts map from object property writes.
2095
2214
  *
@@ -3308,11 +3427,13 @@ function emitPrototypeMethod(
3308
3427
  ): void {
3309
3428
  const fullName = `${className}.${methodName}`;
3310
3429
  if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
3430
+ const params = extractParameters(rhs);
3311
3431
  definitions.push({
3312
3432
  name: fullName,
3313
3433
  kind: 'method',
3314
3434
  line: nodeStartLine(rhs),
3315
3435
  endLine: nodeEndLine(rhs),
3436
+ children: params.length > 0 ? params : undefined,
3316
3437
  });
3317
3438
  } else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
3318
3439
  // Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
@@ -324,7 +324,7 @@ export function hotspotsData(
324
324
  metric: string;
325
325
  level: string;
326
326
  limit: number;
327
- hotspots: unknown[];
327
+ items: unknown[];
328
328
  } {
329
329
  const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
330
330
  try {
@@ -337,19 +337,19 @@ export function hotspotsData(
337
337
  // ── Native fast path: single query instead of 4 eagerly prepared ──
338
338
  if (nativeDb?.getHotspots) {
339
339
  const rows = nativeDb.getHotspots(kind, metric, noTests, limit);
340
- const hotspots = rows.map(mapNativeHotspotRow);
341
- const base = { metric, level, limit, hotspots };
342
- return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
340
+ const items = rows.map(mapNativeHotspotRow);
341
+ const base = { metric, level, limit, items };
342
+ return paginateResult(base, 'items', { limit: opts.limit, offset: opts.offset });
343
343
  }
344
344
 
345
345
  // ── JS fallback ───────────────────────────────────────────────────
346
346
  const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
347
347
  const stmt = db.prepare(buildHotspotQuery(metric, testFilter));
348
348
  const rows = stmt.all(kind, limit) as HotspotRow[];
349
- const hotspots = rows.map(mapJsHotspotRow);
349
+ const items = rows.map(mapJsHotspotRow);
350
350
 
351
- const base = { metric, level, limit, hotspots };
352
- return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
351
+ const base = { metric, level, limit, items };
352
+ return paginateResult(base, 'items', { limit: opts.limit, offset: opts.offset });
353
353
  } finally {
354
354
  close();
355
355
  }
package/src/index.ts CHANGED
@@ -52,7 +52,11 @@ export { ownersData } from './features/owners.js';
52
52
  export { sequenceData } from './features/sequence.js';
53
53
  export { hotspotsData, moduleBoundariesData, structureData } from './features/structure.js';
54
54
  export { triageData } from './features/triage.js';
55
- export { loadConfig } from './infrastructure/config.js';
55
+ export {
56
+ loadConfig,
57
+ loadConfigWithProvenance,
58
+ resolveUserConfigPath,
59
+ } from './infrastructure/config.js';
56
60
  export type { ArrayCompatSet } from './shared/constants.js';
57
61
  export { EXTENSIONS, IGNORE_DIRS } from './shared/constants.js';
58
62
  export {