graph-indexer 3.0.1 → 3.1.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 (52) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/README.md +61 -8
  3. package/bin/graph-indexer.mjs +46 -5
  4. package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
  5. package/integrations/claude-code/agents/code-structure.md +0 -3
  6. package/integrations/claude-code/skills/setup/SKILL.md +26 -0
  7. package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
  8. package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
  9. package/integrations/claude-code-local/README.md +10 -0
  10. package/integrations/claude-code-local/agents/code-structure.md +22 -0
  11. package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +3 -1
  12. package/package.json +1 -1
  13. package/src/cli/helper.mjs +15 -1
  14. package/src/cli/hook.mjs +123 -41
  15. package/src/cli/init.mjs +16 -1
  16. package/src/cli/resident.mjs +1 -1
  17. package/src/index/indexer.mjs +77 -41
  18. package/src/index/modules.mjs +76 -2
  19. package/src/index/parse-worker.mjs +12 -0
  20. package/src/index/parsepool.mjs +73 -0
  21. package/src/index/resolver.mjs +362 -23
  22. package/src/mcp/server.mjs +16 -12
  23. package/src/mcp/tools.mjs +417 -25
  24. package/src/parse/arity.mjs +39 -2
  25. package/src/parse/extract.mjs +269 -45
  26. package/src/parse/lang/ecma.mjs +2 -0
  27. package/src/parse/lang/go.mjs +2 -0
  28. package/src/parse/lang/jvm.mjs +33 -0
  29. package/src/parse/lang/markdown.mjs +92 -0
  30. package/src/parse/lang/python.mjs +43 -0
  31. package/src/parse/lang/rust.mjs +215 -15
  32. package/src/parse/lang/sfc.mjs +125 -0
  33. package/src/parse/languages.mjs +9 -2
  34. package/src/query/check.mjs +49 -13
  35. package/src/query/facts.mjs +3 -2
  36. package/src/query/injection-spring.mjs +405 -0
  37. package/src/query/injection.mjs +173 -0
  38. package/src/query/intel.mjs +76 -14
  39. package/src/query/issue.mjs +156 -0
  40. package/src/query/orm.mjs +286 -0
  41. package/src/query/read.mjs +6 -1
  42. package/src/query/report.mjs +53 -0
  43. package/src/query/routes.mjs +142 -0
  44. package/src/query/showcase.mjs +59 -0
  45. package/src/search/search.mjs +85 -17
  46. package/src/search/semantic.mjs +201 -0
  47. package/src/search/vectors.mjs +122 -0
  48. package/src/store/db.mjs +22 -4
  49. package/vendor/grammars/README.md +47 -0
  50. package/vendor/grammars/kotlin.wasm.gz +0 -0
  51. package/vendor/grammars/manifest.json +4 -4
  52. /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Dependency injection: the edge between a value a class receives and the code that provides it,
3
+ * which the compiler does not see. In NestJS a constructor parameter marked `@Inject(TOKEN)` gets
4
+ * whatever a module registered as `{ provide: TOKEN, useClass | useExisting | useFactory | useValue }`;
5
+ * its declared type is often an interface or a union, so neither the type checker nor grep for the
6
+ * type name leads to the implementation.
7
+ *
8
+ * Recognised from the indexed source (TypeScript):
9
+ * - providers: an object with `provide:` and one of `useClass`, `useExisting`, `useFactory`,
10
+ * `useValue`, on one line or over the lines that follow;
11
+ * - injection points: `@Inject(TOKEN)` on a constructor parameter or property.
12
+ * Test files are left out: their providers are mocks.
13
+ * A token is the definition the index binds it to (a constant, a class, a symbol); a string, a
14
+ * constant declared as that string and `Class.name` are one token. An expression such as
15
+ * `getRepositoryToken(User)` is matched by its text, only when written the same way, and marked so;
16
+ * tokens computed at run time (`Object.values(Namespace).map(ns => ({ provide: ns, … }))`) are not
17
+ * found. Microservice clients registered with `ClientsModule.register([{ name: TOKEN }])` are
18
+ * providers too.
19
+ */
20
+
21
+ import { collectSpring } from './injection-spring.mjs';
22
+
23
+ const PROVIDE = /\bprovide\s*:\s*([^,}\n]+?)\s*(?:,|}|$)/;
24
+ const USE = /\buse(Class|Existing|Factory|Value)\s*:\s*([^,}\n]*)/;
25
+ const INJECT = /@Inject\(\s*([^)]*?)\s*\)/g;
26
+ const PARAM = /^\s*(?:(?:private|public|protected|readonly|override)\s+)*([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*([^,=)]+)/;
27
+
28
+ const strip = (s) => String(s ?? '').trim().replace(/\s+/g, ' ');
29
+
30
+ /** The definition a name on a line is bound to, through the index. */
31
+ function bound(intel, fileId, line, name) {
32
+ if (!/^[A-Za-z_$][\w$]*$/.test(name)) return null;
33
+ const r = intel.store.get('SELECT dst_id FROM refs WHERE file_id = ? AND line = ? AND name = ? AND dst_id IS NOT NULL LIMIT 1', fileId, line, name);
34
+ return r ? symRow(intel, r.dst_id) : null;
35
+ }
36
+
37
+ function symRow(intel, id) {
38
+ return intel.store.get('SELECT s.id, s.name, s.qname, s.kind, s.start_line, s.end_line, s.parent_id, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id = ?', id) ?? null;
39
+ }
40
+
41
+ /** A constant's string value (`const REFLECTOR = 'Reflector'`), if it is declared as one. */
42
+ function stringValue(intel, s) {
43
+ if (s.kind !== 'constant' && s.kind !== 'variable') return null;
44
+ const l = intel.fileLines(s.path)?.[s.start_line - 1] ?? '';
45
+ return new RegExp(`\\b${s.name}\\s*(?::[^=]+)?=\\s*(['"\`])([^'"\`]*)\\1\\s*;?\\s*$`).exec(l)?.[2] ?? null;
46
+ }
47
+
48
+ /**
49
+ * A token's identity: the definition it names, or its text (strings and expressions). A string,
50
+ * a constant holding one and `Class.name` are the same token when the strings are equal; `p.provide`
51
+ * is the token of the provider declared as `p`.
52
+ */
53
+ function tokenKey(intel, fileId, line, text) {
54
+ const t = strip(text);
55
+ const dotted = /^([\w$]+)\.(name|provide)$/.exec(t);
56
+ if (dotted) {
57
+ const head = bound(intel, fileId, line, dotted[1]);
58
+ if (head && dotted[2] === 'name' && head.kind === 'class') return { key: `str:${head.name}`, token: t, sym: head, exact: true };
59
+ if (head && dotted[2] === 'provide') return { key: `holder:${head.id}`, token: t, sym: head, exact: true };
60
+ }
61
+ const last = /^(?:[\w$]+\.)*([\w$]+)$/.exec(t)?.[1];
62
+ let s = last ? bound(intel, fileId, line, last) : null;
63
+ // a member of a type (`FactoryProvider.provide`) is a shape, not a token
64
+ if (s && (s.kind === 'field' || s.kind === 'property')) s = null;
65
+ if (s) {
66
+ const v = stringValue(intel, s);
67
+ return { key: v != null ? `str:${v}` : `id:${s.id}`, token: s.qname, sym: s, exact: true };
68
+ }
69
+ const lit = /^(['"`])(.*)\1$/.exec(t);
70
+ return { key: lit ? `str:${lit[2]}` : `text:${t}`, token: t, sym: null, exact: !!lit };
71
+ }
72
+
73
+ /** The variable or constant a line is declared in (`const catsProvider = { provide: … }`). */
74
+ function holderAt(intel, fileId, line) {
75
+ return intel.store.get(`SELECT id FROM symbols WHERE file_id = ? AND kind IN ('variable', 'constant') AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1`, fileId, line, line)?.id ?? null;
76
+ }
77
+
78
+ /** Is the line inside an interface or a type alias? */
79
+ function inTypeDeclaration(intel, fileId, line) {
80
+ const s = intel.store.get(`SELECT kind FROM symbols WHERE file_id = ? AND start_line <= ? AND end_line >= ? AND kind IN ('class', 'interface', 'type', 'function', 'method', 'variable', 'constant')
81
+ ORDER BY start_line DESC LIMIT 1`, fileId, line, line);
82
+ return s?.kind === 'interface' || s?.kind === 'type';
83
+ }
84
+
85
+ /** The innermost class around a line. */
86
+ function classAt(intel, fileId, line) {
87
+ return intel.store.get(`SELECT s.id, s.name, s.qname, s.kind, s.start_line, s.end_line, s.parent_id, f.path FROM symbols s JOIN files f ON f.id = s.file_id
88
+ WHERE s.file_id = ? AND s.kind = 'class' AND s.start_line <= ? AND s.end_line >= ? ORDER BY s.start_line DESC LIMIT 1`, fileId, line, line) ?? null;
89
+ }
90
+
91
+ /**
92
+ * Providers and injection points of the repository, keyed by what is asked for (a NestJS token, a
93
+ * Spring type): { providers: [{ key, token, exact, how, target, targetText, file, line }],
94
+ * injections: [{ key, token, exact, owner, param, type, file, line, from, why? }] }, `from`
95
+ * being the providers the point may receive (Spring: the beans chosen, see injection-spring.mjs).
96
+ */
97
+ export function collectInjection(intel) {
98
+ if (intel._injection?.version === intel.ix?.version) return intel._injection.data;
99
+ const providers = [], injections = [];
100
+ // test modules register mocks (`{ provide: Service, useValue: { get: jest.fn() } }`): not the app's wiring
101
+ const files = intel.store.all(`SELECT id, path FROM files WHERE lang IN ('typescript', 'tsx') AND is_test = 0`);
102
+ for (const f of files) {
103
+ const lines = intel.fileLines(f.path);
104
+ if (!lines) continue;
105
+ const text = lines.join('\n');
106
+ if (!text.includes('provide') && !text.includes('@Inject(') && !text.includes('ClientsModule')) continue;
107
+ lines.forEach((l, i) => {
108
+ const line = i + 1;
109
+ if (/^\s*(\/\/|\/?\*)/.test(l)) return;
110
+ const p = PROVIDE.exec(l);
111
+ // `provide: InjectionToken;` in an interface or type declares the shape, it provides nothing
112
+ if (p && !inTypeDeclaration(intel, f.id, line)) {
113
+ // the `use…` of this provider: on its line, or on the lines below before the next one
114
+ let u = USE.exec(l.slice(p.index)), at = line;
115
+ for (let j = i + 1; !u && j < Math.min(lines.length, i + 15) && !PROVIDE.test(lines[j]); j++) { u = USE.exec(lines[j]); at = j + 1; }
116
+ if (u) {
117
+ const tk = tokenKey(intel, f.id, line, p[1]);
118
+ const targetText = strip(u[2]) || `(${u[1] === 'Value' ? 'a value' : 'code'} on the lines below)`;
119
+ const name = /^(?:new\s+)?(?:[\w$]+\.)*([\w$]+)\b/.exec(targetText)?.[1];
120
+ let target = name && !/^(async|function)$/.test(name) && !/=>/.test(targetText.split('(')[0]) ? bound(intel, f.id, at, name) : null;
121
+ // `useClass: options.useClass` passes along what a caller gave: not a definition here
122
+ if (target && (target.kind === 'field' || target.kind === 'property')) target = null;
123
+ providers.push({ ...tk, how: `use${u[1]}`, target, targetText, file: f.path, line, holder: holderAt(intel, f.id, line) });
124
+ }
125
+ }
126
+ // a microservice client: `ClientsModule.register([{ name: MATH_SERVICE, transport: … }])`
127
+ const client = /^\s*name\s*:\s*([^,}\n]+?)\s*(?:,|}|$)/.exec(l) ?? /\{\s*name\s*:\s*([^,}\n]+?)\s*[,}]/.exec(l);
128
+ if (client && lines.slice(Math.max(0, i - 6), i + 1).some(x => /\bClientsModule\.register(Async)?\s*\(/.test(x))) {
129
+ providers.push({ ...tokenKey(intel, f.id, line, client[1]), how: 'ClientsModule', target: null, targetText: 'a client proxy for the microservice', file: f.path, line, holder: null });
130
+ }
131
+ for (const m of l.matchAll(INJECT)) {
132
+ if (!m[1]) continue;
133
+ const tk = tokenKey(intel, f.id, line, m[1]);
134
+ // the parameter or property it decorates: the rest of the line, or the next line
135
+ const rest = l.slice(m.index + m[0].length);
136
+ const pm = PARAM.exec(rest.trim() ? rest : (lines[i + 1] ?? ''));
137
+ injections.push({ ...tk, owner: classAt(intel, f.id, line), param: pm?.[1] ?? null, type: pm ? strip(pm[2]).replace(/;$/, '') : null, file: f.path, line });
138
+ }
139
+ });
140
+ }
141
+ // `@Inject(p.provide)`: the token of the provider declared as `p`
142
+ for (const j of injections) {
143
+ if (!j.key.startsWith('holder:')) continue;
144
+ const p = providers.find(x => x.holder === Number(j.key.slice(7)));
145
+ if (p) Object.assign(j, { key: p.key, token: `${j.token} (${p.token})` });
146
+ }
147
+ for (const j of injections) j.from = providers.filter(p => p.key === j.key);
148
+ const spring = collectSpring(intel);
149
+ const data = { providers: [...providers, ...spring.providers], injections: [...injections, ...spring.injections] };
150
+ intel._injection = { version: intel.ix?.version, data };
151
+ return data;
152
+ }
153
+
154
+ /** For a definition: the tokens it provides, each with where it is injected. */
155
+ export function providedBy(intel, id) {
156
+ const { providers, injections } = collectInjection(intel);
157
+ return providers.filter(p => p.target?.id === id).map(p => ({ ...p, into: injections.filter(j => j.from.includes(p)) }));
158
+ }
159
+
160
+ /** For a token or a type asked for by injection: what provides it and where it is injected. */
161
+ export function tokenUses(intel, id) {
162
+ const { providers, injections } = collectInjection(intel);
163
+ const key = `id:${id}`;
164
+ const js = injections.filter(j => j.key === key);
165
+ // a NestJS token's providers name it; a Spring type's are the beans its injection points receive
166
+ const ps = [...new Set([...providers.filter(p => p.key === key && !p.types), ...js.flatMap(j => j.from)])];
167
+ return { providers: ps, injections: js };
168
+ }
169
+
170
+ /** What a class receives by injection (`@Inject(TOKEN)`, Spring's injection points), each with its providers. */
171
+ export function injectedInto(intel, classId) {
172
+ return collectInjection(intel).injections.filter(j => j.owner?.id === classId);
173
+ }
@@ -173,9 +173,18 @@ export class CodeIntel {
173
173
  }
174
174
  const norm = t.replace(/::|#|->/g, '.');
175
175
  if (norm.includes('.')) {
176
- const name = norm.split('.').pop();
177
- const rows = this.store.all(`SELECT id, qname FROM symbols WHERE name = ?`, name).filter(r => r.qname === norm || r.qname.endsWith('.' + norm));
178
- if (rows.length) return { matches: this.#rank(rows.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'qualified' };
176
+ const qual = norm.split('.'), name = qual.pop();
177
+ const rows = this.store.all(`SELECT s.id, s.qname, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.name = ?`, name);
178
+ const exact = rows.filter(r => r.qname === norm || r.qname.endsWith('.' + norm));
179
+ if (exact.length) return { matches: this.#rank(exact.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'qualified' };
180
+ // `module.func`, `pkg.mod.func`, `ns::Type::method`, `Foo.prototype.bar`: the owner named
181
+ // last before the name is one of the definition's enclosing names, its file or a directory
182
+ const owner = qual[qual.length - 1];
183
+ const inModule = rows.filter(r => r.qname.split('.').slice(0, -1).includes(owner) || r.path.replace(/\.[^./]+$/, '').split('/').includes(owner));
184
+ if (inModule.length) return { matches: this.#rank(inModule.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'module' };
185
+ // the qualifier names nothing that declares `name`: answering with another owner's
186
+ // `name` would present the wrong definition's uses as the asked one's
187
+ if (rows.length) return { matches: [], how: 'qualifier-unknown', sameName: this.#rank(rows.map(r => r.id)).slice(0, limit).map(id => this.sym(id)) };
179
188
  }
180
189
  let rows = this.store.all('SELECT id FROM symbols WHERE name = ?', norm.split('.').pop());
181
190
  if (!rows.length) rows = this.store.all('SELECT id FROM symbols WHERE name_lc = ?', norm.split('.').pop().toLowerCase());
@@ -198,6 +207,32 @@ export class CodeIntel {
198
207
  return [...ids].sort((a, b) => score(b) - score(a) || a - b);
199
208
  }
200
209
 
210
+ /**
211
+ * Where `word` occurs in code the parser could not read (an ERROR region, give or take a
212
+ * line): a declaration or a use of it there may be missing from the index or bound wrongly.
213
+ * The first such line per file.
214
+ * @returns {{ path: string, line: number }[]}
215
+ */
216
+ filesWithParseErrors(word, { lang = null, max = 50 } = {}) {
217
+ if (!word || !/^[\w$]+$/.test(word)) return [];
218
+ const re = new RegExp(`(?<![\\w$])${escapeRegExp(word)}(?![\\w$])`);
219
+ const rows = this.store.all(`SELECT path, parse_error_lines FROM files WHERE parse_errors > 0${lang ? ' AND lang = ?' : ''} ORDER BY is_test, path LIMIT 5000`, ...(lang ? [lang] : []));
220
+ const out = [];
221
+ for (const r of rows) {
222
+ let regions;
223
+ try { regions = JSON.parse(r.parse_error_lines ?? '[]'); } catch { regions = []; }
224
+ const lines = regions.length ? this.fileLines(r.path) : null;
225
+ if (!lines) continue;
226
+ hit: for (const [a, b] of regions) {
227
+ for (let ln = Math.max(1, a - 1); ln <= Math.min(lines.length, b + 1); ln++) {
228
+ if (re.test(lines[ln - 1])) { out.push({ path: r.path, line: ln }); break hit; }
229
+ }
230
+ }
231
+ if (out.length >= max) break;
232
+ }
233
+ return out;
234
+ }
235
+
201
236
  members(id) {
202
237
  return this.store.all('SELECT id, name, qname, kind, start_line, end_line, sig, visibility FROM symbols WHERE parent_id = ? ORDER BY start_line', id);
203
238
  }
@@ -235,12 +270,14 @@ export class CodeIntel {
235
270
 
236
271
  /**
237
272
  * The subtypes of a member's type, transitively, split into those that inherit the member — they
238
- * run it as their own, so a change to it reaches them — and those that redeclare it (their own
239
- * version runs; their subtypes are not followed).
240
- * @returns {{ inherit: object[], override: object[] }} index rows of the subtypes
273
+ * run it as their own, so a change to it reaches them — those that extend it (they redeclare it,
274
+ * but their version uses this one: `TRANSFORMS = {**Base.TRANSFORMS, …}`, `super().m()`; a change
275
+ * still reaches them, and their subtypes that inherit their version), and those that override it
276
+ * (their own version runs without this one).
277
+ * @returns {{ inherit: object[], extend: object[], override: object[] }} index rows of the subtypes
241
278
  */
242
279
  inheritance(id, { max = 30 } = {}) {
243
- const out = { inherit: [], override: [] };
280
+ const out = { inherit: [], extend: [], override: [] };
244
281
  const sym = this.sym(id);
245
282
  if (!sym) return out;
246
283
  const type = this.ix.resolver.enclosingType(sym.id);
@@ -263,20 +300,30 @@ export class CodeIntel {
263
300
  frontier = next;
264
301
  }
265
302
  const declares = new Map();
266
- const declared = (tid) => { if (!declares.has(tid)) declares.set(tid, this.#membersNamed(tid, sym.name).length > 0); return declares.get(tid); };
303
+ const declared = (tid) => { if (!declares.has(tid)) declares.set(tid, this.#membersNamed(tid, sym.name)); return declares.get(tid).length > 0; };
304
+ // a redeclaration that reads the member of a type after it in the resolution order uses that version
305
+ const usesNext = (tid, rest) => {
306
+ const later = new Set(rest.flatMap(t => declared(t) ? declares.get(t) : []));
307
+ return declares.get(tid).some(id => this.store.all('SELECT dst_id FROM refs WHERE src_id = ? AND dst_id IS NOT NULL', id).some(r => later.has(r.dst_id)));
308
+ };
267
309
  for (const sid of subs) {
268
310
  const order = this.ix.resolver.mro(sid);
269
311
  if (!order.includes(type.id)) continue;
270
- const owner = order.find(declared);
312
+ const declarers = order.filter(declared);
313
+ const owner = declarers[0];
314
+ // follow the redeclarations that use the next version down to this one
315
+ let k = 0;
316
+ while (declarers[k] !== type.id && k + 1 < declarers.length && usesNext(declarers[k], order.slice(order.indexOf(declarers[k]) + 1))) k++;
271
317
  let bucket = null;
272
318
  if (owner === type.id) bucket = out.inherit;
319
+ else if (declarers[k] === type.id) bucket = out.extend;
273
320
  // its own version, where it would otherwise run this one
274
- else if (owner === sid && order.slice(1).find(declared) === type.id) bucket = out.override;
321
+ else if (owner === sid && declarers[1] === type.id) bucket = out.override;
275
322
  if (!bucket) continue;
276
323
  const st = this.sym(sid);
277
324
  if (!st) continue;
278
325
  bucket.push(st);
279
- if (out.inherit.length + out.override.length >= max) return out;
326
+ if (out.inherit.length + out.extend.length + out.override.length >= max) return out;
280
327
  }
281
328
  return out;
282
329
  }
@@ -308,8 +355,23 @@ export class CodeIntel {
308
355
  overloads(sym) {
309
356
  // where each overload is a method of its own (Java, C#, Kotlin…), calls are bound to the one they reach
310
357
  if (this.ix.resolver.specs[sym.lang]?.distinctOverloads) return [];
311
- return this.store.all('SELECT id FROM symbols WHERE file_id = ? AND qname = ? AND kind = ? AND (parent_id IS ? OR parent_id = ?) AND is_static = ?',
312
- sym.file_id, sym.qname, sym.kind, sym.parent_id, sym.parent_id, sym.is_static ? 1 : 0).map(r => r.id);
358
+ // methods of one name from impls of different traits or types (Rust `Debug::fmt` / `Display::fmt`,
359
+ // `impl AsyncWrite for Cursor<Vec<u8>>` / `… for Cursor<&mut [u8]>`) are not redeclarations
360
+ return this.store.all('SELECT id FROM symbols WHERE file_id = ? AND qname = ? AND kind = ? AND (parent_id IS ? OR parent_id = ?) AND is_static = ? AND (kind != \'method\' OR (bases IS ? AND (? != \'rust\' OR decorators IS ?)))',
361
+ sym.file_id, sym.qname, sym.kind, sym.parent_id, sym.parent_id, sym.is_static ? 1 : 0, sym.bases ?? null, sym.lang, sym.decorators ?? null).map(r => r.id);
362
+ }
363
+
364
+ /**
365
+ * A Rust trait method's implementations (`fn m` in each `impl Trait for X`): calls of them are
366
+ * calls of the trait's method, as rust-analyzer counts them, so they are its references.
367
+ */
368
+ #traitImpls(sym) {
369
+ if (sym.lang !== 'rust' || sym.kind !== 'method' || sym.parent_id == null) return [];
370
+ const trait = this.sym(sym.parent_id);
371
+ if (trait?.kind !== 'trait') return [];
372
+ const r = this.ix.resolver;
373
+ return this.store.all("SELECT s.id, s.file_id FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.name = ? AND s.kind = 'method' AND f.lang = 'rust' AND s.bases LIKE ?", sym.name, `%"${trait.name}"%`)
374
+ .filter(m => m.id !== sym.id && r.resolveTypeName(trait.name, m.file_id)?.id === trait.id).map(m => m.id);
313
375
  }
314
376
 
315
377
  /** Is `memberId` one of the members `sym` overrides (declared in a supertype)? */
@@ -391,7 +453,7 @@ export class CodeIntel {
391
453
  references(id, { kinds = null, includeTests = true, minConf = 0, family = true } = {}) {
392
454
  const sym = this.sym(id);
393
455
  if (!sym) return null;
394
- const ids = [...new Set([id, ...this.overloads(sym)])];
456
+ const ids = [...new Set([id, ...this.overloads(sym), ...this.#traitImpls(sym)])];
395
457
  const via = new Map();
396
458
  if (family && !sym.is_static && (sym.kind === 'method' || sym.kind === 'property' || sym.kind === 'field')) {
397
459
  for (const s of this.#supertypeMembers(sym)) if (!ids.includes(s.id)) { ids.push(s.id); via.set(s.id, s.via); }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Where an issue points in the code. An issue names things in code spans, examples and tracebacks
3
+ * (`DATE_DIFF`, `rich_club_coefficient(normalized=True)`, `exp.Agg`); the code to change is where
4
+ * several of those names meet. Each name is matched against identifiers by their words, so
5
+ * `DATE_DIFF`, `DateDiff` and `_date_diff_sql` are one term, and each definition scores the
6
+ * rarity (inverse document frequency) of the distinct terms its lines carry, plus the terms its
7
+ * own name or its file's path is made of. Library code only: tests are left out.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ // words of examples that name the language, not the code under it
13
+ const NOISE = new Set(('true false none null undefined self cls this def class return import from print len str int float bool list dict set tuple range '
14
+ + 'isinstance assert lambda and or not in is if else elif for while with as pass raise try except finally yield async await new let const var '
15
+ + 'function public private static void string object type repr').split(' '));
16
+ const IDENT = /[A-Za-z_][A-Za-z0-9_]*/g;
17
+ const WORD = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
18
+ const MAX_FILE_BYTES = 1_000_000;
19
+ // examples, benchmarks and docs are not where an issue gets fixed
20
+ const ASIDE_PATH = /(^|\/)(examples?|samples?|demos?|sandbox|playground|docs?(_src|_source|_examples?)?|documentation|benchmarks?|bench|scripts?|fixtures?|testdata|tutorials?|website|\.github)\//i;
21
+ // measured on the dev split of bench/agentic/localize.mjs: mild length normalisation (BM25's b),
22
+ // the title's terms four times an example's, tables and lists of names at 0.6 of a definition
23
+ const B = 0.2;
24
+ const TITLE_W = 4;
25
+ const VALUE_W = 0.6;
26
+ const VALUE_KINDS = new Set(['field', 'property', 'variable', 'constant']);
27
+
28
+ const partsCache = new Map();
29
+ function parts(id) {
30
+ let p = partsCache.get(id);
31
+ if (!p) {
32
+ p = (id.match(WORD) ?? []).map(w => w.toLowerCase());
33
+ if (partsCache.size > 200_000) partsCache.clear();
34
+ partsCache.set(id, p);
35
+ }
36
+ return p;
37
+ }
38
+
39
+ /** Whether an issue reads like one: long enough, or carrying code. */
40
+ export function isIssueText(text) {
41
+ const t = String(text ?? '');
42
+ return t.length >= 240 || /```/.test(t) || (t.match(/`[^`\n]+`/g) ?? []).length >= 3;
43
+ }
44
+
45
+ /** The code-like names an issue uses: from its code spans and blocks, and identifier-shaped words of its prose. */
46
+ export function issueTerms(text) {
47
+ const t = String(text ?? '');
48
+ const code = [];
49
+ // a fence's language tag (```python) is not part of the code
50
+ const prose = t.replace(/```[^\n]*\n([\s\S]*?)```/g, (_, m) => { code.push(m); return ' '; })
51
+ .replace(/`([^`\n]+)`/g, (_, m) => { code.push(m); return ' '; });
52
+ // the title names what the issue is about; examples name everything they touch
53
+ const title = new Set((t.trim().split('\n')[0].match(IDENT) ?? []).map(x => parts(x).join('_')));
54
+ const ids = [];
55
+ for (const c of code) for (const m of c.matchAll(IDENT)) ids.push(m[0]);
56
+ // prose words that can only be code: snake_case, camelCase, dotted paths
57
+ for (const m of prose.matchAll(/[A-Za-z_][\w]*(?:\.[A-Za-z_]\w*)+|\b[a-z]+_[a-z_\d]+\b|\b[a-z]+[A-Z]\w*\b|\b[A-Z][a-z]+[A-Z]\w*\b/g)) for (const x of m[0].split('.')) ids.push(x);
58
+ const terms = new Map();
59
+ for (const id of ids) {
60
+ const p = parts(id);
61
+ if (!p.length || NOISE.has(id.toLowerCase())) continue;
62
+ if (p.length === 1 && p[0].length < 3) continue;
63
+ if (p.every(w => /^\d+$/.test(w))) continue;
64
+ const key = p.join('_');
65
+ if (!terms.has(key)) terms.set(key, { key, parts: p, forms: new Set(), title: title.has(key) });
66
+ terms.get(key).forms.add(id);
67
+ }
68
+ return [...terms.values()];
69
+ }
70
+
71
+ /**
72
+ * Definitions ranked by the issue's terms they carry. Returns [{ id, score, terms }] (best first).
73
+ * @param {import('./intel.mjs').CodeIntel} intel
74
+ */
75
+ export function issueSeeds(intel, text, { limit = 12, path: prefix = null } = {}) {
76
+ const terms = issueTerms(text);
77
+ if (!terms.length) return [];
78
+ const byFirst = new Map();
79
+ for (const t of terms) { if (!byFirst.has(t.parts[0])) byFirst.set(t.parts[0], []); byFirst.get(t.parts[0]).push(t); }
80
+ const files = intel.store.all('SELECT id, path, size FROM files WHERE is_test = 0');
81
+ const hits = new Map(); // symbol id -> Set(term key)
82
+ const owned = new Map(); // symbol id -> lines it owns (its own, not its members')
83
+ const df = new Map(); // term key -> number of definitions carrying it
84
+ let total = 0;
85
+ for (const f of files) {
86
+ if (prefix && !f.path.startsWith(prefix)) continue;
87
+ if (f.size > MAX_FILE_BYTES) continue;
88
+ if (ASIDE_PATH.test(f.path) && !(prefix && ASIDE_PATH.test(prefix))) continue;
89
+ const syms = intel.store.all('SELECT id, start_line, end_line FROM symbols WHERE file_id = ? ORDER BY start_line', f.id);
90
+ total += syms.length;
91
+ if (!syms.length) continue;
92
+ let text;
93
+ try { text = fs.readFileSync(path.join(intel.root, f.path), 'utf8'); } catch { continue; }
94
+ const lines = text.split('\n');
95
+ // innermost definition of each line
96
+ const owner = new Int32Array(lines.length + 2).fill(-1);
97
+ const span = new Int32Array(lines.length + 2).fill(1 << 30);
98
+ for (const s of syms) {
99
+ const w = s.end_line - s.start_line;
100
+ for (let l = s.start_line; l <= Math.min(s.end_line, lines.length); l++) if (w < span[l]) { span[l] = w; owner[l] = s.id; }
101
+ }
102
+ for (let i = 0; i < lines.length; i++) {
103
+ const o = owner[i + 1];
104
+ if (o < 0) continue;
105
+ owned.set(o, (owned.get(o) ?? 0) + 1);
106
+ for (const m of lines[i].matchAll(IDENT)) {
107
+ const p = parts(m[0]);
108
+ for (let k = 0; k < p.length; k++) {
109
+ const cands = byFirst.get(p[k]);
110
+ if (!cands) continue;
111
+ for (const t of cands) {
112
+ if (k + t.parts.length > p.length) continue;
113
+ let ok = true;
114
+ for (let j = 1; j < t.parts.length; j++) if (p[k + j] !== t.parts[j]) { ok = false; break; }
115
+ if (!ok) continue;
116
+ let set = hits.get(o);
117
+ if (!set) hits.set(o, (set = new Set()));
118
+ if (!set.has(t.key)) { set.add(t.key); df.set(t.key, (df.get(t.key) ?? 0) + 1); }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (!hits.size) return [];
125
+ const idf = (k) => Math.log(1 + total / (df.get(k) ?? total));
126
+ const rows = new Map();
127
+ const ids = [...hits.keys()];
128
+ for (let i = 0; i < ids.length; i += 500) {
129
+ const chunk = ids.slice(i, i + 500);
130
+ for (const r of intel.store.all(`SELECT s.id, s.name, s.kind, s.start_line, s.end_line, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id IN (${chunk.map(() => '?').join(',')})`, ...chunk)) rows.set(r.id, r);
131
+ }
132
+ // longer definitions carry more terms by chance: BM25's length normalisation, over owned lines
133
+ let sumLen = 0;
134
+ for (const v of owned.values()) sumLen += v;
135
+ const avgLen = sumLen / Math.max(1, owned.size);
136
+ const byKey = new Map(terms.map(t => [t.key, t]));
137
+ const scored = [];
138
+ for (const [id, set] of hits) {
139
+ const r = rows.get(id);
140
+ if (!r) continue;
141
+ let score = 0;
142
+ for (const k of set) score += idf(k) * (byKey.get(k)?.title ? TITLE_W : 1);
143
+ // its own name is a term the issue uses: the issue names this definition
144
+ const own = parts(r.name).join('_');
145
+ if (set.has(own)) score += idf(own);
146
+ // its file's path is made of a term (a dialect, a format, a module named in the issue)
147
+ const pathWords = new Set(r.path.split(/[/._-]+/).flatMap(parts));
148
+ for (const t of terms) if (t.parts.length === 1 && pathWords.has(t.parts[0]) && !set.has(t.key)) score += 0.5 * idf(t.key);
149
+ score /= 1 - B + B * ((owned.get(id) ?? 1) / avgLen);
150
+ // a table or list of names (`__all__`, a function map) enumerates terms rather than combining them
151
+ if (VALUE_KINDS.has(r.kind)) score *= VALUE_W;
152
+ scored.push({ id, score, terms: [...set] });
153
+ }
154
+ scored.sort((a, b) => b.score - a.score || a.id - b.id);
155
+ return scored.slice(0, limit);
156
+ }