graph-indexer 2.1.1 → 3.0.1

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 (131) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +236 -312
  3. package/SECURITY.md +53 -58
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +9 -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 +103 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +129 -0
  11. package/package.json +32 -109
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +366 -0
  14. package/src/cli/init.mjs +272 -0
  15. package/src/cli/resident.mjs +185 -0
  16. package/src/index/discover.mjs +128 -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 +332 -0
  21. package/src/index/resolver.mjs +961 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +974 -0
  25. package/src/parse/arity.mjs +153 -0
  26. package/src/parse/extract.mjs +1038 -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 +345 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +683 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +130 -0
  44. package/src/query/textsearch.mjs +178 -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/git.mjs +30 -0
  49. package/src/util/paths.mjs +63 -0
  50. package/src/util/regex.mjs +2 -0
  51. package/vendor/grammars/bash.wasm.gz +0 -0
  52. package/vendor/grammars/c.wasm.gz +0 -0
  53. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  54. package/vendor/grammars/cpp.wasm.gz +0 -0
  55. package/vendor/grammars/css.wasm.gz +0 -0
  56. package/vendor/grammars/go.wasm.gz +0 -0
  57. package/vendor/grammars/java.wasm.gz +0 -0
  58. package/vendor/grammars/javascript.wasm.gz +0 -0
  59. package/vendor/grammars/kotlin.wasm.gz +0 -0
  60. package/vendor/grammars/manifest.json +148 -0
  61. package/vendor/grammars/php.wasm.gz +0 -0
  62. package/vendor/grammars/python.wasm.gz +0 -0
  63. package/vendor/grammars/ruby.wasm.gz +0 -0
  64. package/vendor/grammars/rust.wasm.gz +0 -0
  65. package/vendor/grammars/scala.wasm.gz +0 -0
  66. package/vendor/grammars/tsx.wasm.gz +0 -0
  67. package/vendor/grammars/typescript.wasm.gz +0 -0
  68. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  69. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  70. package/cli-ui.mjs +0 -78
  71. package/colbert.mjs +0 -168
  72. package/config.mjs +0 -494
  73. package/daemon-ctl.mjs +0 -175
  74. package/daemon-lock.mjs +0 -69
  75. package/embedders/python/mlx_embed_server.py +0 -61
  76. package/embedders/python/requirements-mlx.txt +0 -5
  77. package/embedders/setup-mlx.mjs +0 -110
  78. package/embeddings.mjs +0 -571
  79. package/engine/binary.mjs +0 -437
  80. package/engine/memory.mjs +0 -1003
  81. package/engine/sqlite.mjs +0 -1136
  82. package/enrichment.mjs +0 -521
  83. package/git-signals.mjs +0 -248
  84. package/indexer.mjs +0 -546
  85. package/init.mjs +0 -1809
  86. package/layout.mjs +0 -123
  87. package/mcp/centrality.mjs +0 -117
  88. package/mcp/format.mjs +0 -61
  89. package/mcp/resolver.mjs +0 -116
  90. package/mcp/symbolgraph.mjs +0 -137
  91. package/mcp/taint.mjs +0 -225
  92. package/mcp/tools.mjs +0 -1582
  93. package/mcp/topology.mjs +0 -676
  94. package/mcp-server.mjs +0 -197
  95. package/parse/extractor.mjs +0 -628
  96. package/parse/imports.mjs +0 -365
  97. package/parse/interprocedural.mjs +0 -134
  98. package/parse/languages.mjs +0 -253
  99. package/parse/metadata.mjs +0 -701
  100. package/parse/routes.mjs +0 -327
  101. package/parse/scip.mjs +0 -391
  102. package/parse/taint-patterns.mjs +0 -119
  103. package/prompts/CORE.md +0 -91
  104. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  105. package/prompts/INTEGRATION.md +0 -123
  106. package/prompts/frameworks/ANDROID.md +0 -17
  107. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  108. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  109. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  110. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  111. package/prompts/frameworks/RAILS.md +0 -20
  112. package/prompts/frameworks/REACT.md +0 -20
  113. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  114. package/prompts/languages/BASH.md +0 -22
  115. package/prompts/languages/C.md +0 -21
  116. package/prompts/languages/CSHARP.md +0 -24
  117. package/prompts/languages/CSS_SCSS.md +0 -22
  118. package/prompts/languages/GO.md +0 -24
  119. package/prompts/languages/JAVA.md +0 -23
  120. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  121. package/prompts/languages/KOTLIN.md +0 -24
  122. package/prompts/languages/PHP.md +0 -23
  123. package/prompts/languages/PYTHON.md +0 -23
  124. package/prompts/languages/RUBY.md +0 -23
  125. package/prompts/languages/RUST.md +0 -21
  126. package/prompts/languages/SWIFT.md +0 -21
  127. package/seal.mjs +0 -400
  128. package/search-core.mjs +0 -777
  129. package/search-sparse.mjs +0 -183
  130. package/storage.mjs +0 -73
  131. package/watch-daemon.mjs +0 -289
@@ -0,0 +1,683 @@
1
+ /**
2
+ * CodeIntel: the query facade used by the MCP tools and the CLI.
3
+ *
4
+ * Every public query first calls ensureFresh(): files touched since the last check are
5
+ * re-indexed before answering, and the files a result points into are re-validated, so an agent
6
+ * that just edited code never receives stale line numbers or stale call graphs.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { Store } from '../store/db.mjs';
11
+ import { Indexer } from '../index/indexer.mjs';
12
+ import { SearchEngine } from '../search/search.mjs';
13
+ import { computeCentrality, loadGraph } from '../index/graph.mjs';
14
+ import { confidenceLabel } from '../index/resolver.mjs';
15
+ import { dataDir, withinRoot, isRevision } from '../util/paths.mjs';
16
+ import { isConstructor } from '../parse/languages.mjs';
17
+ import { sigArity } from '../parse/arity.mjs';
18
+ import { escapeRegExp } from '../util/regex.mjs';
19
+ import { gitSync } from '../util/git.mjs';
20
+
21
+ const TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'type', 'object', 'module', 'impl']);
22
+ const CALLABLE = new Set(['function', 'method', 'constructor', 'macro']);
23
+ const DEP_KINDS = ['call', 'new', 'inherit', 'type', 'decorator', 'value', 'read'];
24
+ // examples, docs snippets and sample apps are visited after library code and tests
25
+ const EXAMPLE_PATH = /(^|\/)(docs?_src|docs?|examples?|samples?|demos?|tutorials?|benchmarks?|playground)\//i;
26
+
27
+ export class CodeIntel {
28
+ constructor({ root, dbPath = null, log = () => {}, watch = false }) {
29
+ this.root = path.resolve(root);
30
+ this.dbPath = dbPath ?? path.join(dataDir(this.root), 'index.db');
31
+ this.log = log;
32
+ this.watchEnabled = watch;
33
+ this.dirty = new Set();
34
+ this.lastSweep = 0;
35
+ this.sweepIntervalMs = 3000;
36
+ this.watcher = null;
37
+ this.watcherHealthy = false;
38
+ this.lastSyncStats = null;
39
+ this.fileCache = new Map();
40
+ this._centrality = null;
41
+ this._centralityVersion = -1;
42
+ this.indexing = null;
43
+ }
44
+
45
+ // ── lifecycle ────────────────────────────────────────────────────────────────
46
+ /**
47
+ * Open the store and reconcile with the working tree. With `background: true` the initial
48
+ * sync runs asynchronously (queries answer from the partial index meanwhile) — used by the
49
+ * MCP server so the first tool call never times out on a large repository.
50
+ */
51
+ async open({ onProgress = null, background = false } = {}) {
52
+ this.store = new Store(this.dbPath);
53
+ this.ix = new Indexer({ root: this.root, store: this.store, log: this.log });
54
+ this.ix.load();
55
+ this.search = new SearchEngine(this.ix, { centrality: () => this.centrality() });
56
+ if (this.watchEnabled) this.#startWatcher();
57
+ this.initialDone = false;
58
+ this.progress = null;
59
+ const p = this.ix.sync({ onProgress: (done, total) => { this.progress = { done, total }; onProgress?.(done, total); } })
60
+ .then((res) => { this.lastSyncStats = res; this.lastSweep = Date.now(); this.initialDone = true; this.progress = null; return res; })
61
+ .finally(() => { if (this.indexing === p) this.indexing = null; });
62
+ this.indexing = p;
63
+ this.ready = p;
64
+ if (!background) await p;
65
+ return background ? null : p;
66
+ }
67
+
68
+ close() {
69
+ try { this.watcher?.close(); } catch { /* closed */ }
70
+ this.store?.close();
71
+ }
72
+
73
+ #startWatcher() {
74
+ try {
75
+ this.watcher = fs.watch(this.root, { recursive: true, persistent: false }, (_ev, file) => {
76
+ if (!file) { this.lastSweep = 0; return; }
77
+ const rel = String(file).split(path.sep).join('/');
78
+ if (rel.startsWith('.git/') || rel.includes('/.git/') || rel.startsWith('.graph-indexer/') || rel.includes('node_modules/')) return;
79
+ this.dirty.add(rel);
80
+ });
81
+ this.watcher.on?.('error', () => { this.watcherHealthy = false; });
82
+ this.watcherHealthy = true;
83
+ } catch (e) {
84
+ this.log(`file watcher unavailable (${e.message}); falling back to periodic stat sweeps`);
85
+ this.watcherHealthy = false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Freshness contract: before answering, apply pending file changes. With a healthy watcher we
91
+ * re-index only the dirty paths (plus a periodic full sweep as a safety net); without one we
92
+ * do a stat sweep at most every few seconds (cheap: size+mtime compare).
93
+ */
94
+ async ensureFresh() {
95
+ if (!this.initialDone) return null; // initial build in progress: answer from the partial index
96
+ if (this.indexing) await this.indexing;
97
+ const now = Date.now();
98
+ const sweepDue = now - this.lastSweep > (this.watcherHealthy ? 30_000 : this.sweepIntervalMs);
99
+ let res = null;
100
+ if (sweepDue) {
101
+ this.dirty.clear();
102
+ this.indexing = this.ix.sync();
103
+ try { res = await this.indexing; } finally { this.indexing = null; }
104
+ this.lastSweep = Date.now();
105
+ } else if (this.dirty.size) {
106
+ const paths = [...this.dirty];
107
+ this.dirty.clear();
108
+ this.indexing = this.ix.syncPaths(paths);
109
+ try { res = await this.indexing; } finally { this.indexing = null; }
110
+ }
111
+ if (res && (res.changed || res.added || res.removed)) this.lastSyncStats = { ...res, at: Date.now() };
112
+ return res;
113
+ }
114
+
115
+ /** Re-validate specific files right before rendering them (cheap stat). */
116
+ async revalidate(paths) {
117
+ const stale = this.ix.staleAmong(paths);
118
+ if (stale.length) await this.ix.syncPaths(stale);
119
+ return stale;
120
+ }
121
+
122
+ centrality() {
123
+ if (this._centralityVersion !== this.ix.version || !this._centrality) {
124
+ this._centrality = computeCentrality(this.store);
125
+ this._centralityVersion = this.ix.version;
126
+ }
127
+ return this._centrality;
128
+ }
129
+
130
+ // ── source access ────────────────────────────────────────────────────────────
131
+ fileLines(rel) {
132
+ if (!withinRoot(this.root, rel)) return null;
133
+ const abs = path.join(this.root, rel);
134
+ let st;
135
+ try { st = fs.statSync(abs); } catch { return null; }
136
+ const key = rel;
137
+ const c = this.fileCache.get(key);
138
+ if (c && c.mtime === st.mtimeMs && c.size === st.size) return c.lines;
139
+ let text;
140
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { return null; }
141
+ const lines = text.split(/\r?\n/);
142
+ this.fileCache.set(key, { mtime: st.mtimeMs, size: st.size, lines });
143
+ if (this.fileCache.size > 256) this.fileCache.delete(this.fileCache.keys().next().value);
144
+ return lines;
145
+ }
146
+
147
+ // ── symbol lookup ────────────────────────────────────────────────────────────
148
+ sym(id) {
149
+ return this.store.get(`SELECT s.*, f.path, f.lang, f.is_test FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id = ?`, id) ?? null;
150
+ }
151
+
152
+ /**
153
+ * Resolve a user-supplied target to symbols. Accepts:
154
+ * `name` · `Class.method` · `path/file.ts:Class.method` · `path/file.ts#qname` · `path/file.ts:42`
155
+ * @returns {{ matches: object[], how: string }}
156
+ */
157
+ findSymbols(target, { limit = 20 } = {}) {
158
+ let t = String(target).trim().replace(/\(\)$/, '');
159
+ if (t.startsWith(this.root + '/')) t = t.slice(this.root.length + 1); // absolute path inside the repository
160
+ let m = /^(.+?\.[A-Za-z0-9]+):(\d+)$/.exec(t);
161
+ if (m) {
162
+ const [, file, line] = m;
163
+ 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.start_line <= ? AND s.end_line >= ?
164
+ ORDER BY (s.end_line - s.start_line) ASC LIMIT 1`, file.replace(/^\.\//, ''), Number(line), Number(line));
165
+ return { matches: rows.map(r => this.sym(r.id)), how: 'location' };
166
+ }
167
+ m = /^([^:#]+?\.[A-Za-z0-9]+)[:#](.+)$/.exec(t);
168
+ if (m) {
169
+ const [, file, q] = m;
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
+ file.replace(/^\.\//, ''), q, q, '%.' + q, q, limit);
172
+ return { matches: rows.map(r => this.sym(r.id)), how: 'file+name' };
173
+ }
174
+ const norm = t.replace(/::|#|->/g, '.');
175
+ if (norm.includes('.')) {
176
+ const name = norm.split('.').pop();
177
+ const rows = this.store.all(`SELECT id, qname FROM symbols WHERE name = ?`, name).filter(r => r.qname === norm || r.qname.endsWith('.' + norm));
178
+ if (rows.length) return { matches: this.#rank(rows.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'qualified' };
179
+ }
180
+ let rows = this.store.all('SELECT id FROM symbols WHERE name = ?', norm.split('.').pop());
181
+ if (!rows.length) rows = this.store.all('SELECT id FROM symbols WHERE name_lc = ?', norm.split('.').pop().toLowerCase());
182
+ return { matches: this.#rank(rows.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'name' };
183
+ }
184
+
185
+ /**
186
+ * Order same-name candidates the way a developer means them: library code before tests and
187
+ * example/sample apps, definitions that are used before unused copies, central before peripheral.
188
+ */
189
+ #rank(ids) {
190
+ const cen = this.centrality();
191
+ const meta = new Map(ids.map(id => [id, this.store.get(`SELECT f.is_test, f.path, s.kind, s.exported, (SELECT COUNT(*) FROM refs r WHERE r.dst_id = s.id) AS nrefs
192
+ FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id = ?`, id)]));
193
+ const score = (id) => {
194
+ const m = meta.get(id);
195
+ return (cen.get(id) ?? 0) + (m?.is_test ? -1 : 0) + (m && EXAMPLE_PATH.test(m.path) ? -0.8 : 0) + (m?.exported ? 0.2 : 0)
196
+ + (TYPE_KINDS.has(m?.kind) || CALLABLE.has(m?.kind) ? 0.3 : 0) + Math.log1p(m?.nrefs ?? 0) * 0.1;
197
+ };
198
+ return [...ids].sort((a, b) => score(b) - score(a) || a - b);
199
+ }
200
+
201
+ members(id) {
202
+ return this.store.all('SELECT id, name, qname, kind, start_line, end_line, sig, visibility FROM symbols WHERE parent_id = ? ORDER BY start_line', id);
203
+ }
204
+
205
+ /** Members declared outside the type body (Go/Rust/C++ receivers, JS prototype assignments). */
206
+ ownedMembers(sym) {
207
+ const t = this.ix.table;
208
+ const ids = t.byOwner.get(sym.name) ?? [];
209
+ const f = t.file(sym.file_id);
210
+ return ids.map(id => t.sym(id)).filter(s => s && s.parentId !== sym.id && (t.file(s.fileId)?.dir === f?.dir || t.file(s.fileId)?.package === f?.package || s.fileId === sym.file_id))
211
+ .map(s => this.store.get('SELECT s.id, s.name, s.qname, s.kind, s.start_line, s.end_line, s.sig, s.visibility, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id = ?', s.id));
212
+ }
213
+
214
+ // ── references ───────────────────────────────────────────────────────────────
215
+ /** Supertypes (resolved) of the type that declares a member, for dispatch-aware references. */
216
+ #supertypeMembers(sym) {
217
+ const t = this.ix.table, r = this.ix.resolver;
218
+ const type = r.enclosingType(sym.id);
219
+ if (!type) return [];
220
+ const out = [];
221
+ const seen = new Set();
222
+ const visit = (typeSym, depth) => {
223
+ if (!typeSym || depth > 5 || seen.has(typeSym.id)) return;
224
+ seen.add(typeSym.id);
225
+ for (const b of typeSym.bases ?? []) {
226
+ const bt = r.resolveTypeName(b, typeSym.fileId);
227
+ if (!bt) continue;
228
+ for (const mid of this.#membersNamed(bt.id, sym.name)) if (!t.sym(mid)?.isStatic && this.#sameArity(sym, mid)) out.push({ id: mid, via: bt.name });
229
+ visit(bt, depth + 1);
230
+ }
231
+ };
232
+ if (type.id != null) visit(t.sym(type.id), 0);
233
+ return out;
234
+ }
235
+
236
+ /**
237
+ * The subtypes of a member's type, transitively, split into those that inherit the member — they
238
+ * run it as their own, so a change to it reaches them — and those that redeclare it (their own
239
+ * version runs; their subtypes are not followed).
240
+ * @returns {{ inherit: object[], override: object[] }} index rows of the subtypes
241
+ */
242
+ inheritance(id, { max = 30 } = {}) {
243
+ const out = { inherit: [], override: [] };
244
+ const sym = this.sym(id);
245
+ if (!sym) return out;
246
+ const type = this.ix.resolver.enclosingType(sym.id);
247
+ if (!type || type.id == null || type.id === sym.id) return out;
248
+ // every subtype, then which version each one runs: the first type of its method resolution
249
+ // order that declares the member (so `class C(B, A)` runs B's override even when reached via A)
250
+ const subs = [];
251
+ const seen = new Set([type.id]);
252
+ let frontier = [type.id];
253
+ for (let depth = 0; depth < 8 && frontier.length && subs.length < 3000; depth++) {
254
+ const next = [];
255
+ for (const tid of frontier) {
256
+ for (const { src_id } of this.store.all("SELECT DISTINCT src_id FROM refs WHERE dst_id = ? AND kind = 'inherit' AND src_id IS NOT NULL", tid)) {
257
+ if (seen.has(src_id)) continue;
258
+ seen.add(src_id);
259
+ subs.push(src_id);
260
+ next.push(src_id);
261
+ }
262
+ }
263
+ frontier = next;
264
+ }
265
+ const declares = new Map();
266
+ const declared = (tid) => { if (!declares.has(tid)) declares.set(tid, this.#membersNamed(tid, sym.name).length > 0); return declares.get(tid); };
267
+ for (const sid of subs) {
268
+ const order = this.ix.resolver.mro(sid);
269
+ if (!order.includes(type.id)) continue;
270
+ const owner = order.find(declared);
271
+ let bucket = null;
272
+ if (owner === type.id) bucket = out.inherit;
273
+ // its own version, where it would otherwise run this one
274
+ else if (owner === sid && order.slice(1).find(declared) === type.id) bucket = out.override;
275
+ if (!bucket) continue;
276
+ const st = this.sym(sid);
277
+ if (!st) continue;
278
+ bucket.push(st);
279
+ if (out.inherit.length + out.override.length >= max) return out;
280
+ }
281
+ return out;
282
+ }
283
+
284
+ /** Members of a type with a name: declared in its body, or outside it with the type as owner (Go, Rust, C++). */
285
+ /** Where overloads are methods of their own, an override has the parameters of what it overrides. */
286
+ #sameArity(sym, otherId) {
287
+ if (!this.ix.resolver.specs[sym.lang]?.distinctOverloads) return true;
288
+ const o = this.store.get('SELECT name, sig FROM symbols WHERE id = ?', otherId);
289
+ const a = sigArity(sym.sig, sym.name), b = o ? sigArity(o.sig, o.name) : null;
290
+ return !a || !b || (a.min === b.min && a.max === b.max);
291
+ }
292
+
293
+ #membersNamed(typeId, name) {
294
+ const t = this.ix.table;
295
+ const ty = t.sym(typeId);
296
+ const out = [...(t.byParent.get(typeId)?.get(name) ?? [])];
297
+ if (ty) {
298
+ const dir = path.posix.dirname(t.file(ty.fileId)?.path ?? '');
299
+ for (const id of t.byOwner.get(ty.name) ?? []) {
300
+ const m = t.sym(id);
301
+ if (m && m.name === name && m.parentId !== typeId && path.posix.dirname(t.file(m.fileId)?.path ?? '') === dir && !out.includes(id)) out.push(id);
302
+ }
303
+ }
304
+ return out;
305
+ }
306
+
307
+ /** Overload set: same qualified name, same file, same container (TS/Java/C#/C++ overloads). */
308
+ overloads(sym) {
309
+ // where each overload is a method of its own (Java, C#, Kotlin…), calls are bound to the one they reach
310
+ if (this.ix.resolver.specs[sym.lang]?.distinctOverloads) return [];
311
+ return this.store.all('SELECT id FROM symbols WHERE file_id = ? AND qname = ? AND kind = ? AND (parent_id IS ? OR parent_id = ?) AND is_static = ?',
312
+ sym.file_id, sym.qname, sym.kind, sym.parent_id, sym.parent_id, sym.is_static ? 1 : 0).map(r => r.id);
313
+ }
314
+
315
+ /** Is `memberId` one of the members `sym` overrides (declared in a supertype)? */
316
+ #isSupertypeMemberOf(memberId, sym) {
317
+ return this.#supertypeMembers(sym).some(s => s.id === memberId);
318
+ }
319
+
320
+ /** Members with the same name in subtypes (overrides/implementations), transitively. */
321
+ #subtypeMembers(sym) {
322
+ const t = this.ix.table, r = this.ix.resolver;
323
+ const type = r.enclosingType(sym.id);
324
+ if (!type || type.id == null) return [];
325
+ const out = [];
326
+ const seen = new Set([type.id]);
327
+ let frontier = [type.id];
328
+ for (let depth = 0; depth < 4 && frontier.length; depth++) {
329
+ const next = [];
330
+ for (const tid of frontier) {
331
+ const subs = this.store.all("SELECT DISTINCT src_id FROM refs WHERE dst_id = ? AND kind = 'inherit' AND src_id IS NOT NULL", tid);
332
+ for (const { src_id } of subs) {
333
+ if (seen.has(src_id)) continue;
334
+ seen.add(src_id);
335
+ next.push(src_id);
336
+ const st = t.sym(src_id);
337
+ for (const mid of this.#membersNamed(src_id, sym.name)) if (!t.sym(mid)?.isStatic && this.#sameArity(sym, mid)) out.push({ id: mid, via: st?.name ?? '?' });
338
+ }
339
+ }
340
+ frontier = next;
341
+ }
342
+ return out;
343
+ }
344
+
345
+ /**
346
+ * String literals naming a member in code that works with its class or a subclass —
347
+ * `sinon.stub(obj, 'name')`, `jest.spyOn(x, 'name')`, `getattr(o, "name")`,
348
+ * `patch.object(C, "name")`, `patch("pkg.mod.C.name")`: uses no type checker sees, which a
349
+ * rename has to update too. Test titles (`describe('name', …)`) are left out.
350
+ * @returns {{ path: string, line: number, text: string }[]}
351
+ */
352
+ stringMentions(id, { max = 20 } = {}) {
353
+ const sym = this.sym(id);
354
+ if (!sym || !['method', 'property', 'field', 'function'].includes(sym.kind) || sym.name.length < 3) return [];
355
+ const files = new Set(this.references(id, { family: true })?.groups.keys() ?? []);
356
+ const type = this.ix.resolver.enclosingType(sym.id);
357
+ if (type?.id != null) {
358
+ const family = new Set([type.id]);
359
+ let frontier = [type.id];
360
+ for (let depth = 0; depth < 4 && frontier.length; depth++) {
361
+ const next = [];
362
+ for (const tid of frontier) {
363
+ for (const { src_id } of this.store.all("SELECT DISTINCT src_id FROM refs WHERE dst_id = ? AND kind = 'inherit' AND src_id IS NOT NULL", tid)) {
364
+ if (!family.has(src_id)) { family.add(src_id); next.push(src_id); }
365
+ }
366
+ }
367
+ frontier = next;
368
+ }
369
+ const ids = [...family];
370
+ 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);
371
+ } else files.add(sym.path);
372
+ const name = escapeRegExp(sym.name);
373
+ const lit = new RegExp(`(['"\`])(?:[\\w.]*\\.)?${name}\\1`);
374
+ const title = /^\s*(?:describe|it|test|context|suite)(?:\.\w+)?\s*\(/;
375
+ const out = [];
376
+ for (const f of [...files].sort()) {
377
+ const lines = this.fileLines(f) ?? [];
378
+ for (let i = 0; i < lines.length && out.length < max; i++) {
379
+ if (lit.test(lines[i]) && !title.test(lines[i])) out.push({ path: f, line: i + 1, text: lines[i].trim() });
380
+ }
381
+ }
382
+ return out;
383
+ }
384
+
385
+ /**
386
+ * All references to a symbol, grouped by file. Includes its overload set and, for members,
387
+ * the method family: calls through a supertype (may dispatch here) and calls to overrides in
388
+ * subtypes — each labelled so the agent can tell direct uses from dispatch-related ones.
389
+ * @returns {{ groups: Map<string, object[]>, total: number, unresolvedSameName: number }}
390
+ */
391
+ references(id, { kinds = null, includeTests = true, minConf = 0, family = true } = {}) {
392
+ const sym = this.sym(id);
393
+ if (!sym) return null;
394
+ const ids = [...new Set([id, ...this.overloads(sym)])];
395
+ const via = new Map();
396
+ if (family && !sym.is_static && (sym.kind === 'method' || sym.kind === 'property' || sym.kind === 'field')) {
397
+ for (const s of this.#supertypeMembers(sym)) if (!ids.includes(s.id)) { ids.push(s.id); via.set(s.id, s.via); }
398
+ for (const s of this.#subtypeMembers(sym)) if (!ids.includes(s.id)) { ids.push(s.id); via.set(s.id, s.via); }
399
+ }
400
+ const rows = this.store.all(`SELECT r.id, r.kind, r.line, r.col, r.conf, r.ncand, r.recv, r.dst_id, f.path, f.is_test, src.qname AS src_qname, src.kind AS src_kind, src.start_line AS src_line
401
+ FROM refs r JOIN files f ON f.id = r.file_id LEFT JOIN symbols src ON src.id = r.src_id
402
+ WHERE r.dst_id IN (${ids.map(() => '?').join(',')}) ORDER BY f.is_test, f.path, r.line`, ...ids); // production code before tests
403
+ const out = [];
404
+ for (const r of rows) {
405
+ if (kinds && !kinds.includes(r.kind)) continue;
406
+ if (!includeTests && r.is_test) continue;
407
+ if (r.conf < minConf) continue;
408
+ // `super().m()` binds statically to the parent's m: it cannot reach an override elsewhere
409
+ if (r.recv === 'super' && via.has(r.dst_id) && this.#isSupertypeMemberOf(r.dst_id, sym)) continue;
410
+ out.push({ ...r, via: via.get(r.dst_id) ?? null, confidence: confidenceLabel(via.has(r.dst_id) ? r.conf * 0.8 : r.conf) });
411
+ }
412
+ const unbound = this.#unboundSameName(sym, ids, { includeTests });
413
+ const unresolved = unbound.total;
414
+ // where the other references with this name went (lets an agent rule them out without grepping)
415
+ const elsewhere = this.store.all(`SELECT d.qname, df.path, COUNT(*) AS n FROM refs r JOIN symbols d ON d.id = r.dst_id JOIN files df ON df.id = d.file_id
416
+ JOIN files f ON f.id = r.file_id WHERE r.name = ? AND r.dst_id NOT IN (${ids.map(() => '?').join(',')})${includeTests ? '' : ' AND f.is_test = 0'}
417
+ GROUP BY r.dst_id ORDER BY n DESC LIMIT 4`, sym.name, ...ids);
418
+ const groups = new Map();
419
+ for (const r of out) (groups.get(r.path) ?? groups.set(r.path, []).get(r.path)).push(r);
420
+ return { sym, groups, total: out.length, unresolvedSameName: unresolved, unbound, elsewhere };
421
+ }
422
+
423
+ /**
424
+ * Same-name call sites whose receiver type is unknown, split by plausibility: a call to
425
+ * `Reflector.get` has to obtain a Reflector somewhere, so a site is plausible when its file
426
+ * mentions the declaring type (or a related type) or its receiver is named after it
427
+ * (`this.reflector.get`). The rest are in files that never mention the type.
428
+ */
429
+ #unboundSameName(sym, ids, { includeTests = true } = {}) {
430
+ const rows = this.store.all(`SELECT r.line, r.recv, f.path FROM refs r JOIN files f ON f.id = r.file_id
431
+ WHERE r.name = ? AND r.dst_id IS NULL AND r.kind IN ('call','new','decorator')${includeTests ? '' : ' AND f.is_test = 0'} ORDER BY f.is_test, f.path, r.line`, sym.name);
432
+ if (!rows.length) return { total: 0, plausible: [], typeNames: [] };
433
+ const typeNames = new Set();
434
+ const owner = sym.parent_id != null ? this.sym(sym.parent_id) : null;
435
+ // a type is mentioned by its name; an anonymous class (`<anonymous>`) never is
436
+ const named = (n) => n && !n.startsWith('<anonymous');
437
+ if (owner && TYPE_KINDS.has(owner.kind)) {
438
+ if (named(owner.name)) typeNames.add(owner.name);
439
+ // subclasses that inherit the member without redeclaring it: `jsonSocket.send()` on a
440
+ // JsonSocket extends TcpSocket runs TcpSocket.send
441
+ if (!sym.is_static) for (const name of this.#inheritingSubtypes(owner.id, sym.name)) if (named(name)) typeNames.add(name);
442
+ }
443
+ for (const id of ids) {
444
+ const s = id === sym.id ? null : this.sym(id);
445
+ const o = s?.parent_id != null ? this.sym(s.parent_id) : null;
446
+ if (o && TYPE_KINDS.has(o.kind) && named(o.name)) typeNames.add(o.name);
447
+ }
448
+ if (!typeNames.size) {
449
+ // a free function: plausible where its file is imported, or next to it
450
+ const dir = path.posix.dirname(sym.path);
451
+ const importers = new Set(this.store.all('SELECT DISTINCT f.path FROM imports i JOIN files f ON f.id = i.file_id WHERE i.target_file_id = ?', sym.file_id).map(r => r.path));
452
+ const plausible = rows.filter(r => importers.has(r.path) || path.posix.dirname(r.path) === dir);
453
+ return { total: rows.length, plausible, typeNames: [] };
454
+ }
455
+ const names = [...typeNames];
456
+ const wordRe = new RegExp(`\\b(${names.map(n => escapeRegExp(n)).join('|')})\\b`);
457
+ const lower = names.map(n => n.toLowerCase());
458
+ const mentions = new Map();
459
+ const plausible = rows.filter(r => {
460
+ const recv = (r.recv || '').toLowerCase();
461
+ if (recv && lower.some(n => recv.includes(n))) return true;
462
+ if (!mentions.has(r.path)) {
463
+ const lines = this.fileLines(r.path);
464
+ mentions.set(r.path, !!lines && lines.some(l => wordRe.test(l)));
465
+ }
466
+ return mentions.get(r.path);
467
+ });
468
+ return { total: rows.length, plausible, typeNames: names };
469
+ }
470
+
471
+ /** Names of the subtypes of a type (transitively) that inherit its member `name` without redeclaring it. */
472
+ #inheritingSubtypes(typeId, name, max = 20) {
473
+ const t = this.ix.table;
474
+ const out = [], seen = new Set([typeId]);
475
+ let frontier = [typeId];
476
+ for (let depth = 0; depth < 4 && frontier.length && out.length < max; depth++) {
477
+ const next = [];
478
+ for (const tid of frontier) {
479
+ for (const { src_id } of this.store.all("SELECT DISTINCT src_id FROM refs WHERE dst_id = ? AND kind = 'inherit' AND src_id IS NOT NULL", tid)) {
480
+ if (seen.has(src_id)) continue;
481
+ seen.add(src_id);
482
+ if (this.#membersNamed(src_id, name).length) continue; // overrides it: its calls bind to its own member
483
+ const st = t.sym(src_id);
484
+ if (st && out.length < max) { out.push(st.name); next.push(src_id); }
485
+ }
486
+ }
487
+ frontier = next;
488
+ }
489
+ return out;
490
+ }
491
+
492
+ /** Outgoing references of a symbol (what it calls/uses), including nested closures. */
493
+ callees(id, { kinds = ['call', 'new'] } = {}) {
494
+ const sym = this.sym(id);
495
+ if (!sym) return [];
496
+ const scope = [id, ...this.store.all(`SELECT id FROM symbols WHERE file_id = ? AND start_line >= ? AND end_line <= ? AND id != ? AND kind IN ('function','method')`, sym.file_id, sym.start_line, sym.end_line, id).map(r => r.id)];
497
+ const rows = this.store.all(`SELECT r.kind, r.line, r.name, r.recv, r.conf, r.ncand, r.dst_id, d.qname AS dst_qname, d.kind AS dst_kind, df.path AS dst_path, d.start_line AS dst_line
498
+ FROM refs r LEFT JOIN symbols d ON d.id = r.dst_id LEFT JOIN files df ON df.id = d.file_id
499
+ WHERE r.src_id IN (${scope.map(() => '?').join(',')}) AND r.kind IN (${kinds.map(() => '?').join(',')}) ORDER BY r.line`, ...scope, ...kinds);
500
+ return rows;
501
+ }
502
+
503
+ /** Transitive callers/dependents (BFS over reverse edges), confidence multiplied along paths. */
504
+ dependents(startIds, { depth = 3, maxNodes = 300, minConf = 0.3, kinds = DEP_KINDS } = {}) {
505
+ const seen = new Map(); // id -> { depth, conf, via, kind, sites }
506
+ const fileLevel = new Map(); // path -> { depth, conf, via, sites } for module-level references
507
+ const overflow = new Set(); // dependents beyond maxNodes: counted, not expanded
508
+ let frontier = [];
509
+ for (const id of startIds) {
510
+ seen.set(id, { depth: 0, conf: 1, via: null });
511
+ frontier.push({ id, conf: 1 });
512
+ }
513
+ // overrides / implementations of a changed method must keep matching its signature
514
+ for (const id of startIds) {
515
+ const sym = this.sym(id);
516
+ if (!sym || sym.is_static || sym.kind !== 'method') continue;
517
+ for (const m of this.#subtypeMembers(sym)) {
518
+ if (seen.has(m.id)) continue;
519
+ seen.set(m.id, { depth: 1, conf: 0.9, via: id, kind: 'override', sites: [] });
520
+ frontier.push({ id: m.id, conf: 0.9 });
521
+ }
522
+ }
523
+ for (let d = 1; d <= depth && frontier.length; d++) {
524
+ const next = [];
525
+ for (const { id, conf } of frontier) {
526
+ const sym = this.sym(id);
527
+ const targets = [id];
528
+ if (sym && !sym.is_static && (sym.kind === 'method' || sym.kind === 'property')) for (const s of this.#supertypeMembers(sym)) targets.push(s.id);
529
+ // a constructor is invoked through its class: `new Foo()` / `Foo()` (Python)
530
+ if (isConstructor(sym) && sym.parent_id != null) targets.push(sym.parent_id);
531
+ const rows = this.store.all(`SELECT r.src_id, r.conf, r.kind, r.line, r.dst_id, r.recv, f.path, f.is_test FROM refs r JOIN files f ON f.id = r.file_id
532
+ WHERE r.dst_id IN (${targets.map(() => '?').join(',')}) AND r.kind IN (${kinds.map(() => '?').join(',')})`, ...targets, ...kinds);
533
+ const rank = (r) => (EXAMPLE_PATH.test(r.path) ? 2 : r.is_test ? 1 : 0);
534
+ rows.sort((a, b) => rank(a) - rank(b));
535
+ for (const r of rows) {
536
+ // through the class only its instantiations reach the constructor
537
+ if (r.dst_id !== id && isConstructor(sym) && r.dst_id === sym.parent_id && r.kind !== 'call' && r.kind !== 'new') continue;
538
+ // `super().m()` runs exactly the parent's m, never this override (it is static dispatch)
539
+ if (r.dst_id !== id && r.recv === 'super') continue;
540
+ const c = conf * r.conf;
541
+ if (c < minConf) continue;
542
+ if (r.src_id == null) {
543
+ const prev = fileLevel.get(r.path);
544
+ if (!prev) fileLevel.set(r.path, { depth: d, conf: c, via: id, sites: [r.line] });
545
+ else { if (prev.conf < c) Object.assign(prev, { depth: d, conf: c, via: id }); if (prev.sites.length < 20) prev.sites.push(r.line); }
546
+ continue;
547
+ }
548
+ const prev = seen.get(r.src_id);
549
+ if (prev) {
550
+ if (prev.via === id && prev.sites && prev.sites.length < 20) prev.sites.push({ path: r.path, line: r.line });
551
+ if (prev.conf >= c) continue;
552
+ }
553
+ if (!prev && seen.size >= maxNodes + startIds.length) { overflow.add(r.src_id); continue; } // count, don't expand
554
+ seen.set(r.src_id, { depth: prev ? Math.min(prev.depth, d) : d, conf: c, via: id, kind: r.kind, sites: prev?.via === id ? prev.sites : [{ path: r.path, line: r.line }] });
555
+ if (!prev) next.push({ id: r.src_id, conf: c });
556
+ }
557
+ }
558
+ frontier = next;
559
+ }
560
+ for (const s of startIds) seen.delete(s);
561
+ return { nodes: seen, fileLevel, truncated: overflow.size > 0, overflow: overflow.size };
562
+ }
563
+
564
+ /** Members this method overrides/implements (up) and members overriding it (down). */
565
+ methodFamily(sym) {
566
+ const load = (list) => list.map(x => this.sym(x.id)).filter(Boolean);
567
+ return { up: load(this.#supertypeMembers(sym)), down: load(this.#subtypeMembers(sym)) };
568
+ }
569
+
570
+ /** Unbound same-name call sites that may be uses of a symbol (see #unboundSameName). */
571
+ unboundFor(id, opts = {}) {
572
+ const sym = this.sym(id);
573
+ if (!sym) return { total: 0, plausible: [], typeNames: [] };
574
+ return this.#unboundSameName(sym, [id, ...this.overloads(sym)], opts);
575
+ }
576
+
577
+ // ── impact ───────────────────────────────────────────────────────────────────
578
+ /** Symbols overlapping the working-tree diff (git), as impact seeds. */
579
+ diffSeeds(base = 'HEAD') {
580
+ if (!isRevision(base)) return { error: `Not a git revision: ${base}`, seeds: [], files: [] };
581
+ const r = gitSync(this.root, ['diff', '-U0', '--no-color', base, '--'], { maxBuffer: 1 << 28 });
582
+ if (r.status !== 0) return { error: (r.stderr || 'git diff failed').trim(), seeds: [], files: [] };
583
+ const changes = new Map();
584
+ let file = null;
585
+ for (const line of r.stdout.split('\n')) {
586
+ if (line.startsWith('+++ ')) { file = line.slice(4).replace(/^b\//, ''); if (file === '/dev/null') file = null; continue; }
587
+ const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
588
+ if (h && file) {
589
+ const start = Number(h[1]), len = h[2] === undefined ? 1 : Number(h[2]);
590
+ (changes.get(file) ?? changes.set(file, []).get(file)).push([start, Math.max(start, start + len - 1)]);
591
+ }
592
+ }
593
+ const untracked = gitSync(this.root, ['ls-files', '--others', '--exclude-standard']);
594
+ for (const f of (untracked.stdout || '').split('\n').filter(Boolean)) if (!changes.has(f)) changes.set(f, [[1, 1e9]]);
595
+ const seeds = new Set();
596
+ for (const [f, ranges] of changes) {
597
+ for (const [a, b] of ranges) {
598
+ const rows = this.store.all(`SELECT s.id, s.start_line, s.end_line, s.kind FROM symbols s JOIN files fl ON fl.id = s.file_id WHERE fl.path = ? AND s.start_line <= ? AND s.end_line >= ?`, f, b, a);
599
+ // innermost symbols only
600
+ const inner = rows.filter(x => !rows.some(y => y.id !== x.id && y.start_line >= x.start_line && y.end_line <= x.end_line && (y.end_line - y.start_line) < (x.end_line - x.start_line)));
601
+ for (const x of inner) seeds.add(x.id);
602
+ }
603
+ }
604
+ return { seeds: [...seeds], files: [...changes.keys()] };
605
+ }
606
+
607
+ /** Files that historically change together with the given files (git log, bounded). */
608
+ coChange(files, { commits = 400, limit = 8 } = {}) {
609
+ if (!this._cochange || this._cochangeAt < Date.now() - 600_000) {
610
+ const r = gitSync(this.root, ['log', `-n${commits}`, '--name-only', '--pretty=format:%x00'], { maxBuffer: 1 << 28 });
611
+ const counts = new Map(); const pair = new Map();
612
+ if (r.status === 0) {
613
+ for (const block of r.stdout.split('\0')) {
614
+ const fs_ = [...new Set(block.split('\n').map(s => s.trim()).filter(Boolean))];
615
+ if (fs_.length < 2 || fs_.length > 40) { for (const f of fs_) counts.set(f, (counts.get(f) ?? 0) + 1); continue; }
616
+ for (const f of fs_) counts.set(f, (counts.get(f) ?? 0) + 1);
617
+ for (const a of fs_) for (const b of fs_) if (a !== b) { const k = a + '\0' + b; pair.set(k, (pair.get(k) ?? 0) + 1); }
618
+ }
619
+ }
620
+ this._cochange = { counts, pair };
621
+ this._cochangeAt = Date.now();
622
+ }
623
+ const { counts, pair } = this._cochange;
624
+ const agg = new Map();
625
+ for (const f of files) {
626
+ const n = counts.get(f) ?? 0;
627
+ if (!n) continue;
628
+ for (const [k, v] of pair) {
629
+ if (!k.startsWith(f + '\0')) continue;
630
+ const other = k.slice(f.length + 1);
631
+ if (files.includes(other) || v < 2) continue;
632
+ const score = v / n;
633
+ const prev = agg.get(other);
634
+ if (!prev || prev.score < score) agg.set(other, { file: other, together: v, of: n, score });
635
+ }
636
+ }
637
+ return [...agg.values()].sort((a, b) => b.score - a.score || b.together - a.together).slice(0, limit);
638
+ }
639
+
640
+ /** Tests related to files by naming convention (foo.ts ↔ foo.test.ts / test_foo.py / foo_test.go). */
641
+ conventionTests(files) {
642
+ const out = new Set();
643
+ for (const f of files) {
644
+ const base = path.posix.basename(f).replace(/\.[^.]+$/, '');
645
+ const stem = base.replace(/\.(test|spec)$/, '');
646
+ const rows = this.store.all(`SELECT path FROM files WHERE is_test = 1 AND (path LIKE ? OR path LIKE ? OR path LIKE ? OR path LIKE ? OR path LIKE ?)`,
647
+ `%/${stem}.test.%`, `%/${stem}.spec.%`, `%test_${stem}.%`, `%/${stem}_test.%`, `%/${stem}Test.%`);
648
+ for (const r of rows) out.add(r.path);
649
+ }
650
+ return [...out];
651
+ }
652
+
653
+ // ── maps ─────────────────────────────────────────────────────────────────────
654
+ /** Symbols ranked by (personalized) centrality, grouped by file. */
655
+ rankedSymbols({ focusIds = null, pathPrefix = null, limit = 400 } = {}) {
656
+ let scores;
657
+ if (focusIds?.length) {
658
+ const pers = new Map(focusIds.map(id => [id, 1]));
659
+ scores = computeCentrality(this.store, { personalization: pers, graph: loadGraph(this.store) });
660
+ } else scores = this.centrality();
661
+ const rows = this.store.all(`SELECT s.id, s.name, s.qname, s.kind, s.parent_id, s.start_line, s.end_line, s.sig, s.exported, f.path, f.is_test
662
+ FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.kind NOT IN ('variable','field','property','constant','selector','keyframes')`);
663
+ const filtered = rows.filter(r => !r.is_test && (!pathPrefix || r.path.startsWith(pathPrefix)));
664
+ filtered.sort((a, b) => (scores.get(b.id) ?? 0) - (scores.get(a.id) ?? 0) || a.id - b.id);
665
+ return filtered.slice(0, limit).map(r => ({ ...r, score: scores.get(r.id) ?? 0 }));
666
+ }
667
+
668
+ stats() {
669
+ const c = (sql) => this.store.get(sql)?.n ?? 0;
670
+ const langs = this.store.all('SELECT lang, COUNT(*) AS n FROM files GROUP BY lang ORDER BY n DESC');
671
+ return {
672
+ root: this.root,
673
+ files: c('SELECT COUNT(*) n FROM files'),
674
+ symbols: c('SELECT COUNT(*) n FROM symbols'),
675
+ refs: c('SELECT COUNT(*) n FROM refs'),
676
+ resolved: c('SELECT COUNT(*) n FROM refs WHERE dst_id IS NOT NULL'),
677
+ exact: c('SELECT COUNT(*) n FROM refs WHERE conf >= 0.9'),
678
+ langs,
679
+ lastSync: this.lastSyncStats,
680
+ watcher: this.watcherHealthy,
681
+ };
682
+ }
683
+ }