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,323 @@
1
+ /**
2
+ * Resolved indirection: conclusions about a piece of code that an agent would otherwise derive by
3
+ * searching and reasoning, one hop per call.
4
+ *
5
+ * Measured on the fourth benchmark round: before its first edit an agent spends most of its thought
6
+ * working out which code actually runs — which override of a method a subclass uses, which entry of a
7
+ * registration table (`TRANSFORMS = {exp.Concat: …}`) handles a key, which method a name-built
8
+ * `getattr(self, f"{key}_sql")` reaches, what a decorator does. Serving where names are defined
9
+ * (evidence) did not lower that thought; these facts serve the conclusion itself, with its location,
10
+ * so the agent can cite it instead of deriving it. Everything here is static analysis of the index and
11
+ * the source text: nothing is imported or executed.
12
+ *
13
+ * overrides a method defined in the lines: what it overrides, which subclasses override it and
14
+ * which inherit it unchanged (those run it as their own)
15
+ * dispatch a call through self/this in the lines to a method that subclasses override
16
+ * handler a function registered as a value in a table (dict/object literal keyed by classes,
17
+ * constants or strings): which table, which key, in which class
18
+ * key a class defined in the lines that is a key of such tables: which handler each class uses
19
+ * by-name a method reached by a name built at run time (getattr/hasattr with a prefix or suffix)
20
+ * decorator what a decorator applied in the lines is (its definition and first doc line)
21
+ *
22
+ * Each fact says whether it is exact (bound by the index) or heuristic (a naming convention).
23
+ */
24
+
25
+ const CALLABLE = new Set(['function', 'method', 'constructor', 'class']);
26
+ const MEMBER = new Set(['method', 'function', 'constructor', 'property']);
27
+
28
+ // the first sentence of a docstring: what the definition is for
29
+ const firstSentence = (doc) => {
30
+ const t = String(doc ?? '').replace(/\s+/g, ' ').trim();
31
+ if (!t) return '';
32
+ const m = /^(.{12,200}?[.!?])(\s|$)/.exec(t);
33
+ return (m ? m[1] : t.slice(0, 140)).replace(/\s+(Parameters|Args|Arguments|Returns)\b.*$/, '');
34
+ };
35
+ const loc = (s) => `${s.path}:${s.start_line}`;
36
+ const short = (s) => s.qname ?? s.name;
37
+
38
+ // ── registration tables ───────────────────────────────────────────────────────
39
+ // Python dicts, JS/TS objects, PHP arrays and Ruby hashes whose entries map a key to code:
40
+ // KEY: handler, "KEY": lambda self: self._parse_x(), [Kind.X]: fn, 'key' => [Cls::class, 'm'], **Base.TABLE / ...base
41
+ const ENTRY = /^\s*(\[?[A-Za-z_][\w.]*\]?|"[^"\n]*"|'[^'\n]*')\s*(?::(?!:)|=>)/;
42
+ const SPREAD = /^\s*(?:\*\*|\.\.\.)([A-Za-z_][\w.]*)\s*,?\s*$/;
43
+ const OPENS = /(?:=|:|=>)\s*(?:\{|\[|array\()\s*(?:#.*|\/\/.*)?$/;
44
+
45
+ export function tablesFor(intel) {
46
+ const version = intel.ix?.version ?? 0;
47
+ if (intel._tables?.version === version) return intel._tables;
48
+ const tables = new Map(), byHandler = new Map(), byKey = new Map();
49
+ const fields = intel.store.all(`SELECT s.id, s.qname, s.name, s.parent_id, s.start_line, s.end_line, f.path, f.id AS file_id
50
+ FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.kind IN ('field', 'variable', 'constant', 'property') AND s.end_line - s.start_line >= 2 AND f.is_test = 0`);
51
+ const refsIn = intel.store.q(`SELECT r.kind, r.name, r.line, r.dst_id, d.kind AS dst_kind FROM refs r LEFT JOIN symbols d ON d.id = r.dst_id
52
+ WHERE r.file_id = ? AND r.line BETWEEN ? AND ? ORDER BY r.line, r.col`);
53
+ for (const f of fields) {
54
+ const lines = intel.fileLines(f.path);
55
+ if (!lines || !OPENS.test(lines[f.start_line - 1] ?? '')) continue;
56
+ const entries = [], spreads = [];
57
+ let cur = null;
58
+ for (let n = f.start_line + 1; n <= f.end_line; n++) {
59
+ const sp = SPREAD.exec(lines[n - 1] ?? '');
60
+ if (sp) { spreads.push({ line: n, text: sp[1] }); continue; }
61
+ const m = ENTRY.exec(lines[n - 1] ?? '');
62
+ if (!m) continue;
63
+ cur = { key: m[1].replace(/^\[|\]$/g, ''), line: n, end: n };
64
+ entries.push(cur);
65
+ }
66
+ if (!entries.length && !spreads.length) continue;
67
+ const starts = [...entries.map(e => e.line), ...spreads.map(x => x.line)].sort((a, b) => a - b);
68
+ for (const e of entries) e.end = (starts.find(l => l > e.line) ?? f.end_line + 1) - 1;
69
+ const refs = refsIn.all(f.file_id, f.start_line, f.end_line);
70
+ const t = { id: f.id, qname: f.qname, name: f.name, owner: f.parent_id, path: f.path, line: f.start_line, parents: [], entries: [] };
71
+ for (const s of spreads) {
72
+ const r = refs.find(x => x.line === s.line && x.dst_id != null && x.name === s.text.split('.').pop());
73
+ if (r) t.parents.push(r.dst_id);
74
+ }
75
+ for (const e of entries) {
76
+ const keyName = e.key.split('.').pop();
77
+ const inEntry = refs.filter(r => r.line >= e.line && r.line <= e.end);
78
+ const keyRef = inEntry.find(r => r.line === e.line && r.name === keyName && r.dst_id != null);
79
+ const handlers = [...new Set(inEntry.filter(r => r !== keyRef && r.dst_id != null && (r.kind === 'value' || r.kind === 'call' || r.kind === 'new')
80
+ && CALLABLE.has(r.dst_kind)).map(r => r.dst_id))];
81
+ if (!handlers.length) continue;
82
+ t.entries.push({ key: e.key, keyId: keyRef?.dst_id ?? null, line: e.line, handlers });
83
+ }
84
+ // a table is a registry when most of its entries map to code
85
+ if (t.entries.length < 2 && !t.parents.length) continue;
86
+ if (t.entries.length < Math.max(2, entries.length / 3) && !t.parents.length) continue;
87
+ tables.set(t.id, t);
88
+ }
89
+ // inherited tables: a subclass that redefines TABLE with `**Base.TABLE` extends it
90
+ for (const t of tables.values()) {
91
+ for (const e of t.entries) {
92
+ for (const h of e.handlers) (byHandler.get(h) ?? byHandler.set(h, []).get(h)).push({ table: t, entry: e });
93
+ const k = e.keyId ?? `"${e.key}"`;
94
+ (byKey.get(k) ?? byKey.set(k, []).get(k)).push({ table: t, entry: e });
95
+ }
96
+ }
97
+ intel._tables = { version, tables, byHandler, byKey };
98
+ return intel._tables;
99
+ }
100
+
101
+ // ── dispatch by a name built at run time ──────────────────────────────────────
102
+ // getattr(self, f"{expression.key}_sql") · getattr(self, "visit_" + node) · hasattr(self, f"_parse_{x}")
103
+ const BUILT_NAME = /\b(?:getattr|hasattr)\(\s*(?:self|cls|this|[a-z_]\w*)\s*,\s*(?:f["']([A-Za-z_]*)\{[^}]+\}([A-Za-z_]*)["']|["']([A-Za-z_]+)["']\s*\+|[\w.]+\s*\+\s*["']([A-Za-z_]+)["'])/;
104
+
105
+ const BUILT_ASSIGN = /\b\w+\s*=\s*(?:f["']([A-Za-z_]*)\{[^}]+\}([A-Za-z_]*)["']|["']([A-Za-z_]+)["']\s*\+|[\w.]+\s*\+\s*["']([A-Za-z_]+)["'])\s*$/;
106
+
107
+ // a registry built by scanning names: `for name in dir(cls): if name.endswith("_sql")` / `startswith("visit_")`
108
+ const NAME_FILTER = /\.(endswith|startswith)\(\s*["']([A-Za-z_]{3,})["']\s*\)/;
109
+ const SCANS_NAMES = /\b(?:dir|vars|getmembers|inspect\.getmembers)\(|__dict__|\bgetattr\(/;
110
+ const CONVENTION_HINT = /getattr\(|hasattr\(|endswith\(|startswith\(/;
111
+
112
+ function conventionsOf(intel) {
113
+ const version = intel.ix?.version ?? 0;
114
+ if (intel._conventions?.version === version) return intel._conventions.list;
115
+ const list = [];
116
+ const seen = new Set();
117
+ const files = intel.store.all(`SELECT id, path FROM files WHERE is_test = 0 AND lang IN ('python', 'javascript', 'typescript', 'tsx', 'ruby', 'php')`);
118
+ for (const f of files) {
119
+ const lines = intel.fileLines(f.path);
120
+ if (!lines) continue;
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const text = lines[i];
123
+ if (!CONVENTION_HINT.test(text)) continue;
124
+ let m = BUILT_NAME.exec(text), prefix = null, suffix = null, shown = text.trim();
125
+ if (m) { prefix = m[1] ?? m[3] ?? ''; suffix = m[2] ?? m[4] ?? ''; }
126
+ else if (/\b(?:getattr|hasattr)\(/.test(text)) {
127
+ // the name built a few lines earlier: handler = f"{expression.key}_sql"; getattr(self, handler)
128
+ for (let n = Math.max(0, i - 12); n < i && !m; n++) {
129
+ m = BUILT_ASSIGN.exec(lines[n]);
130
+ if (m) { prefix = m[1] ?? m[3] ?? ''; suffix = m[2] ?? m[4] ?? ''; shown = `${lines[n].trim()} … ${text.trim()}`; }
131
+ }
132
+ }
133
+ if (!m && (m = NAME_FILTER.exec(text))) {
134
+ // only a registry when the same few lines enumerate names (dir(cls), vars(), getattr)
135
+ const near = lines.slice(Math.max(0, i - 6), i + 7).join('\n');
136
+ if (!SCANS_NAMES.test(near)) m = null;
137
+ else if (m[1] === 'endswith') { prefix = ''; suffix = m[2]; } else { prefix = m[2]; suffix = ''; }
138
+ }
139
+ if (!m || (prefix + suffix).length < 3) continue;
140
+ const line = i + 1;
141
+ const site = intel.store.get(`SELECT id FROM symbols WHERE file_id = ? AND start_line <= ? AND end_line >= ? AND kind IN ('function', 'method')
142
+ ORDER BY end_line - start_line LIMIT 1`, f.id, line, line);
143
+ const caller = site ? intel.sym(site.id) : null;
144
+ // whose members the convention reaches: the caller's class, or the classes of the file for a module function
145
+ const owners = caller?.parent_id != null ? [caller.parent_id]
146
+ : intel.store.all(`SELECT id FROM symbols WHERE file_id = ? AND kind = 'class' AND parent_id IS NULL`, f.id).map(r => r.id);
147
+ const key = `${owners.join(',')}|${prefix}|${suffix}`;
148
+ if (seen.has(key)) continue;
149
+ seen.add(key);
150
+ list.push({ prefix, suffix, owners, caller, path: f.path, line, text: shown.slice(0, 120) });
151
+ }
152
+ }
153
+ intel._conventions = { version, list };
154
+ return list;
155
+ }
156
+
157
+ /** The convention through which a method is called by a built name, if any: its site and the key stem. */
158
+ function reachedByName(intel, sym) {
159
+ if (!MEMBER.has(sym.kind) || sym.parent_id == null) return null;
160
+ for (const c of conventionsOf(intel)) {
161
+ if (!sym.name.startsWith(c.prefix) || !sym.name.endsWith(c.suffix) || sym.name.length <= c.prefix.length + c.suffix.length) continue;
162
+ if (c.owners.length && !c.owners.some(o => o === sym.parent_id || isSubtypeOf(intel, sym.parent_id, o))) continue;
163
+ return { ...c, stem: sym.name.slice(c.prefix.length, sym.name.length - c.suffix.length) };
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function isSubtypeOf(intel, typeId, superId, depth = 0) {
169
+ if (typeId === superId) return true;
170
+ if (depth > 6 || typeId == null) return false;
171
+ for (const { dst_id } of intel.store.all("SELECT DISTINCT dst_id FROM refs WHERE src_id = ? AND kind = 'inherit' AND dst_id IS NOT NULL", typeId)) {
172
+ if (isSubtypeOf(intel, dst_id, superId, depth + 1)) return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ // ── facts for lines an agent reads ────────────────────────────────────────────
178
+ /**
179
+ * Facts about lines [from, to] of `file`, ranked, at most `max`.
180
+ * @param {object} opts
181
+ * @param {Set<string>} [opts.shown] fact keys already given in this session (not repeated)
182
+ * @returns {{ text: string, kind: string, tier: 'exact'|'heuristic', targets: number[], key: string }[]}
183
+ */
184
+ export function factsForRange(intel, file, from, to, { max = 6, shown = new Set() } = {}) {
185
+ const f = intel.store.get('SELECT id FROM files WHERE path = ?', file);
186
+ if (!f) return [];
187
+ const defs = intel.store.all(`SELECT s.id FROM symbols s WHERE s.file_id = ? AND s.start_line <= ? AND s.end_line >= ?
188
+ AND s.kind IN ('function', 'method', 'constructor', 'property', 'class', 'interface', 'struct', 'trait') ORDER BY s.start_line`, f.id, to, from)
189
+ .map(r => intel.sym(r.id)).filter(Boolean)
190
+ // a definition the lines show (at least half of it, or its header) — not the class that merely encloses them
191
+ .filter(s => s.start_line >= from || (Math.min(to, s.end_line) - Math.max(from, s.start_line) + 1) >= (s.end_line - s.start_line + 1) / 2);
192
+ const out = [];
193
+ const push = (fact, score) => { if (!shown.has(fact.key) && !out.some(o => o.key === fact.key)) out.push({ ...fact, score }); };
194
+
195
+ const T = tablesFor(intel);
196
+ for (const s of defs) {
197
+ if (MEMBER.has(s.kind)) {
198
+ // handler registered in tables
199
+ const regs = T.byHandler.get(s.id) ?? [];
200
+ if (regs.length) {
201
+ const first = regs.slice(0, 3).map(({ table, entry }) => `${entry.key} in ${table.qname} (${table.path}:${entry.line})`);
202
+ push({ kind: 'handler', tier: 'exact', key: `handler:${s.id}`, targets: regs.map(r => r.table.id),
203
+ text: `${s.name} is registered as the handler of ${first.join('; ')}${regs.length > 3 ? `; and ${regs.length - 3} more entries` : ''}` }, 5);
204
+ }
205
+ // reached by a name built at run time
206
+ const conv = reachedByName(intel, s);
207
+ if (conv) {
208
+ const keyCls = intel.store.all(`SELECT s.id, s.qname, f.path, s.start_line FROM symbols s JOIN files f ON f.id = s.file_id
209
+ WHERE s.name_lc = ? AND s.kind = 'class' AND f.is_test = 0 LIMIT 3`, conv.stem.replace(/_/g, '').toLowerCase());
210
+ push({ kind: 'by-name', tier: 'heuristic', key: `byname:${s.id}`, targets: [conv.caller?.id, ...keyCls.map(k => k.id)].filter(x => x != null),
211
+ text: `${s.name} is called by name from ${conv.caller ? short(conv.caller) : conv.path} (${conv.path}:${conv.line}: ${conv.text.slice(0, 80)})`
212
+ + (keyCls.length ? ` — for ${keyCls.map(k => `${k.qname} (${k.path}:${k.start_line})`).join(', ')}` : '') + ' [name convention]' }, 4);
213
+ }
214
+ // overrides and inheritors
215
+ if (s.kind === 'method' || s.kind === 'property') {
216
+ const fam = intel.methodFamily(s);
217
+ const inh = intel.inheritance(s.id, { max: 40 });
218
+ const parts = [];
219
+ if (fam.up.length) parts.push(`overrides ${fam.up.slice(0, 2).map(u => `${short(u)} (${loc(u)})`).join(', ')}`);
220
+ if (inh.override.length) parts.push(`overridden in ${inh.override.length} subclass${inh.override.length > 1 ? 'es' : ''}: ${inh.override.slice(0, 4).map(o => `${o.name} (${o.path})`).join(', ')}${inh.override.length > 4 ? ', …' : ''}`);
221
+ if (inh.inherit.length) parts.push(`inherited unchanged by ${inh.inherit.length}: ${inh.inherit.slice(0, 5).map(o => o.name).join(', ')}${inh.inherit.length > 5 ? ', …' : ''}`);
222
+ if (parts.length && (fam.up.length || inh.override.length || inh.inherit.length >= 1)) {
223
+ const downIds = fam.down.map(d => d.id);
224
+ push({ kind: 'overrides', tier: 'exact', key: `ovr:${s.id}`, targets: [...fam.up.map(u => u.id), ...downIds],
225
+ text: `${s.qname}: ${parts.join('; ')}` }, inh.override.length || inh.inherit.length ? 4 : 2);
226
+ }
227
+ }
228
+ // decorators
229
+ for (const d of intel.store.all(`SELECT DISTINCT r.dst_id FROM refs r WHERE r.file_id = ? AND r.kind = 'decorator' AND r.dst_id IS NOT NULL
230
+ AND r.line BETWEEN ? AND ?`, f.id, Math.max(1, s.start_line - 6), s.start_line)) {
231
+ const dec = intel.sym(d.dst_id);
232
+ if (!dec || !CALLABLE.has(dec.kind) || dec.is_test) continue;
233
+ const doc = firstSentence(dec.doc);
234
+ push({ kind: 'decorator', tier: 'exact', key: `dec:${dec.id}`, targets: [dec.id],
235
+ text: `@${dec.name} is ${short(dec)} (${loc(dec)})${doc ? `: ${doc.slice(0, 140)}` : ''}` }, 2);
236
+ }
237
+ } else if (s.kind === 'class') {
238
+ // a class used as a key of registration tables
239
+ const regs = T.byKey.get(s.id) ?? [];
240
+ if (regs.length) {
241
+ const per = regs.slice(0, 4).map(({ table, entry }) => `${table.qname} → ${entry.handlers.map(h => intel.sym(h)?.name).filter(Boolean).join('/')} (${table.path}:${entry.line})`);
242
+ push({ kind: 'key', tier: 'exact', key: `key:${s.id}`, targets: regs.flatMap(r => r.entry.handlers),
243
+ text: `${s.name} is a key of ${regs.length} table entr${regs.length > 1 ? 'ies' : 'y'}: ${per.join('; ')}${regs.length > 4 ? '; …' : ''}` }, 5);
244
+ }
245
+ // handled by a method whose name is built from the class name
246
+ for (const c of conventionsOf(intel)) {
247
+ const want = `${c.prefix}${s.name.toLowerCase()}${c.suffix}`;
248
+ const m = intel.store.all(`SELECT s.id FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.name_lc = ? AND f.is_test = 0 LIMIT 6`, want.toLowerCase())
249
+ .map(r => intel.sym(r.id)).filter(x => x && x.parent_id != null && (!c.owners.length || c.owners.some(o => o === x.parent_id || isSubtypeOf(intel, x.parent_id, o))));
250
+ if (!m.length) continue;
251
+ push({ kind: 'by-name', tier: 'heuristic', key: `keyname:${s.id}:${c.prefix}${c.suffix}`, targets: m.map(x => x.id),
252
+ text: `${s.name} is handled by name (${c.path}:${c.line}) by ${m.slice(0, 3).map(x => `${short(x)} (${loc(x)})`).join(', ')}${m.length > 3 ? ', …' : ''} [name convention]` }, 3);
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ // registration tables defined in the lines: what they extend and which inherited keys they replace
258
+ for (const t of T.tables.values()) {
259
+ if (t.path !== file || t.line < from || t.line > to) continue;
260
+ const inherited = new Map(); // key -> table that defines it
261
+ const visit = (id, depth) => {
262
+ const p = T.tables.get(id);
263
+ if (!p || depth > 6) return;
264
+ for (const e of p.entries) if (!inherited.has(e.key)) inherited.set(e.key, p);
265
+ for (const q of p.parents) visit(q, depth + 1);
266
+ };
267
+ for (const p of t.parents) visit(p, 0);
268
+ const parents = t.parents.map(p => T.tables.get(p) ?? intel.sym(p)).filter(Boolean);
269
+ const replaced = t.entries.filter(e => inherited.has(e.key)).map(e => e.key);
270
+ const subs = [...T.tables.values()].filter(x => x.parents.includes(t.id));
271
+ const parts = [];
272
+ if (parents.length) parts.push(`extends ${parents.map(p => `${p.qname} (${p.path}:${p.line ?? p.start_line})`).join(', ')} with ${t.entries.length} entries`
273
+ + (replaced.length ? `, ${replaced.length} replacing inherited ones (${replaced.slice(0, 6).join(', ')}${replaced.length > 6 ? ', …' : ''})` : ', none replacing inherited ones'));
274
+ // entries for a key whose class a method also handles by name (the table entry is the one to read)
275
+ const shadow = [];
276
+ for (const e of t.entries) {
277
+ if (shadow.length >= 4 || e.keyId == null) continue;
278
+ const k = intel.sym(e.keyId);
279
+ if (!k || k.kind !== 'class') continue;
280
+ for (const c of conventionsOf(intel)) {
281
+ const m = intel.store.all(`SELECT s.id FROM symbols s WHERE s.name_lc = ? AND s.parent_id IS NOT NULL LIMIT 4`, `${c.prefix}${k.name}${c.suffix}`.toLowerCase())
282
+ .map(r => intel.sym(r.id)).filter(x => x && (!c.owners.length || c.owners.some(o => o === x.parent_id || isSubtypeOf(intel, x.parent_id, o))));
283
+ if (m.length && t.owner != null && m.some(x => x.parent_id === t.owner || isSubtypeOf(intel, t.owner, x.parent_id))) { shadow.push(`${e.key} (also ${short(m[0])} by name, ${loc(m[0])})`); break; }
284
+ }
285
+ }
286
+ if (shadow.length) parts.push(`entries for keys also handled by a method by name: ${shadow.join(', ')}`);
287
+ if (subs.length) parts.push(`extended by ${subs.length}: ${subs.slice(0, 5).map(x => x.qname).join(', ')}${subs.length > 5 ? ', …' : ''}`);
288
+ if (parts.length) push({ kind: 'table', tier: 'exact', key: `tbl:${t.id}`, targets: [...t.parents, ...subs.map(x => x.id)], text: `${t.qname} ${parts.join('; ')}` }, 3);
289
+ }
290
+ // calls through self/this in the lines to methods that subclasses override
291
+ const calls = intel.store.all(`SELECT r.line, r.name, r.dst_id FROM refs r WHERE r.file_id = ? AND r.kind = 'call' AND r.dst_id IS NOT NULL
292
+ AND r.line BETWEEN ? AND ? AND r.recv IN ('self', 'this', 'cls', '$this', 'static') ORDER BY r.line`, f.id, from, to);
293
+ const seenDst = new Set();
294
+ for (const c of calls) {
295
+ if (seenDst.has(c.dst_id)) continue;
296
+ seenDst.add(c.dst_id);
297
+ const target = intel.sym(c.dst_id);
298
+ if (!target || target.kind !== 'method') continue;
299
+ const down = intel.methodFamily(target).down;
300
+ if (!down.length) continue;
301
+ push({ kind: 'dispatch', tier: 'exact', key: `dsp:${c.dst_id}`, targets: [c.dst_id, ...down.map(d => d.id)],
302
+ text: `self.${c.name}() (line ${c.line}) runs ${short(target)} (${loc(target)}) or one of ${down.length} override${down.length > 1 ? 's' : ''}: ${down.slice(0, 4).map(d => `${short(d)} (${loc(d)})`).join(', ')}${down.length > 4 ? ', …' : ''}` }, 3);
303
+ }
304
+ out.sort((a, b) => b.score - a.score);
305
+ return out.slice(0, max);
306
+ }
307
+
308
+ /** The same facts for one definition (all of its lines). */
309
+ export function factsForSymbol(intel, id, opts = {}) {
310
+ const s = intel.sym(id);
311
+ return s ? factsForRange(intel, s.path, s.start_line, s.end_line, opts) : [];
312
+ }
313
+
314
+ /** Every table entry keyed by a class, constant or string, and the handler each table maps it to. */
315
+ export function handlersOfKey(intel, keyIdOrText) {
316
+ const T = tablesFor(intel);
317
+ return (T.byKey.get(keyIdOrText) ?? []).map(({ table, entry }) => ({ table: table.qname, path: table.path, line: entry.line, handlers: entry.handlers.map(h => intel.sym(h)).filter(Boolean) }));
318
+ }
319
+
320
+ /** Render facts as lines for tool output and hooks. */
321
+ export function renderFacts(facts) {
322
+ return facts.map(f => `• ${f.text}`);
323
+ }