graph-indexer 3.0.0 → 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 (60) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/README.md +72 -15
  3. package/SECURITY.md +21 -4
  4. package/bin/graph-indexer.mjs +46 -5
  5. package/integrations/claude-code/.claude-plugin/plugin.json +3 -2
  6. package/integrations/claude-code/agents/code-structure.md +0 -3
  7. package/integrations/claude-code/skills/setup/SKILL.md +26 -0
  8. package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
  9. package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
  10. package/integrations/claude-code-local/README.md +10 -0
  11. package/integrations/claude-code-local/agents/code-structure.md +22 -0
  12. package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +12 -6
  13. package/integrations/opencode/graph-indexer.js +13 -8
  14. package/package.json +2 -2
  15. package/src/cli/helper.mjs +15 -1
  16. package/src/cli/hook.mjs +140 -48
  17. package/src/cli/init.mjs +49 -17
  18. package/src/cli/resident.mjs +15 -5
  19. package/src/index/discover.mjs +3 -5
  20. package/src/index/indexer.mjs +77 -41
  21. package/src/index/modules.mjs +82 -7
  22. package/src/index/parse-worker.mjs +12 -0
  23. package/src/index/parsepool.mjs +73 -0
  24. package/src/index/resolver.mjs +371 -25
  25. package/src/mcp/server.mjs +16 -12
  26. package/src/mcp/tools.mjs +419 -26
  27. package/src/parse/arity.mjs +41 -3
  28. package/src/parse/extract.mjs +277 -46
  29. package/src/parse/lang/ecma.mjs +2 -0
  30. package/src/parse/lang/go.mjs +2 -0
  31. package/src/parse/lang/jvm.mjs +33 -0
  32. package/src/parse/lang/markdown.mjs +92 -0
  33. package/src/parse/lang/python.mjs +43 -0
  34. package/src/parse/lang/rust.mjs +215 -15
  35. package/src/parse/lang/sfc.mjs +125 -0
  36. package/src/parse/languages.mjs +9 -2
  37. package/src/query/check.mjs +55 -17
  38. package/src/query/facts.mjs +3 -2
  39. package/src/query/injection-spring.mjs +405 -0
  40. package/src/query/injection.mjs +173 -0
  41. package/src/query/intel.mjs +87 -22
  42. package/src/query/issue.mjs +156 -0
  43. package/src/query/orm.mjs +286 -0
  44. package/src/query/read.mjs +6 -1
  45. package/src/query/report.mjs +53 -0
  46. package/src/query/routes.mjs +142 -0
  47. package/src/query/showcase.mjs +59 -0
  48. package/src/query/testcmd.mjs +25 -14
  49. package/src/query/textsearch.mjs +29 -11
  50. package/src/search/search.mjs +85 -17
  51. package/src/search/semantic.mjs +201 -0
  52. package/src/search/vectors.mjs +122 -0
  53. package/src/store/db.mjs +22 -4
  54. package/src/util/git.mjs +30 -0
  55. package/src/util/paths.mjs +40 -1
  56. package/src/util/regex.mjs +2 -0
  57. package/vendor/grammars/README.md +47 -0
  58. package/vendor/grammars/kotlin.wasm.gz +0 -0
  59. package/vendor/grammars/manifest.json +4 -4
  60. /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
+ }
@@ -7,15 +7,16 @@
7
7
  */
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
- import { spawnSync } from 'node:child_process';
11
10
  import { Store } from '../store/db.mjs';
12
11
  import { Indexer } from '../index/indexer.mjs';
13
12
  import { SearchEngine } from '../search/search.mjs';
14
13
  import { computeCentrality, loadGraph } from '../index/graph.mjs';
15
14
  import { confidenceLabel } from '../index/resolver.mjs';
16
- import { dataDir } from '../util/paths.mjs';
15
+ import { dataDir, withinRoot, isRevision } from '../util/paths.mjs';
17
16
  import { isConstructor } from '../parse/languages.mjs';
18
17
  import { sigArity } from '../parse/arity.mjs';
18
+ import { escapeRegExp } from '../util/regex.mjs';
19
+ import { gitSync } from '../util/git.mjs';
19
20
 
20
21
  const TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'type', 'object', 'module', 'impl']);
21
22
  const CALLABLE = new Set(['function', 'method', 'constructor', 'macro']);
@@ -128,6 +129,7 @@ export class CodeIntel {
128
129
 
129
130
  // ── source access ────────────────────────────────────────────────────────────
130
131
  fileLines(rel) {
132
+ if (!withinRoot(this.root, rel)) return null;
131
133
  const abs = path.join(this.root, rel);
132
134
  let st;
133
135
  try { st = fs.statSync(abs); } catch { return null; }
@@ -162,7 +164,7 @@ export class CodeIntel {
162
164
  ORDER BY (s.end_line - s.start_line) ASC LIMIT 1`, file.replace(/^\.\//, ''), Number(line), Number(line));
163
165
  return { matches: rows.map(r => this.sym(r.id)), how: 'location' };
164
166
  }
165
- m = /^(.+?\.[A-Za-z0-9]+)[:#](.+)$/.exec(t);
167
+ m = /^([^:#]+?\.[A-Za-z0-9]+)[:#](.+)$/.exec(t);
166
168
  if (m) {
167
169
  const [, file, q] = m;
168
170
  const rows = this.store.all(`SELECT s.id FROM symbols s JOIN files f ON f.id = s.file_id WHERE f.path = ? AND (s.qname = ? OR s.name = ? OR s.qname LIKE ?) ORDER BY s.qname = ? DESC, s.start_line LIMIT ?`,
@@ -171,9 +173,18 @@ export class CodeIntel {
171
173
  }
172
174
  const norm = t.replace(/::|#|->/g, '.');
173
175
  if (norm.includes('.')) {
174
- const name = norm.split('.').pop();
175
- const rows = this.store.all(`SELECT id, qname FROM symbols WHERE name = ?`, name).filter(r => r.qname === norm || r.qname.endsWith('.' + norm));
176
- 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)) };
177
188
  }
178
189
  let rows = this.store.all('SELECT id FROM symbols WHERE name = ?', norm.split('.').pop());
179
190
  if (!rows.length) rows = this.store.all('SELECT id FROM symbols WHERE name_lc = ?', norm.split('.').pop().toLowerCase());
@@ -196,6 +207,32 @@ export class CodeIntel {
196
207
  return [...ids].sort((a, b) => score(b) - score(a) || a - b);
197
208
  }
198
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
+
199
236
  members(id) {
200
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);
201
238
  }
@@ -233,12 +270,14 @@ export class CodeIntel {
233
270
 
234
271
  /**
235
272
  * The subtypes of a member's type, transitively, split into those that inherit the member — they
236
- * run it as their own, so a change to it reaches them — and those that redeclare it (their own
237
- * version runs; their subtypes are not followed).
238
- * @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
239
278
  */
240
279
  inheritance(id, { max = 30 } = {}) {
241
- const out = { inherit: [], override: [] };
280
+ const out = { inherit: [], extend: [], override: [] };
242
281
  const sym = this.sym(id);
243
282
  if (!sym) return out;
244
283
  const type = this.ix.resolver.enclosingType(sym.id);
@@ -261,20 +300,30 @@ export class CodeIntel {
261
300
  frontier = next;
262
301
  }
263
302
  const declares = new Map();
264
- 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
+ };
265
309
  for (const sid of subs) {
266
310
  const order = this.ix.resolver.mro(sid);
267
311
  if (!order.includes(type.id)) continue;
268
- 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++;
269
317
  let bucket = null;
270
318
  if (owner === type.id) bucket = out.inherit;
319
+ else if (declarers[k] === type.id) bucket = out.extend;
271
320
  // its own version, where it would otherwise run this one
272
- 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;
273
322
  if (!bucket) continue;
274
323
  const st = this.sym(sid);
275
324
  if (!st) continue;
276
325
  bucket.push(st);
277
- if (out.inherit.length + out.override.length >= max) return out;
326
+ if (out.inherit.length + out.extend.length + out.override.length >= max) return out;
278
327
  }
279
328
  return out;
280
329
  }
@@ -306,8 +355,23 @@ export class CodeIntel {
306
355
  overloads(sym) {
307
356
  // where each overload is a method of its own (Java, C#, Kotlin…), calls are bound to the one they reach
308
357
  if (this.ix.resolver.specs[sym.lang]?.distinctOverloads) return [];
309
- 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 = ?',
310
- 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);
311
375
  }
312
376
 
313
377
  /** Is `memberId` one of the members `sym` overrides (declared in a supertype)? */
@@ -367,7 +431,7 @@ export class CodeIntel {
367
431
  const ids = [...family];
368
432
  for (const { path: p } of this.store.all(`SELECT DISTINCT f.path FROM refs r JOIN files f ON f.id = r.file_id WHERE r.dst_id IN (${ids.map(() => '?').join(',')})`, ...ids)) files.add(p);
369
433
  } else files.add(sym.path);
370
- const name = sym.name.replace(/[$]/g, '\\$');
434
+ const name = escapeRegExp(sym.name);
371
435
  const lit = new RegExp(`(['"\`])(?:[\\w.]*\\.)?${name}\\1`);
372
436
  const title = /^\s*(?:describe|it|test|context|suite)(?:\.\w+)?\s*\(/;
373
437
  const out = [];
@@ -389,7 +453,7 @@ export class CodeIntel {
389
453
  references(id, { kinds = null, includeTests = true, minConf = 0, family = true } = {}) {
390
454
  const sym = this.sym(id);
391
455
  if (!sym) return null;
392
- const ids = [...new Set([id, ...this.overloads(sym)])];
456
+ const ids = [...new Set([id, ...this.overloads(sym), ...this.#traitImpls(sym)])];
393
457
  const via = new Map();
394
458
  if (family && !sym.is_static && (sym.kind === 'method' || sym.kind === 'property' || sym.kind === 'field')) {
395
459
  for (const s of this.#supertypeMembers(sym)) if (!ids.includes(s.id)) { ids.push(s.id); via.set(s.id, s.via); }
@@ -451,7 +515,7 @@ export class CodeIntel {
451
515
  return { total: rows.length, plausible, typeNames: [] };
452
516
  }
453
517
  const names = [...typeNames];
454
- const wordRe = new RegExp(`\\b(${names.map(n => n.replace(/[$]/g, '\\$')).join('|')})\\b`);
518
+ const wordRe = new RegExp(`\\b(${names.map(n => escapeRegExp(n)).join('|')})\\b`);
455
519
  const lower = names.map(n => n.toLowerCase());
456
520
  const mentions = new Map();
457
521
  const plausible = rows.filter(r => {
@@ -575,7 +639,8 @@ export class CodeIntel {
575
639
  // ── impact ───────────────────────────────────────────────────────────────────
576
640
  /** Symbols overlapping the working-tree diff (git), as impact seeds. */
577
641
  diffSeeds(base = 'HEAD') {
578
- const r = spawnSync('git', ['diff', '-U0', '--no-color', base, '--'], { cwd: this.root, encoding: 'utf8', maxBuffer: 1 << 28 });
642
+ if (!isRevision(base)) return { error: `Not a git revision: ${base}`, seeds: [], files: [] };
643
+ const r = gitSync(this.root, ['diff', '-U0', '--no-color', base, '--'], { maxBuffer: 1 << 28 });
579
644
  if (r.status !== 0) return { error: (r.stderr || 'git diff failed').trim(), seeds: [], files: [] };
580
645
  const changes = new Map();
581
646
  let file = null;
@@ -587,7 +652,7 @@ export class CodeIntel {
587
652
  (changes.get(file) ?? changes.set(file, []).get(file)).push([start, Math.max(start, start + len - 1)]);
588
653
  }
589
654
  }
590
- const untracked = spawnSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd: this.root, encoding: 'utf8' });
655
+ const untracked = gitSync(this.root, ['ls-files', '--others', '--exclude-standard']);
591
656
  for (const f of (untracked.stdout || '').split('\n').filter(Boolean)) if (!changes.has(f)) changes.set(f, [[1, 1e9]]);
592
657
  const seeds = new Set();
593
658
  for (const [f, ranges] of changes) {
@@ -604,7 +669,7 @@ export class CodeIntel {
604
669
  /** Files that historically change together with the given files (git log, bounded). */
605
670
  coChange(files, { commits = 400, limit = 8 } = {}) {
606
671
  if (!this._cochange || this._cochangeAt < Date.now() - 600_000) {
607
- const r = spawnSync('git', ['log', `-n${commits}`, '--name-only', '--pretty=format:%x00'], { cwd: this.root, encoding: 'utf8', maxBuffer: 1 << 28 });
672
+ const r = gitSync(this.root, ['log', `-n${commits}`, '--name-only', '--pretty=format:%x00'], { maxBuffer: 1 << 28 });
608
673
  const counts = new Map(); const pair = new Map();
609
674
  if (r.status === 0) {
610
675
  for (const block of r.stdout.split('\0')) {
@@ -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
+ }