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.
- package/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +31 -105
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- 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
|
+
}
|