graph-indexer 1.1.0 → 2.0.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 (59) hide show
  1. package/CHANGELOG.md +412 -0
  2. package/README.md +228 -350
  3. package/SECURITY.md +68 -0
  4. package/TRADEMARK.md +31 -0
  5. package/cli-ui.mjs +78 -0
  6. package/config.mjs +269 -0
  7. package/daemon-ctl.mjs +175 -0
  8. package/daemon-lock.mjs +69 -0
  9. package/embedders/python/mlx_embed_server.py +61 -0
  10. package/embedders/python/requirements-mlx.txt +5 -0
  11. package/embedders/setup-mlx.mjs +110 -0
  12. package/embeddings.mjs +489 -0
  13. package/engine/binary.mjs +399 -0
  14. package/{core-engine.mjs → engine/memory.mjs} +359 -249
  15. package/engine/sqlite.mjs +863 -0
  16. package/enrichment.mjs +426 -0
  17. package/git-signals.mjs +248 -0
  18. package/indexer.mjs +216 -77
  19. package/init.mjs +1604 -170
  20. package/layout.mjs +123 -0
  21. package/mcp/format.mjs +61 -0
  22. package/mcp/tools.mjs +1157 -0
  23. package/mcp/topology.mjs +551 -0
  24. package/mcp-server.mjs +118 -573
  25. package/package.json +56 -16
  26. package/parse/extractor.mjs +617 -0
  27. package/parse/imports.mjs +365 -0
  28. package/parse/languages.mjs +89 -0
  29. package/parse/metadata.mjs +655 -0
  30. package/parse/routes.mjs +327 -0
  31. package/prompts/CORE.md +86 -0
  32. package/prompts/DOMAIN_TEMPLATE.md +46 -0
  33. package/prompts/INTEGRATION.md +123 -0
  34. package/prompts/frameworks/ANDROID.md +17 -0
  35. package/prompts/frameworks/ASPNET_CORE.md +19 -0
  36. package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
  37. package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
  38. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
  39. package/prompts/frameworks/RAILS.md +20 -0
  40. package/prompts/frameworks/REACT.md +20 -0
  41. package/prompts/frameworks/SPRING_BOOT.md +19 -0
  42. package/prompts/languages/BASH.md +22 -0
  43. package/prompts/languages/C.md +21 -0
  44. package/prompts/languages/CSHARP.md +24 -0
  45. package/prompts/languages/CSS_SCSS.md +22 -0
  46. package/prompts/languages/GO.md +23 -0
  47. package/prompts/languages/JAVA.md +23 -0
  48. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
  49. package/prompts/languages/KOTLIN.md +24 -0
  50. package/prompts/languages/PHP.md +23 -0
  51. package/prompts/languages/PYTHON.md +23 -0
  52. package/prompts/languages/RUBY.md +23 -0
  53. package/prompts/languages/RUST.md +21 -0
  54. package/prompts/languages/SWIFT.md +21 -0
  55. package/search-core.mjs +627 -0
  56. package/storage.mjs +68 -0
  57. package/watch-daemon.mjs +204 -69
  58. package/PROMPT.md +0 -143
  59. package/parser-utils.mjs +0 -861
@@ -0,0 +1,655 @@
1
+ /**
2
+ * @file parse/metadata.mjs
3
+ * @description Chunk metadata extractors: params, return type, class context,
4
+ * decorators, heritage (extends/implements), type annotations,
5
+ * call sites, and the legacy call list.
6
+ * @author MaquinaTech <https://github.com/MaquinaTech>
7
+ * @copyright (c) 2026 MaquinaTech. All rights reserved.
8
+ * @license MIT
9
+ */
10
+
11
+ // ─── Enrichment helpers (param names, return type, class context) ─────────────
12
+
13
+ export function extractParams(chunkNode, ext) {
14
+ const params = [];
15
+ const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
16
+ const paramTypes = JS_LIKE.includes(ext)
17
+ ? ['required_parameter', 'optional_parameter', 'formal_parameters', 'identifier']
18
+ : ['parameter', 'formal_parameter', 'identifier'];
19
+
20
+ function walkParams(node) {
21
+ if (node.type === 'formal_parameters' || node.type === 'parameters' || node.type === 'parameter_list') {
22
+ for (const child of node.children) {
23
+ if (child.type === 'required_parameter' || child.type === 'optional_parameter' || child.type === 'formal_parameter') {
24
+ const id = child.childForFieldName?.('pattern') || child.childForFieldName?.('name') ||
25
+ child.children.find(c => c.type === 'identifier');
26
+ if (id) params.push(id.text);
27
+ const typeAnnotation = child.childForFieldName?.('type');
28
+ if (typeAnnotation) {
29
+ const typeText = typeAnnotation.text.replace(/^:\s*/, '').trim();
30
+ if (typeText) params.push(typeText);
31
+ }
32
+ } else if (child.type === 'identifier') {
33
+ params.push(child.text);
34
+ }
35
+ }
36
+ }
37
+ for (const child of node.children) walkParams(child);
38
+ }
39
+
40
+ // Avoid deep recursion into the function body by walking only the params node.
41
+ const paramsNode = chunkNode.childForFieldName?.('parameters') || chunkNode.childForFieldName?.('formal_parameters');
42
+ if (paramsNode) walkParams(paramsNode);
43
+ return [...new Set(params)].filter(p => p && p.length > 1).slice(0, 15);
44
+ }
45
+
46
+ export function extractReturnType(chunkNode, ext) {
47
+ const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
48
+ if (JS_LIKE.includes(ext)) {
49
+ const retTypeNode = chunkNode.childForFieldName?.('return_type');
50
+ if (retTypeNode) return retTypeNode.text.replace(/^:\s*/, '').trim().slice(0, 80);
51
+ }
52
+ return '';
53
+ }
54
+
55
+ export function extractClassContext(chunkNode) {
56
+ let parent = chunkNode.parent;
57
+ while (parent) {
58
+ if (parent.type === 'class_declaration' || parent.type === 'class_definition' ||
59
+ parent.type === 'class_body' || parent.type === 'impl_item') {
60
+ const nameNode = parent.childForFieldName?.('name');
61
+ if (nameNode) return nameNode.text;
62
+ }
63
+ parent = parent.parent;
64
+ }
65
+ return '';
66
+ }
67
+
68
+ /**
69
+ * Extract decorator / annotation names applied to a chunk (and, for class chunks,
70
+ * to the methods inside it). Decorators encode what a symbol *is* in modern
71
+ * frameworks — `@Controller`, `@Injectable`, `@Get`, `@Entity` (TS: NestJS,
72
+ * Angular, TypeORM) and `@app.route`, `@pytest.fixture`, `@dataclass`,
73
+ * `@property` (Python) — yet as raw snippet text they are diluted to a single
74
+ * low-weight token inside a large class body. Surfacing them as a dedicated field
75
+ * lets a class annotated `@Controller` be retrieved by "controller" and a method
76
+ * annotated `@Get` by "get/route", independent of language.
77
+ *
78
+ * Generalises by node type only (the tree-sitter `decorator` node is shared across
79
+ * TS/JS/Python grammars) — no framework-specific names are hardcoded. Callee
80
+ * arguments are stripped: `@Controller('cats')` -> 'Controller',
81
+ * `@app.route('/x')` -> 'app.route', `@UseGuards(AuthGuard)` -> 'UseGuards'.
82
+ *
83
+ * @returns {string[]} unique decorator callee names (max 24)
84
+ */
85
+ export function extractDecorators(chunkNode) {
86
+ const names = new Set();
87
+ const addDecorator = (decoNode) => {
88
+ let t = (decoNode.text || '').trim().replace(/^@/, '');
89
+ t = t.split('(')[0]; // drop call arguments: @Get(':id') -> Get
90
+ t = t.split(/[\s\n{]/)[0].trim(); // first token only
91
+ if (t && t.length <= 64) names.add(t);
92
+ };
93
+
94
+ // (1) Decorators that PRECEDE the chunk as siblings. Python wraps a decorated
95
+ // symbol in `decorated_definition` ([decorator…, def]); some grammars place
96
+ // class decorators as leading siblings rather than children.
97
+ let prev = chunkNode.previousSibling;
98
+ while (prev) {
99
+ if (prev.type === 'decorator') addDecorator(prev);
100
+ else if (prev.isNamed && prev.type !== 'comment') break;
101
+ prev = prev.previousSibling;
102
+ }
103
+
104
+ // (2) Decorators within the chunk's subtree. A captured TS class chunk (the
105
+ // enclosing export_statement / class_declaration) carries its own class
106
+ // decorators plus the @Get/@Post/@Inject decorators on its methods.
107
+ // Bounded traversal so a large class body cannot inflate indexing time.
108
+ let budget = 800;
109
+ const stack = [chunkNode];
110
+ while (stack.length && budget-- > 0) {
111
+ const n = stack.pop();
112
+ for (let i = 0; i < n.namedChildCount; i++) {
113
+ const child = n.namedChild(i);
114
+ if (child.type === 'decorator') addDecorator(child);
115
+ else stack.push(child);
116
+ }
117
+ }
118
+
119
+ return Array.from(names).slice(0, 24);
120
+ }
121
+
122
+ /**
123
+ * Extract the base classes and implemented interfaces of a class chunk — the
124
+ * inheritance edge that links a concept to its implementations
125
+ * (`class ValidationPipe extends BasePipe implements PipeTransform`). Surfacing
126
+ * this lets an agent move from an abstract type to the concrete classes that
127
+ * realise it, and feeds the semantic embedding so an implementation is retrievable
128
+ * by the interface it fulfils.
129
+ *
130
+ * Generalises across `extends`/`implements` (TS/JS) and base-class argument lists
131
+ * (Python) by node type. Returns parent type names, e.g.
132
+ * ['BasePipe', 'PipeTransform', 'OnInit'].
133
+ *
134
+ * @returns {string[]} base/interface names (max 12)
135
+ */
136
+ export function extractHeritage(chunkNode, ext) {
137
+ const bases = new Set();
138
+ const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
139
+
140
+ const addTypeNames = (node) => {
141
+ const stack = [node];
142
+ let budget = 200;
143
+ while (stack.length && budget-- > 0) {
144
+ const n = stack.pop();
145
+ if ((n.type === 'type_identifier' || n.type === 'identifier') && /^[A-Za-z_$]/.test(n.text)) {
146
+ bases.add(n.text);
147
+ }
148
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
149
+ }
150
+ };
151
+
152
+ if (JS_LIKE.includes(ext)) {
153
+ // Walk the chunk subtree for extends/implements clauses, but never descend
154
+ // into class_body — base types of the class only, not its method internals.
155
+ const stack = [chunkNode];
156
+ let budget = 400;
157
+ while (stack.length && budget-- > 0) {
158
+ const n = stack.pop();
159
+ if (n.type === 'extends_clause' || n.type === 'implements_clause') { addTypeNames(n); continue; }
160
+ for (let i = 0; i < n.namedChildCount; i++) {
161
+ const c = n.namedChild(i);
162
+ if (c.type !== 'class_body' && c.type !== 'statement_block') stack.push(c);
163
+ }
164
+ }
165
+ } else if (ext === '.py') {
166
+ const sc = chunkNode.childForFieldName?.('superclasses');
167
+ if (sc) {
168
+ for (let i = 0; i < sc.namedChildCount; i++) {
169
+ const c = sc.namedChild(i);
170
+ // skip keyword args like metaclass=… (keyword_argument node)
171
+ if (c.type === 'identifier' || c.type === 'attribute') bases.add(c.text);
172
+ }
173
+ }
174
+ } else {
175
+ // Other indexed languages: each grammar exposes the base class / implemented
176
+ // interface / supertrait names under a small set of "heritage clause" node
177
+ // types (discovered per grammar). We walk the chunk for those clause nodes
178
+ // and collect the capitalized type names inside — cheap, no type inference.
179
+ // A grammar we don't have a clause for simply yields [] (today's behaviour).
180
+ const CLAUSES = HERITAGE_CLAUSES[ext];
181
+ if (CLAUSES) {
182
+ const stack = [chunkNode];
183
+ let budget = 400;
184
+ while (stack.length && budget-- > 0) {
185
+ const n = stack.pop();
186
+ if (CLAUSES.has(n.type)) { collectHeritageNames(n, bases); continue; }
187
+ for (let i = 0; i < n.namedChildCount; i++) {
188
+ const c = n.namedChild(i);
189
+ if (!HERITAGE_BODY_TYPES.has(c.type)) stack.push(c); // base types sit before the body
190
+ }
191
+ }
192
+ }
193
+ // Go has no inheritance keyword — embedding (an anonymous field whose name IS
194
+ // a type) is the closest "is-a/has-a" edge, so surface embedded type names.
195
+ if (ext === '.go') collectGoEmbedding(chunkNode, bases);
196
+ }
197
+ return Array.from(bases).slice(0, 12);
198
+ }
199
+
200
+ // Per-grammar node types whose subtree holds base/interface/supertrait names.
201
+ const HERITAGE_CLAUSES = {
202
+ '.java': new Set(['superclass', 'super_interfaces']),
203
+ '.cs': new Set(['base_list']),
204
+ '.php': new Set(['base_clause', 'class_interface_clause']),
205
+ '.kt': new Set(['delegation_specifier']),
206
+ '.swift': new Set(['inheritance_specifier']),
207
+ '.rb': new Set(['superclass']),
208
+ '.rs': new Set(['trait_bounds']), // supertrait bounds on a trait_item
209
+ };
210
+ // Member-body nodes the heritage walk must not descend into (base types precede them).
211
+ const HERITAGE_BODY_TYPES = new Set([
212
+ 'class_body', 'declaration_list', 'enum_body', 'statement_block', 'block',
213
+ 'function_body', 'field_declaration_list', 'interface_body', 'struct_body', 'class_body_declaration',
214
+ ]);
215
+
216
+ /** Collect capitalized type names anywhere under a heritage-clause node. */
217
+ function collectHeritageNames(node, set) {
218
+ const stack = [node];
219
+ let budget = 200;
220
+ while (stack.length && budget-- > 0) {
221
+ const n = stack.pop();
222
+ if (HERITAGE_NAME_TYPES.has(n.type) && /^[A-Z]/.test(n.text || '')) set.add(n.text);
223
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
224
+ }
225
+ }
226
+ const HERITAGE_NAME_TYPES = new Set([
227
+ 'type_identifier', 'identifier', 'constant', 'simple_identifier', 'name', 'scoped_type_identifier',
228
+ ]);
229
+
230
+ /** Go struct/interface embedding: a field/spec whose name IS a bare type. */
231
+ function collectGoEmbedding(chunkNode, set) {
232
+ const stack = [chunkNode];
233
+ let budget = 400;
234
+ while (stack.length && budget-- > 0) {
235
+ const n = stack.pop();
236
+ // An embedded field_declaration has a type but no field name; the type node
237
+ // is the embed. (A named field `leash Leash` has a `name` child.)
238
+ if (n.type === 'field_declaration' && !n.childForFieldName?.('name')) {
239
+ for (let i = 0; i < n.namedChildCount; i++) {
240
+ const c = n.namedChild(i);
241
+ if ((c.type === 'type_identifier' || c.type === 'qualified_type') && /^[A-Z]/.test(c.text || '')) {
242
+ set.add(c.text.split('.').pop());
243
+ }
244
+ }
245
+ }
246
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
247
+ }
248
+ }
249
+
250
+ // C#: C# spells type names as bare `identifier` / `generic_name` (NOT the
251
+ // `type_identifier` node the cross-language branch keys on), so the generic walk
252
+ // yields [] for C#. These constants drive a `.cs`-gated branch that reads types
253
+ // only from the field-precise positions where an identifier IS a type, so we never
254
+ // mistake a variable/member name for a type.
255
+ const CS_TYPE_HOST = new Set(['parameter', 'variable_declaration', 'property_declaration', 'method_declaration', 'base_list']);
256
+ // Generic-constraint / contextual keywords that occupy type position but aren't types.
257
+ const CS_TYPE_KEYWORDS = new Set(['where', 'new', 'class', 'struct', 'unmanaged', 'notnull', 'var']);
258
+
259
+ /** Collect simple C# type names from a type-position node (descending through
260
+ * generics / arrays / nullables / tuples). Skips predefined primitives and `var`,
261
+ * keeps only PascalCase names, and reduces a qualified name to its last segment. */
262
+ function _csTypeNamesFrom(node, out) {
263
+ if (!node) return;
264
+ const stack = [node];
265
+ let budget = 200;
266
+ while (stack.length && budget-- > 0) {
267
+ const n = stack.pop();
268
+ const t = n.type;
269
+ if (t === 'predefined_type' || t === 'implicit_type') continue; // int/string/void/var…
270
+ if (t === 'identifier' || t === 'type_identifier') {
271
+ const nm = n.text;
272
+ if (nm && /^[A-Z]/.test(nm) && !CS_TYPE_KEYWORDS.has(nm)) out.add(nm);
273
+ continue;
274
+ }
275
+ if (t === 'qualified_name') {
276
+ // The type is the LAST segment; it may itself be a generic_name
277
+ // (System.Collections.Generic.List<Order>). Descend into just that
278
+ // segment → "List" + "Order" (not a malformed "List<Order>"), while
279
+ // never visiting the namespace identifiers (System/Collections/Generic).
280
+ const last = n.namedChild(n.namedChildCount - 1);
281
+ if (last && (last.type === 'generic_name' || last.type === 'identifier' || last.type === 'type_identifier')) {
282
+ stack.push(last);
283
+ } else {
284
+ const seg = (n.text || '').split('.').pop();
285
+ if (seg && /^[A-Z]/.test(seg) && !CS_TYPE_KEYWORDS.has(seg)) out.add(seg);
286
+ }
287
+ continue;
288
+ }
289
+ // generic_name / array_type / nullable_type / tuple_type / type_argument_list /
290
+ // pointer_type … → descend to reach the inner type names.
291
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
292
+ }
293
+ }
294
+
295
+ export function extractTypeAnnotations(chunkNode, ext) {
296
+ const types = new Set();
297
+ // JS/TS + Python use the precise annotation branches below. Every other indexed
298
+ // language is covered by the shared `type_identifier` branch (that node appears
299
+ // ONLY in type position across Go/Rust/Java/Kotlin/Swift/C) plus a `named_type`
300
+ // branch for PHP — so the type-user dimension of find_references works in those
301
+ // languages too, with no early-out and no type inference. C# is the exception:
302
+ // it spells types as bare `identifier`/`generic_name`, so it gets a dedicated
303
+ // field-precise branch below (gated on `.cs`). Ruby (dynamically typed) still
304
+ // carries no cheap type signal and naturally yields [].
305
+
306
+ function walk(node) {
307
+ if (node.type === 'type_annotation') {
308
+ const typeText = node.text.replace(/^:\s*/, '').trim();
309
+ const PRIMITIVES = new Set(['string', 'number', 'boolean', 'void', 'any', 'unknown', 'never', 'null', 'undefined', 'object', 'symbol', 'bigint']);
310
+ for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
311
+ if (!PRIMITIVES.has(match[1].toLowerCase())) types.add(match[1]);
312
+ }
313
+ }
314
+ // type_identifier / generic_type: a named type in type position. This is the
315
+ // cross-language branch — type_identifier is how Go/Rust/Java/Kotlin/Swift/C
316
+ // (and TS) all spell a referenced type name.
317
+ else if (node.type === 'type_identifier' || node.type === 'generic_type') {
318
+ const name = node.children[0]?.text || node.text;
319
+ if (name && /^[A-Z]/.test(name)) types.add(name);
320
+ }
321
+ // PHP: a typed parameter / return uses `named_type` (e.g. `Owner`, `\App\User`).
322
+ else if (node.type === 'named_type') {
323
+ const seg = (node.text || '').replace(/^\?/, '').split('\\').pop().trim();
324
+ if (seg && /^[A-Z]/.test(seg)) types.add(seg);
325
+ }
326
+ else if (node.type === 'annotation' && ext === '.py') {
327
+ const typeText = node.text.replace(/^->\s*|^:\s*/, '').trim();
328
+ for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
329
+ types.add(match[1]);
330
+ }
331
+ }
332
+ // C#: types live as bare identifiers in field-precise positions — a
333
+ // parameter/field/property/local's `type`, a method's `returns`, and the
334
+ // class `base_list`. Additive and `.cs`-only, so all other languages are
335
+ // byte-identical to before.
336
+ else if (ext === '.cs' && CS_TYPE_HOST.has(node.type)) {
337
+ if (node.type === 'base_list') {
338
+ _csTypeNamesFrom(node, types);
339
+ } else {
340
+ _csTypeNamesFrom(node.childForFieldName?.('type') || node.childForFieldName?.('returns') || null, types);
341
+ }
342
+ }
343
+ for (const child of node.children) walk(child);
344
+ }
345
+ walk(chunkNode);
346
+ return Array.from(types).slice(0, 20);
347
+ }
348
+
349
+ // Call names that are framework/stdlib noise rather than project call edges.
350
+ const CALL_NOISE = new Set(['require', 'console', 'log', 'expect', 'test', 'it', 'describe', 'setTimeout', 'print', 'println!']);
351
+ function _validCallName(c) { return Boolean(c) && c.length > 2 && !CALL_NOISE.has(c); }
352
+
353
+ // Shell builtins / ubiquitous coreutils — emitting these as Bash call edges would
354
+ // bury the project's own function-to-function calls in noise (they never resolve to
355
+ // an indexed symbol anyway). Project functions and notable tools (docker, git, npm,
356
+ // kubectl…) are kept.
357
+ const BASH_BUILTINS = new Set([
358
+ 'echo', 'cd', 'ls', 'cat', 'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'touch', 'ln',
359
+ 'export', 'local', 'readonly', 'declare', 'unset', 'shift', 'read', 'printf',
360
+ 'exit', 'return', 'eval', 'exec', 'trap', 'wait', 'sleep', 'pwd', 'set', 'shopt',
361
+ 'source', 'test', 'true', 'false', 'kill', 'jobs', 'type', 'command', 'getopts',
362
+ 'grep', 'sed', 'awk', 'cut', 'tr', 'sort', 'uniq', 'head', 'tail', 'find', 'xargs',
363
+ 'chmod', 'chown', 'tee', 'wc', 'basename', 'dirname', 'tar', 'curl', 'wget', 'env',
364
+ ]);
365
+
366
+ /**
367
+ * Collapse a call's receiver expression into a compact disambiguation hint:
368
+ * • '' — unqualified call (`foo()`): a free function or in-scope name.
369
+ * • 'this' — `this.`/`self.`-rooted: dispatch on the SAME instance/class.
370
+ * • '<ident>' — the last identifier of the receiver expression (`db.save()` → 'db',
371
+ * `UserService.find()` → 'UserService'), the only cheap type signal
372
+ * available without full inference.
373
+ * This is what lets get_call_graph separate the real callers of `OrderService.save`
374
+ * from every unrelated `save()` in the repo (see mcp/topology.mjs classifyCallers).
375
+ */
376
+ function _receiverHint(objNode) {
377
+ if (!objNode) return '';
378
+ const t = objNode.text || '';
379
+ if (/^(this|self)\b/.test(t)) return 'this';
380
+ const segs = t.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
381
+ return segs.length ? segs[segs.length - 1] : '';
382
+ }
383
+
384
+ /**
385
+ * C#: the invoked-name node is either a plain `identifier` or a `generic_name`
386
+ * (`Method<T>`, children = identifier + type_argument_list) whose leading
387
+ * `identifier` is the real method name. Strip the type-argument list so the
388
+ * recorded callee is `Method`, not `Method<T>`.
389
+ */
390
+ function _csInvokedName(node) {
391
+ if (!node) return '';
392
+ if (node.type === 'identifier') return node.text;
393
+ const id = node.children?.find(c => c.type === 'identifier');
394
+ return id ? id.text : (node.text || '').split('<')[0];
395
+ }
396
+
397
+ // ─── Scope-aware receiver type inference (intra-procedural, best-effort) ──────────
398
+ // `db.s.save()` and `const s = getStore(); s.save()` both record a receiver hint of
399
+ // `s` (a variable, not a type), so classifyCallers can only name-match them and they
400
+ // leak into every `save()` in the repo. A cheap pass over the SAME chunk subtree
401
+ // resolves the simple local bindings the AST already makes explicit — `new Repo()`,
402
+ // a constructor/factory call, and TS/Python type annotations — and tags the call
403
+ // site with `recv_type` (resolved here) or `recv_via_call` (a factory whose return
404
+ // type is resolved at query time from the index). This is strictly additive: a call
405
+ // site that cannot be resolved keeps exactly its old `{ name, recv }` shape and stays
406
+ // name-only. Only languages whose type is recoverable from the AST participate
407
+ // (TS/JS via `new`/annotations, Python via annotations/obvious constructors, plus the
408
+ // trivially-safe Java/C# `new Foo()`); receiver-less languages are untouched.
409
+
410
+ /** Clean a type-position node's text to a type string, or null if it carries no
411
+ * capitalized identifier (drops primitives like `string`/`number[]`). Generics and
412
+ * namespaces are preserved verbatim — classifyCallers token-matches against them. */
413
+ function _bindingTypeName(typeText) {
414
+ if (!typeText) return null;
415
+ const cleaned = typeText.replace(/^->\s*/, '').replace(/^[:\s]+/, '').trim().slice(0, 64);
416
+ return /[A-Z]/.test(cleaned) ? cleaned : null;
417
+ }
418
+
419
+ /** Peel await/parenthesized wrappers off a value expression (TS `await getStore()`,
420
+ * Python `await get_store()`). Bounded to avoid pathological nesting. */
421
+ function _unwrapValueNode(node) {
422
+ let n = node, guard = 0;
423
+ while (n && guard++ < 6) {
424
+ if (n.type === 'await_expression' || n.type === 'await') { n = n.namedChildren?.[0]; continue; }
425
+ if (n.type === 'parenthesized_expression') { n = n.namedChildren?.[0]; continue; }
426
+ break;
427
+ }
428
+ return n;
429
+ }
430
+
431
+ /**
432
+ * Classify a right-hand-side value into the type of the binding it produces:
433
+ * • { type } — directly recoverable now (`new Repo()`, `(x as Repo)`,
434
+ * a Python `Repo()` constructor, Java/C# `new Repo()`).
435
+ * • { viaCall } — a factory call whose return type is resolved later from the
436
+ * callee's recorded return_type (`getStore()`).
437
+ * • null — nothing safe to infer.
438
+ */
439
+ function _classifyValueNode(node) {
440
+ node = _unwrapValueNode(node);
441
+ if (!node) return null;
442
+ const t = node.type;
443
+ if (t === 'new_expression') { // TS/JS: new Repo()
444
+ const ctor = node.childForFieldName?.('constructor') || node.namedChildren?.[0];
445
+ const tn = _bindingTypeName(ctor?.text);
446
+ return tn ? { type: tn } : null;
447
+ }
448
+ if (t === 'object_creation_expression') { // Java / C#: new Repo() (trivially safe)
449
+ const ty = node.childForFieldName?.('type') || node.namedChildren?.find(c => /type|name|identifier/.test(c.type));
450
+ const tn = _bindingTypeName(ty?.text);
451
+ return tn ? { type: tn } : null;
452
+ }
453
+ if (t === 'as_expression' || t === 'satisfies_expression') { // TS: const s = x as Repo
454
+ const ty = node.childForFieldName?.('type') || node.namedChildren?.[node.namedChildren.length - 1];
455
+ const tn = _bindingTypeName(ty?.text);
456
+ if (tn) return { type: tn };
457
+ return _classifyValueNode(node.childForFieldName?.('expression') || node.namedChildren?.[0]);
458
+ }
459
+ if (t === 'call') { // Python: Repo() is a constructor by convention; get_store() is a factory
460
+ const fn = node.childForFieldName?.('function') || node.children?.[0];
461
+ if (fn?.type === 'identifier') {
462
+ return /^[A-Z]/.test(fn.text) ? { type: fn.text } : (_validCallName(fn.text) ? { viaCall: fn.text } : null);
463
+ }
464
+ if (fn?.type === 'attribute') {
465
+ const last = fn.childForFieldName?.('attribute')?.text; // mod.Repo()
466
+ if (last && /^[A-Z]/.test(last)) return { type: last };
467
+ }
468
+ return null;
469
+ }
470
+ if (t === 'call_expression') { // TS/JS/Swift/Go/Rust/C: factory call — defer to return_type
471
+ const fn = node.childForFieldName?.('function') || node.children?.[0];
472
+ if (fn && (fn.type === 'identifier' || fn.type === 'simple_identifier') && _validCallName(fn.text)) return { viaCall: fn.text };
473
+ return null;
474
+ }
475
+ return null;
476
+ }
477
+
478
+ /**
479
+ * Collect simple local variable → binding-type for one chunk subtree. Handles the
480
+ * common, unambiguous forms only; a variable bound to two different things is dropped
481
+ * (conflict) rather than guessed. `this`/`self` are excluded (handled by the `this`
482
+ * receiver bucket). One extra subtree pass per chunk — O(chunk size), no whole-program
483
+ * analysis — justified by the precision win on dynamically-typed receivers.
484
+ */
485
+ function _inferLocalBindings(rootNode) {
486
+ const bindings = new Map();
487
+ const set = (name, val) => {
488
+ if (!name || name === 'this' || name === 'self' || !val) return;
489
+ const prev = bindings.get(name);
490
+ if (prev === undefined) { bindings.set(name, val); return; }
491
+ if (prev.conflict) return;
492
+ if (prev.type !== val.type || prev.viaCall !== val.viaCall) bindings.set(name, { conflict: true });
493
+ };
494
+ function walk(node) {
495
+ const t = node.type;
496
+ if (t === 'variable_declarator') {
497
+ const nameNode = node.childForFieldName?.('name');
498
+ if (nameNode && nameNode.type === 'identifier') {
499
+ const val = _classifyValueNode(node.childForFieldName?.('value'));
500
+ if (val) set(nameNode.text, val);
501
+ }
502
+ } else if (t === 'required_parameter' || t === 'optional_parameter') {
503
+ const id = node.childForFieldName?.('pattern');
504
+ const ty = node.childForFieldName?.('type');
505
+ if (id && id.type === 'identifier' && ty) { const tn = _bindingTypeName(ty.text); if (tn) set(id.text, { type: tn }); }
506
+ } else if (t === 'assignment') {
507
+ const left = node.childForFieldName?.('left');
508
+ const ty = node.childForFieldName?.('type');
509
+ if (left && left.type === 'identifier') {
510
+ if (ty) { const tn = _bindingTypeName(ty.text); if (tn) set(left.text, { type: tn }); }
511
+ else { const val = _classifyValueNode(node.childForFieldName?.('right')); if (val) set(left.text, val); }
512
+ }
513
+ } else if (t === 'typed_parameter' || t === 'typed_default_parameter') {
514
+ const id = node.childForFieldName?.('name') || node.children?.find(c => c.type === 'identifier');
515
+ const ty = node.childForFieldName?.('type');
516
+ if (id && ty) { const tn = _bindingTypeName(ty.text); if (tn) set(id.text, { type: tn }); }
517
+ }
518
+ node.children.forEach(walk);
519
+ }
520
+ walk(rootNode);
521
+ return bindings;
522
+ }
523
+
524
+ /** Resolve a call site's receiver object node to a binding ({type}|{viaCall}|null):
525
+ * a simple variable via the binding map, or an inline `new Repo()`/`getStore()`. */
526
+ function _inferReceiverType(objNode, bindings) {
527
+ if (!objNode) return null;
528
+ if (objNode.type === 'identifier' || objNode.type === 'simple_identifier') {
529
+ const v = objNode.text;
530
+ if (v === 'this' || v === 'self') return null;
531
+ const b = bindings.get(v);
532
+ return (b && !b.conflict) ? b : null;
533
+ }
534
+ return _classifyValueNode(objNode); // inline: new Repo().save(), getStore().save()
535
+ }
536
+
537
+ /**
538
+ * Walk a subtree and collect every call site as { name, recv } (receiver hint), plus
539
+ * an optional `recv_type` / `recv_via_call` when the receiver's type is recoverable
540
+ * intra-procedurally (see the inference helpers above). Deduplicated by (name, recv).
541
+ * Cross-language: call_expression (JS/TS/Go/Rust/C and Swift via simple_identifier/
542
+ * navigation_expression), call (Python), macro_invocation (Rust), method_invocation
543
+ * (Java/C#), method_call (Ruby), command (Bash). The receiver is the precision half of
544
+ * the call graph — extractCalls() derives the legacy name-only list from these sites.
545
+ */
546
+ export function extractCallSites(rootNode) {
547
+ const sites = [];
548
+ const bindings = _inferLocalBindings(rootNode);
549
+ const seen = new Set();
550
+ const add = (name, recv, objNode = null) => {
551
+ if (!name) return;
552
+ const key = name + ' ' + recv;
553
+ if (seen.has(key)) return;
554
+ seen.add(key);
555
+ const site = { name, recv };
556
+ const inferred = objNode ? _inferReceiverType(objNode, bindings) : null;
557
+ if (inferred?.type) site.recv_type = inferred.type;
558
+ else if (inferred?.viaCall) site.recv_via_call = inferred.viaCall;
559
+ sites.push(site);
560
+ };
561
+ function walk(node) {
562
+ const t = node.type;
563
+ if (t === 'call_expression') {
564
+ const funcNode = node.childForFieldName?.('function') || node.children[0];
565
+ if (funcNode) {
566
+ if (funcNode.type === 'identifier') add(funcNode.text, '');
567
+ else if (funcNode.type === 'simple_identifier') add(funcNode.text, '');
568
+ else if (funcNode.type === 'member_expression' || funcNode.type === 'property_identifier') {
569
+ const prop = funcNode.childForFieldName?.('property');
570
+ const obj = funcNode.childForFieldName?.('object');
571
+ if (prop) add(prop.text, _receiverHint(obj), obj);
572
+ else add(funcNode.text.split('.').pop(), '');
573
+ } else if (funcNode.type === 'navigation_expression') { // Swift obj.method / self.method
574
+ const suffix = funcNode.childForFieldName?.('suffix');
575
+ const m = suffix?.namedChildren?.find(c => c.type === 'simple_identifier')?.text
576
+ || suffix?.text?.replace(/^\./, '');
577
+ const target = funcNode.childForFieldName?.('target');
578
+ if (m) add(m, _receiverHint(target), target);
579
+ }
580
+ }
581
+ } else if (t === 'command') {
582
+ const cmd = (node.childForFieldName?.('name')?.text || '').trim();
583
+ // bare identifiers only — skip paths, env-prefixed assignments, builtins.
584
+ if (cmd && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(cmd) && !BASH_BUILTINS.has(cmd)) add(cmd, '');
585
+ } else if (t === 'call') {
586
+ const funcNode = node.childForFieldName?.('function') || node.children[0];
587
+ if (funcNode) {
588
+ if (funcNode.type === 'identifier') add(funcNode.text, '');
589
+ else if (funcNode.type === 'attribute') {
590
+ const attr = funcNode.childForFieldName?.('attribute');
591
+ const obj = funcNode.childForFieldName?.('object');
592
+ if (attr) add(attr.text, _receiverHint(obj), obj);
593
+ }
594
+ }
595
+ } else if (t === 'macro_invocation') {
596
+ const macroNode = node.childForFieldName?.('macro') || node.children[0];
597
+ if (macroNode && macroNode.type === 'identifier') add(macroNode.text + '!', '');
598
+ } else if (t === 'method_invocation') {
599
+ const nameNode = node.childForFieldName?.('name') || node.children.find(c => c.type === 'identifier');
600
+ const obj = node.childForFieldName?.('object');
601
+ if (nameNode) add(nameNode.text, _receiverHint(obj), obj);
602
+ } else if (t === 'invocation_expression') { // C# — the grammar has NO method_invocation; a call is
603
+ // invocation_expression(function, arguments). The function is a bare identifier (same-class /
604
+ // using-static call) or a member_access_expression (obj.Method() / this.Method()).
605
+ const fn = node.childForFieldName?.('function');
606
+ if (fn) {
607
+ if (fn.type === 'member_access_expression') {
608
+ const nm = _csInvokedName(fn.childForFieldName?.('name'));
609
+ const expr = fn.childForFieldName?.('expression');
610
+ if (nm) add(nm, _receiverHint(expr), expr);
611
+ } else if (fn.type === 'identifier') {
612
+ add(fn.text, '');
613
+ } else if (fn.type === 'generic_name') {
614
+ const nm = _csInvokedName(fn);
615
+ if (nm) add(nm, '');
616
+ }
617
+ }
618
+ } else if (t === 'method_call') {
619
+ const method = node.childForFieldName?.('method') || node.children.find(c => c.type === 'identifier');
620
+ const receiver = node.childForFieldName?.('receiver');
621
+ if (method && method.type === 'identifier') add(method.text, _receiverHint(receiver), receiver);
622
+ } else if (t === 'function_call_expression') {
623
+ const fn = node.childForFieldName?.('function');
624
+ if (fn && (fn.type === 'name' || fn.type === 'qualified_name')) {
625
+ const nm = (fn.text || '').split('\\').filter(Boolean).pop();
626
+ if (nm) add(nm, '');
627
+ }
628
+ } else if (t === 'member_call_expression' || t === 'nullsafe_member_call_expression') {
629
+ const nameNode = node.childForFieldName?.('name');
630
+ const obj = node.childForFieldName?.('object');
631
+ if (nameNode && nameNode.type === 'name') add(nameNode.text, _receiverHint(obj), obj);
632
+ } else if (t === 'scoped_call_expression') {
633
+ const nameNode = node.childForFieldName?.('name');
634
+ const scope = node.childForFieldName?.('scope');
635
+ if (nameNode && nameNode.type === 'name') add(nameNode.text, _receiverHint(scope), scope);
636
+ }
637
+ node.children.forEach(walk);
638
+ }
639
+ walk(rootNode);
640
+ return sites.filter(s => _validCallName(s.name));
641
+ }
642
+
643
+ /** Legacy name-only outgoing-call list (unique callee names). Derived from
644
+ * extractCallSites so the two never diverge; preserved for the BM25 document
645
+ * and the back-compat findCallers contract. */
646
+ export function extractCalls(rootNode) {
647
+ const seen = new Set();
648
+ const out = [];
649
+ for (const { name } of extractCallSites(rootNode)) {
650
+ if (seen.has(name)) continue;
651
+ seen.add(name);
652
+ out.push(name);
653
+ }
654
+ return out;
655
+ }