graph-indexer 2.1.0 → 3.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 (129) hide show
  1. package/CHANGELOG.md +260 -1
  2. package/README.md +232 -312
  3. package/SECURITY.md +37 -59
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
  6. package/integrations/claude-code/.mcp.json +8 -0
  7. package/integrations/claude-code/agents/code-structure.md +22 -0
  8. package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +124 -0
  11. package/package.json +31 -105
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +356 -0
  14. package/src/cli/init.mjs +255 -0
  15. package/src/cli/resident.mjs +175 -0
  16. package/src/index/discover.mjs +130 -0
  17. package/src/index/fingerprint.mjs +30 -0
  18. package/src/index/graph.mjs +61 -0
  19. package/src/index/indexer.mjs +476 -0
  20. package/src/index/modules.mjs +331 -0
  21. package/src/index/resolver.mjs +954 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +973 -0
  25. package/src/parse/arity.mjs +152 -0
  26. package/src/parse/extract.mjs +1031 -0
  27. package/src/parse/lang/csharp.mjs +106 -0
  28. package/src/parse/lang/ecma.mjs +438 -0
  29. package/src/parse/lang/go.mjs +194 -0
  30. package/src/parse/lang/jvm.mjs +118 -0
  31. package/src/parse/lang/kotlin.mjs +97 -0
  32. package/src/parse/lang/misc.mjs +115 -0
  33. package/src/parse/lang/native.mjs +126 -0
  34. package/src/parse/lang/python.mjs +153 -0
  35. package/src/parse/lang/rust.mjs +157 -0
  36. package/src/parse/lang/scripting.mjs +219 -0
  37. package/src/parse/languages.mjs +48 -0
  38. package/src/parse/runtime.mjs +56 -0
  39. package/src/query/check.mjs +343 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +680 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +119 -0
  44. package/src/query/textsearch.mjs +160 -0
  45. package/src/search/search.mjs +210 -0
  46. package/src/search/tokenize.mjs +188 -0
  47. package/src/store/db.mjs +169 -0
  48. package/src/util/paths.mjs +24 -0
  49. package/vendor/grammars/bash.wasm.gz +0 -0
  50. package/vendor/grammars/c.wasm.gz +0 -0
  51. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  52. package/vendor/grammars/cpp.wasm.gz +0 -0
  53. package/vendor/grammars/css.wasm.gz +0 -0
  54. package/vendor/grammars/go.wasm.gz +0 -0
  55. package/vendor/grammars/java.wasm.gz +0 -0
  56. package/vendor/grammars/javascript.wasm.gz +0 -0
  57. package/vendor/grammars/kotlin.wasm.gz +0 -0
  58. package/vendor/grammars/manifest.json +148 -0
  59. package/vendor/grammars/php.wasm.gz +0 -0
  60. package/vendor/grammars/python.wasm.gz +0 -0
  61. package/vendor/grammars/ruby.wasm.gz +0 -0
  62. package/vendor/grammars/rust.wasm.gz +0 -0
  63. package/vendor/grammars/scala.wasm.gz +0 -0
  64. package/vendor/grammars/tsx.wasm.gz +0 -0
  65. package/vendor/grammars/typescript.wasm.gz +0 -0
  66. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  67. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  68. package/cli-ui.mjs +0 -78
  69. package/colbert.mjs +0 -168
  70. package/config.mjs +0 -494
  71. package/daemon-ctl.mjs +0 -175
  72. package/daemon-lock.mjs +0 -69
  73. package/embedders/python/mlx_embed_server.py +0 -61
  74. package/embedders/python/requirements-mlx.txt +0 -5
  75. package/embedders/setup-mlx.mjs +0 -110
  76. package/embeddings.mjs +0 -571
  77. package/engine/binary.mjs +0 -437
  78. package/engine/memory.mjs +0 -1003
  79. package/engine/sqlite.mjs +0 -1136
  80. package/enrichment.mjs +0 -521
  81. package/git-signals.mjs +0 -248
  82. package/indexer.mjs +0 -546
  83. package/init.mjs +0 -1809
  84. package/layout.mjs +0 -123
  85. package/mcp/centrality.mjs +0 -117
  86. package/mcp/format.mjs +0 -61
  87. package/mcp/resolver.mjs +0 -116
  88. package/mcp/symbolgraph.mjs +0 -137
  89. package/mcp/taint.mjs +0 -225
  90. package/mcp/tools.mjs +0 -1582
  91. package/mcp/topology.mjs +0 -676
  92. package/mcp-server.mjs +0 -197
  93. package/parse/extractor.mjs +0 -628
  94. package/parse/imports.mjs +0 -365
  95. package/parse/interprocedural.mjs +0 -134
  96. package/parse/languages.mjs +0 -253
  97. package/parse/metadata.mjs +0 -701
  98. package/parse/routes.mjs +0 -327
  99. package/parse/scip.mjs +0 -391
  100. package/parse/taint-patterns.mjs +0 -119
  101. package/prompts/CORE.md +0 -91
  102. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  103. package/prompts/INTEGRATION.md +0 -123
  104. package/prompts/frameworks/ANDROID.md +0 -17
  105. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  106. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  107. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  108. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  109. package/prompts/frameworks/RAILS.md +0 -20
  110. package/prompts/frameworks/REACT.md +0 -20
  111. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  112. package/prompts/languages/BASH.md +0 -22
  113. package/prompts/languages/C.md +0 -21
  114. package/prompts/languages/CSHARP.md +0 -24
  115. package/prompts/languages/CSS_SCSS.md +0 -22
  116. package/prompts/languages/GO.md +0 -24
  117. package/prompts/languages/JAVA.md +0 -23
  118. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  119. package/prompts/languages/KOTLIN.md +0 -24
  120. package/prompts/languages/PHP.md +0 -23
  121. package/prompts/languages/PYTHON.md +0 -23
  122. package/prompts/languages/RUBY.md +0 -23
  123. package/prompts/languages/RUST.md +0 -21
  124. package/prompts/languages/SWIFT.md +0 -21
  125. package/seal.mjs +0 -400
  126. package/search-core.mjs +0 -777
  127. package/search-sparse.mjs +0 -183
  128. package/storage.mjs +0 -73
  129. package/watch-daemon.mjs +0 -289
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Edit checker: what an agent's uncommitted changes broke or left behind, found without building
3
+ * the project. Compares every changed file with its version at the base commit (HEAD by default)
4
+ * and reports, with locations:
5
+ *
6
+ * syntax parse errors that are new since the base version
7
+ * arity calls whose argument count no longer fits the (changed) definition — call sites
8
+ * of changed signatures anywhere, and calls written in the changed lines
9
+ * removed definitions that were deleted or renamed but are still used or imported
10
+ * untouched callers of a changed signature in files the change did not touch (to review)
11
+ * inherited subclasses that inherit a changed member (they run the new code) or override it
12
+ * tests the test files that exercise the changed code, and the command to run them
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { specForPath, isConstructor } from '../parse/languages.mjs';
18
+ import { prepareLanguage, extractFile } from '../parse/extract.mjs';
19
+ import { paramArity, callAt, callArgc, fits, describeArity } from '../parse/arity.mjs';
20
+ import { testCommands, testCaseCommands } from './testcmd.mjs';
21
+
22
+ const CALLABLE = new Set(['function', 'method', 'constructor']);
23
+ const MEMBER_KINDS = new Set(['class', 'interface', 'struct', 'trait', 'enum', 'object', 'impl', 'module', 'type']);
24
+
25
+ function git(root, args) {
26
+ const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 1 << 28 });
27
+ return r.status === 0 ? r.stdout : null;
28
+ }
29
+
30
+ /** Changed files vs base with their changed (new-side) line ranges; untracked files count whole. */
31
+ function changedFiles(root, base) {
32
+ const out = new Map();
33
+ const diff = git(root, ['diff', '-U0', '--no-color', '--no-renames', base, '--']);
34
+ if (diff === null) return null;
35
+ let file = null, deleted = false;
36
+ for (const line of diff.split('\n')) {
37
+ if (line.startsWith('--- ')) { deleted = false; continue; }
38
+ if (line.startsWith('+++ ')) {
39
+ const p = line.slice(4);
40
+ if (p === '/dev/null') { deleted = true; continue; }
41
+ file = p.replace(/^b\//, '');
42
+ if (!out.has(file)) out.set(file, { ranges: [], status: 'M' });
43
+ continue;
44
+ }
45
+ const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
46
+ if (h && file && !deleted) {
47
+ const start = Number(h[1]), len = h[2] === undefined ? 1 : Number(h[2]);
48
+ if (len > 0) out.get(file).ranges.push([start, start + len - 1]);
49
+ }
50
+ }
51
+ for (const l of (git(root, ['diff', '--name-status', '--no-renames', base, '--']) ?? '').split('\n')) {
52
+ const [st, p] = l.split('\t');
53
+ if (!p) continue;
54
+ if (st === 'D') out.set(p, { ranges: [], status: 'D' });
55
+ else if (st === 'A' && out.has(p)) out.get(p).status = 'A';
56
+ }
57
+ for (const f of (git(root, ['ls-files', '--others', '--exclude-standard']) ?? '').split('\n').filter(Boolean)) {
58
+ if (!out.has(f)) out.set(f, { ranges: [[1, 1e9]], status: 'A' });
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** Parse a source text: tree + extracted symbols, each symbol with its definition node's arity. */
64
+ async function analyse(spec, source, rel, tree = null) {
65
+ if (!tree) { const { parser } = await prepareLanguage(spec); tree = parser.parse(source); }
66
+ const ex = await extractFile(spec, source, rel);
67
+ for (const s of ex.symbols) {
68
+ if (!CALLABLE.has(s.kind) && !(s.kind === 'field' && /=>|function/.test(s.sig ?? ''))) continue;
69
+ const node = defNodeAt(tree, s);
70
+ if (!node) continue;
71
+ s.arity = paramArity(node, { method: s.kind === 'method' || s.kind === 'constructor', lang: spec.id, isStatic: s.isStatic, decorators: s.decorators ?? [] });
72
+ }
73
+ return { tree, symbols: ex.symbols };
74
+ }
75
+
76
+ /** The outermost node spanning exactly the symbol's range (the definition, not its name token). */
77
+ function defNodeAt(tree, s) {
78
+ let n = tree.rootNode.descendantForPosition({ row: s.startLine - 1, column: s.startCol });
79
+ let best = null;
80
+ for (let g = 0; n && g < 12; g++, n = n.parent) {
81
+ if (n.startPosition.row !== s.startLine - 1 || n.startPosition.column !== s.startCol) { if (best) break; continue; }
82
+ if (n.endPosition.row === s.endLine - 1) best = n;
83
+ }
84
+ return best;
85
+ }
86
+
87
+ /** New syntax errors: ERROR / MISSING nodes whose line text was not already broken in the base. */
88
+ function syntaxErrors(tree, lines, baseErrorTexts) {
89
+ const out = [];
90
+ const visit = (n, depth) => {
91
+ if (out.length >= 5 || depth > 400) return;
92
+ if (n.type === 'ERROR' || n.isMissing) {
93
+ const line = n.startPosition.row + 1;
94
+ const text = (lines[line - 1] ?? '').trim();
95
+ if (!baseErrorTexts.has(text)) out.push({ line, text, missing: n.isMissing ? n.type : null });
96
+ return;
97
+ }
98
+ if (!n.hasError) return;
99
+ for (let i = 0; i < n.childCount; i++) visit(n.child(i), depth + 1);
100
+ };
101
+ if (tree.rootNode.hasError) visit(tree.rootNode, 0);
102
+ return out;
103
+ }
104
+
105
+ function errorTexts(tree, lines) {
106
+ const out = new Set();
107
+ const visit = (n, depth) => {
108
+ if (depth > 400) return;
109
+ if (n.type === 'ERROR' || n.isMissing) { out.add((lines[n.startPosition.row] ?? '').trim()); return; }
110
+ if (!n.hasError) return;
111
+ for (let i = 0; i < n.childCount; i++) visit(n.child(i), depth + 1);
112
+ };
113
+ if (tree.rootNode.hasError) visit(tree.rootNode, 0);
114
+ return out;
115
+ }
116
+
117
+ const TYPE_KINDS = new Set(['class', 'interface', 'struct', 'trait', 'enum', 'object', 'impl', 'type']);
118
+ /** The member (method, field, …) a changed symbol belongs to: itself, or the member around a local. */
119
+ function memberOf(intel, id) {
120
+ let s = intel.sym(id);
121
+ for (let guard = 0; s && guard < 16; guard++) {
122
+ if (TYPE_KINDS.has(s.kind)) return null;
123
+ if (s.parent_id == null) return s.kind === 'method' ? s : null; // Go/Rust: declared outside its type
124
+ const p = intel.sym(s.parent_id);
125
+ if (!p) return null;
126
+ if (TYPE_KINDS.has(p.kind)) return s;
127
+ s = p;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ const keyOf = (s) => `${s.qname}|${s.kind}`;
133
+ /** "2", "1–2", or "1 | 2" for overload sets (definitions without a countable parameter list are skipped). */
134
+ const arityText = (defs) => [...new Set(defs.filter(d => d.arity).map(d => describeArity(d.arity)))].sort().join(' | ') || '?';
135
+ const inRanges = (line, ranges) => ranges.some(([a, b]) => line >= a && line <= b);
136
+
137
+ /** Arity accepted by a set of overloads: a call fits if any overload fits. */
138
+ function unionFits(argc, arities) {
139
+ const list = arities.filter(Boolean);
140
+ if (!list.length) return true;
141
+ return list.some(r => fits(argc, r));
142
+ }
143
+
144
+ export async function checkChanges(intel, { base = 'HEAD', files = null, tests = true } = {}) {
145
+ const root = intel.root;
146
+ const changed = changedFiles(root, base);
147
+ if (!changed) return { error: `git diff against ${base} failed — is this a git repository with that commit?` };
148
+ // the edited files must be re-indexed now, whatever the file watcher has seen so far
149
+ await intel.ensureFresh();
150
+ await intel.ix.syncPaths([...changed.keys()]);
151
+ const targets = [...changed.keys()].filter(f => specForPath(f) && (!files || files.some(x => f === x || f.startsWith(x.replace(/\/?$/, '/')))));
152
+ const report = { base, files: targets.length, syntax: [], arity: [], removed: [], untouched: [], inherited: [], tests: [], commands: [], cases: [], casesTotal: 0, caseCommands: [], notes: [] };
153
+ if (!targets.length) return report;
154
+ // callers only need a syntax tree; definitions (changed files, callees) also need symbols + arities
155
+ const treeCache = new Map(), symCache = new Map();
156
+ const parsed = async (rel) => {
157
+ if (treeCache.has(rel)) return treeCache.get(rel);
158
+ let v = null;
159
+ const spec = specForPath(rel);
160
+ try {
161
+ if (spec) {
162
+ const src = fs.readFileSync(path.join(root, rel), 'utf8');
163
+ const { parser } = await prepareLanguage(spec);
164
+ v = { tree: parser.parse(src), lines: src.split(/\r?\n/), src, spec };
165
+ }
166
+ } catch { v = null; }
167
+ treeCache.set(rel, v);
168
+ return v;
169
+ };
170
+ const analysed = async (rel) => {
171
+ if (symCache.has(rel)) return symCache.get(rel);
172
+ const t = await parsed(rel);
173
+ const v = t ? { ...t, ...(await analyse(t.spec, t.src, rel, t.tree)) } : null;
174
+ symCache.set(rel, v);
175
+ return v;
176
+ };
177
+ const changedSigs = []; // { cur (index row), arities: new overload arities, was: old description }
178
+ const removedSyms = []; // extracted base symbols
179
+ for (const rel of targets) {
180
+ const info = changed.get(rel);
181
+ const spec = specForPath(rel);
182
+ const baseSrc = info.status === 'A' ? null : git(root, ['show', `${base}:${rel}`]);
183
+ const baseA = baseSrc !== null ? await analyse(spec, baseSrc, rel) : null;
184
+ const cur = info.status === 'D' ? null : await analysed(rel);
185
+ if (cur) {
186
+ const baseErr = baseA ? errorTexts(baseA.tree, baseSrc.split(/\r?\n/)) : new Set();
187
+ for (const e of syntaxErrors(cur.tree, cur.lines, baseErr)) report.syntax.push({ path: rel, ...e });
188
+ }
189
+ if (!baseA) continue;
190
+ const baseTree = baseA.tree;
191
+ const now = new Map();
192
+ for (const s of cur?.symbols ?? []) (now.get(keyOf(s)) ?? now.set(keyOf(s), []).get(keyOf(s))).push(s);
193
+ const before = new Map();
194
+ for (const s of baseA.symbols) (before.get(keyOf(s)) ?? before.set(keyOf(s), []).get(keyOf(s))).push(s);
195
+ for (const [k, olds] of before) {
196
+ const news = now.get(k);
197
+ const o = olds[0];
198
+ if (!news) {
199
+ if (o.kind === 'variable' && o.parentIdx >= 0) continue; // locals come and go
200
+ if (!['function', 'method', 'class', 'interface', 'struct', 'enum', 'trait', 'type', 'constant', 'field', 'property', 'constructor'].includes(o.kind)) continue;
201
+ // a moved definition (same qualified name elsewhere in the repository) is not removed
202
+ if (intel.store.get('SELECT 1 FROM symbols WHERE qname = ? AND kind = ? LIMIT 1', o.qname, o.kind)) continue;
203
+ removedSyms.push({ ...o, path: rel });
204
+ continue;
205
+ }
206
+ const was = arityText(olds);
207
+ const is = arityText(news);
208
+ if (was !== is && news.some(s => s.arity)) changedSigs.push({ qname: o.qname, kind: o.kind, path: rel, was, is, arities: news.map(s => s.arity), name: o.name });
209
+ }
210
+ baseTree.delete();
211
+ }
212
+
213
+ // ── call sites of changed signatures ────────────────────────────────────────
214
+ const seen = new Set();
215
+ for (const c of changedSigs.slice(0, 40)) {
216
+ const rows = intel.store.all(`SELECT s.id, s.parent_id FROM symbols s JOIN files f ON f.id = s.file_id WHERE f.path = ? AND s.qname = ? AND s.kind = ?`, c.path, c.qname, c.kind);
217
+ if (!rows.length) continue;
218
+ const ids = rows.map(r => r.id);
219
+ // a constructor is called through its class
220
+ if (isConstructor({ name: c.name, kind: c.kind, lang: specForPath(c.path)?.id }) && rows[0].parent_id != null) ids.push(rows[0].parent_id);
221
+ const calls = intel.store.all(`SELECT r.line, r.col, r.kind, r.conf, f.path, src.qname AS src_qname FROM refs r JOIN files f ON f.id = r.file_id LEFT JOIN symbols src ON src.id = r.src_id
222
+ WHERE r.dst_id IN (${ids.map(() => '?').join(',')}) AND r.kind IN ('call','new') AND r.conf >= 0.7 ORDER BY f.is_test, f.path, r.line`, ...ids);
223
+ const bad = [], untouched = [];
224
+ for (const r of calls) {
225
+ const t = await parsed(r.path);
226
+ if (!t) continue;
227
+ const node = callAt(t.tree, r.line - 1, r.col);
228
+ const argc = node ? callArgc(node) : null;
229
+ const k = `${r.path}:${r.line}:${r.col}`;
230
+ if (argc && !unionFits(argc, c.arities)) { bad.push({ ...r, argc, text: t.lines[r.line - 1]?.trim() }); seen.add(k); }
231
+ else if (!changed.has(r.path)) untouched.push({ ...r, text: t.lines[r.line - 1]?.trim() });
232
+ }
233
+ if (bad.length) report.arity.push({ target: c.qname, path: c.path, was: c.was, is: c.is, total: calls.length, sites: bad });
234
+ if (untouched.length) report.untouched.push({ target: c.qname, path: c.path, was: c.was, is: c.is, sites: untouched });
235
+ }
236
+
237
+ // ── calls written in the changed lines (to anything) that do not fit their callee ─
238
+ for (const rel of targets) {
239
+ const info = changed.get(rel);
240
+ if (info.status === 'D') continue;
241
+ const fileRow = intel.store.get('SELECT id FROM files WHERE path = ?', rel);
242
+ if (!fileRow) continue;
243
+ const calls = intel.store.all(`SELECT r.line, r.col, r.dst_id, r.conf, d.qname, d.kind, d.parent_id, df.path AS dpath, src.qname AS src_qname FROM refs r
244
+ JOIN symbols d ON d.id = r.dst_id JOIN files df ON df.id = d.file_id LEFT JOIN symbols src ON src.id = r.src_id
245
+ WHERE r.file_id = ? AND r.kind IN ('call','new') AND r.conf >= 0.9`, fileRow.id);
246
+ const t = await parsed(rel);
247
+ if (!t) continue;
248
+ for (const r of calls) {
249
+ if (!inRanges(r.line, info.ranges) || seen.has(`${rel}:${r.line}:${r.col}`)) continue;
250
+ let calleePath = r.dpath, calleeQ = r.qname, calleeKind = r.kind;
251
+ if (MEMBER_KINDS.has(r.kind)) {
252
+ // instantiation: check against the class's constructor
253
+ const lang = specForPath(r.dpath)?.id;
254
+ const ctor = intel.store.all(`SELECT s.name, s.qname, s.kind FROM symbols s WHERE s.parent_id = ? AND s.name IN ('constructor','__init__','initialize','__construct')`, r.dst_id)
255
+ .find(s => isConstructor({ ...s, lang }));
256
+ if (!ctor) continue;
257
+ calleeQ = ctor.qname; calleeKind = ctor.kind;
258
+ } else if (!CALLABLE.has(r.kind)) continue;
259
+ const ct = await analysed(calleePath);
260
+ const defs = ct?.symbols.filter(s => s.qname === calleeQ && s.kind === calleeKind) ?? [];
261
+ const arities = defs.map(s => s.arity).filter(Boolean);
262
+ if (!arities.length) continue;
263
+ const node = callAt(t.tree, r.line - 1, r.col);
264
+ const argc = node ? callArgc(node) : null;
265
+ if (argc && !unionFits(argc, arities)) {
266
+ report.arity.push({ target: calleeQ, path: calleePath, is: arities.map(describeArity).join(' | '), total: 1, sites: [{ ...r, path: rel, argc, text: t.lines[r.line - 1]?.trim() }], newCall: true });
267
+ }
268
+ }
269
+ }
270
+
271
+ // ── removed or renamed definitions still in use ─────────────────────────────
272
+ for (const s of removedSyms.slice(0, 30)) {
273
+ const owner = s.owner ?? (s.qname.includes('.') ? s.qname.split('.').slice(-2, -1)[0] : null);
274
+ let rows = intel.store.all(`SELECT r.line, r.recv, r.kind, f.path, src.qname AS src_qname FROM refs r JOIN files f ON f.id = r.file_id LEFT JOIN symbols src ON src.id = r.src_id
275
+ WHERE r.name = ? AND r.dst_id IS NULL AND r.kind IN ('call','new','type','inherit','decorator','value','read') ORDER BY f.is_test, f.path, r.line`, s.name);
276
+ const files = new Map();
277
+ const mentions = (p, word) => {
278
+ const key = p + '\u0000' + word;
279
+ if (!files.has(key)) { const lines = intel.fileLines(p); files.set(key, !!lines && lines.some(l => new RegExp(`\\b${word.replace(/[$]/g, '\\$')}\\b`).test(l))); }
280
+ return files.get(key);
281
+ };
282
+ rows = rows.filter(r => {
283
+ if (s.parentIdx >= 0 && owner) return (r.recv && r.recv.toLowerCase().includes(owner.toLowerCase())) || r.recv === 'this' && r.path === s.path || mentions(r.path, owner);
284
+ return r.path === s.path || mentions(r.path, s.name);
285
+ });
286
+ // imports of a removed top-level name
287
+ const imports = s.parentIdx < 0 ? intel.store.all(`SELECT f.path, i.line FROM imports i JOIN files f ON f.id = i.file_id JOIN files t ON t.id = i.target_file_id
288
+ WHERE t.path = ? AND (i.imported = ? OR i.local = ?)`, s.path, s.name, s.name) : [];
289
+ if (rows.length || imports.length) report.removed.push({ qname: s.qname, kind: s.kind, path: s.path, uses: rows.slice(0, 12), total: rows.length, imports });
290
+ }
291
+
292
+ // ── subclasses that inherit a changed member run the new code too ───────────
293
+ // (with tests: false — the edit hook — only problems are reported, so the diff is not mapped to symbols)
294
+ const seeds = tests ? intel.diffSeeds(base).seeds ?? [] : [];
295
+ const inheritorFiles = new Set();
296
+ const byType = new Map();
297
+ const members = new Map();
298
+ for (const id of seeds) { const m = memberOf(intel, id); if (m && !m.is_test) members.set(m.id, m); }
299
+ for (const m of members.values()) {
300
+ const { inherit, override } = intel.inheritance(m.id);
301
+ if (!inherit.length && !override.length) continue;
302
+ const owner = m.parent_id != null ? intel.sym(m.parent_id) : null;
303
+ const type = owner?.qname ?? m.qname.split('.').slice(0, -1).join('.');
304
+ const e = byType.get(type) ?? byType.set(type, { type, path: m.path, members: [], inherit: new Map(), override: [] }).get(type);
305
+ e.members.push(m.name);
306
+ for (const s of inherit) {
307
+ inheritorFiles.add(s.path); // a test subclass is not listed, but its file is a test to run
308
+ if (!s.is_test) e.inherit.set(s.id, { qname: s.qname, path: s.path });
309
+ }
310
+ for (const s of override) if (!s.is_test) e.override.push({ qname: `${s.qname}.${m.name}`, path: s.path });
311
+ }
312
+ report.inherited = [...byType.values()].map(e => ({ ...e, inherit: [...e.inherit.values()] })).filter(e => e.inherit.length || e.override.length);
313
+
314
+ // ── tests to run ─────────────────────────────────────────────────────────────
315
+ if (seeds.length) {
316
+ const { nodes, fileLevel } = intel.dependents(seeds, { depth: 3, maxNodes: 300 });
317
+ const tests = new Set();
318
+ for (const [id] of nodes) { const x = intel.sym(id); if (x?.is_test) tests.add(x.path); }
319
+ for (const [p] of fileLevel) if (intel.store.get('SELECT is_test FROM files WHERE path = ?', p)?.is_test) tests.add(p);
320
+ for (const t of intel.conventionTests([...targets, ...inheritorFiles])) tests.add(t);
321
+ for (const f of inheritorFiles) if (intel.store.get('SELECT is_test FROM files WHERE path = ?', f)?.is_test) tests.add(f);
322
+ for (const rel of targets) if (intel.store.get('SELECT is_test FROM files WHERE path = ?', rel)?.is_test) tests.add(rel);
323
+ report.tests = [...tests].sort();
324
+ report.commands = testCommands(root, report.tests.slice(0, 40));
325
+ // the test functions closest to the change: changed themselves, calling it, or calling a
326
+ // function that calls it — running these first verifies a fix in seconds
327
+ const near = new Map(seeds.map(id => [id, 0]));
328
+ for (const [id, info] of nodes) if (info.depth <= 2 && !near.has(id)) near.set(id, info.depth);
329
+ const cases = [];
330
+ for (const [id, depth] of near) {
331
+ const x = intel.sym(id);
332
+ if (!x?.is_test || (x.kind !== 'function' && x.kind !== 'method') || !/^test/i.test(x.name)) continue;
333
+ const parent = x.parent_id != null ? intel.sym(x.parent_id) : null;
334
+ cases.push({ path: x.path, name: x.name, cls: parent?.kind === 'class' ? parent.name : null, depth });
335
+ }
336
+ cases.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path) || a.name.localeCompare(b.name));
337
+ report.casesTotal = cases.length;
338
+ report.cases = cases.slice(0, 15);
339
+ if (cases.length <= 15) report.caseCommands = testCaseCommands(root, cases);
340
+ }
341
+ for (const v of treeCache.values()) v?.tree?.delete(); // WASM memory
342
+ return report;
343
+ }