milens 0.6.3 → 0.6.4
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/.agents/skills/adapters/SKILL.md +31 -0
- package/.agents/skills/analyzer/SKILL.md +55 -0
- package/.agents/skills/apps/SKILL.md +42 -0
- package/.agents/skills/docs/SKILL.md +46 -0
- package/.agents/skills/milens/SKILL.md +168 -0
- package/.agents/skills/milens-code-review/SKILL.md +186 -0
- package/.agents/skills/milens-eval/SKILL.md +221 -0
- package/.agents/skills/milens-plan/SKILL.md +227 -0
- package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
- package/.agents/skills/milens-security-review/SKILL.md +224 -0
- package/.agents/skills/milens-tdd/SKILL.md +156 -0
- package/.agents/skills/parser/SKILL.md +60 -0
- package/.agents/skills/root/SKILL.md +64 -0
- package/.agents/skills/scripts/SKILL.md +27 -0
- package/.agents/skills/security/SKILL.md +44 -0
- package/.agents/skills/server/SKILL.md +46 -0
- package/.agents/skills/store/SKILL.md +53 -0
- package/.agents/skills/test/SKILL.md +73 -0
- package/LICENSE +75 -75
- package/README.md +508 -432
- package/adapters/README.md +107 -0
- package/adapters/claude-code/.claude/mcp.json +9 -0
- package/adapters/claude-code/CLAUDE.md +58 -0
- package/adapters/codex/.codex/codex.md +52 -0
- package/adapters/copilot/.github/copilot-instructions.md +62 -0
- package/adapters/cursor/.cursorrules +9 -0
- package/adapters/gemini/.gemini/context.md +58 -0
- package/adapters/opencode/.opencode/config.json +9 -0
- package/adapters/opencode/AGENTS.md +58 -0
- package/adapters/zed/.zed/settings.json +8 -0
- package/dist/agents-md.d.ts +3 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +112 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/analyzer/engine.js +1 -1
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/cli.js +1190 -401
- package/dist/cli.js.map +1 -1
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +64 -0
- package/dist/metrics.js.map +1 -0
- package/dist/parser/lang-go.js +47 -47
- package/dist/parser/lang-java.js +29 -29
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +38 -38
- package/dist/parser/lang-py.js +34 -34
- package/dist/parser/lang-ruby.js +14 -14
- package/dist/parser/lang-rust.js +30 -30
- package/dist/parser/lang-ts.js +191 -191
- package/dist/security/deps.d.ts +38 -0
- package/dist/security/deps.d.ts.map +1 -0
- package/dist/security/deps.js +685 -0
- package/dist/security/deps.js.map +1 -0
- package/dist/security/rules.d.ts +42 -0
- package/dist/security/rules.d.ts.map +1 -0
- package/dist/security/rules.js +940 -0
- package/dist/security/rules.js.map +1 -0
- package/dist/server/hooks.d.ts +26 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +253 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/mcp-prompts.d.ts +277 -0
- package/dist/server/mcp-prompts.d.ts.map +1 -0
- package/dist/server/mcp-prompts.js +627 -0
- package/dist/server/mcp-prompts.js.map +1 -0
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +618 -643
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/test-plan.d.ts +20 -0
- package/dist/server/test-plan.d.ts.map +1 -0
- package/dist/server/test-plan.js +100 -0
- package/dist/server/test-plan.js.map +1 -0
- package/dist/skills.js +152 -152
- package/dist/store/annotations.d.ts +41 -0
- package/dist/store/annotations.d.ts.map +1 -0
- package/dist/store/annotations.js +192 -0
- package/dist/store/annotations.js.map +1 -0
- package/dist/store/confidence.d.ts +18 -0
- package/dist/store/confidence.d.ts.map +1 -0
- package/dist/store/confidence.js +82 -0
- package/dist/store/confidence.js.map +1 -0
- package/dist/store/db.d.ts +37 -14
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +332 -239
- package/dist/store/db.js.map +1 -1
- package/dist/store/schema.sql +128 -116
- package/dist/store/vectors.js +2 -2
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +24 -0
- package/package.json +80 -66
- package/dist/gateway/analyzer.d.ts +0 -6
- package/dist/gateway/analyzer.d.ts.map +0 -1
- package/dist/gateway/analyzer.js +0 -218
- package/dist/gateway/analyzer.js.map +0 -1
- package/dist/gateway/cache.d.ts +0 -35
- package/dist/gateway/cache.d.ts.map +0 -1
- package/dist/gateway/cache.js +0 -175
- package/dist/gateway/cache.js.map +0 -1
- package/dist/gateway/config.d.ts +0 -10
- package/dist/gateway/config.d.ts.map +0 -1
- package/dist/gateway/config.js +0 -167
- package/dist/gateway/config.js.map +0 -1
- package/dist/gateway/context-memory.d.ts +0 -68
- package/dist/gateway/context-memory.d.ts.map +0 -1
- package/dist/gateway/context-memory.js +0 -157
- package/dist/gateway/context-memory.js.map +0 -1
- package/dist/gateway/observability.d.ts +0 -83
- package/dist/gateway/observability.d.ts.map +0 -1
- package/dist/gateway/observability.js +0 -152
- package/dist/gateway/observability.js.map +0 -1
- package/dist/gateway/privacy.d.ts +0 -27
- package/dist/gateway/privacy.d.ts.map +0 -1
- package/dist/gateway/privacy.js +0 -139
- package/dist/gateway/privacy.js.map +0 -1
- package/dist/gateway/providers.d.ts +0 -66
- package/dist/gateway/providers.d.ts.map +0 -1
- package/dist/gateway/providers.js +0 -377
- package/dist/gateway/providers.js.map +0 -1
- package/dist/gateway/router.d.ts +0 -18
- package/dist/gateway/router.d.ts.map +0 -1
- package/dist/gateway/router.js +0 -102
- package/dist/gateway/router.js.map +0 -1
- package/dist/gateway/server.d.ts +0 -20
- package/dist/gateway/server.d.ts.map +0 -1
- package/dist/gateway/server.js +0 -387
- package/dist/gateway/server.js.map +0 -1
- package/dist/gateway/translator.d.ts +0 -19
- package/dist/gateway/translator.d.ts.map +0 -1
- package/dist/gateway/translator.js +0 -340
- package/dist/gateway/translator.js.map +0 -1
- package/dist/gateway/types.d.ts +0 -215
- package/dist/gateway/types.d.ts.map +0 -1
- package/dist/gateway/types.js +0 -3
- package/dist/gateway/types.js.map +0 -1
- package/dist/store/gateway-schema.sql +0 -53
package/dist/store/db.js
CHANGED
|
@@ -7,6 +7,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
export class Database {
|
|
8
8
|
db;
|
|
9
9
|
stmts;
|
|
10
|
+
/** Access the raw better-sqlite3 connection (for AnnotationStore etc.) */
|
|
11
|
+
get connection() {
|
|
12
|
+
return this.db;
|
|
13
|
+
}
|
|
10
14
|
constructor(dbPath) {
|
|
11
15
|
this.db = new BetterSqlite3(dbPath);
|
|
12
16
|
this.db.pragma('journal_mode = WAL');
|
|
@@ -21,48 +25,62 @@ export class Database {
|
|
|
21
25
|
prepareStatements() {
|
|
22
26
|
return {
|
|
23
27
|
checkHash: this.db.prepare('SELECT hash FROM file_hashes WHERE path = ?'),
|
|
24
|
-
upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
|
|
28
|
+
upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
|
|
25
29
|
ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, analyzed_at = datetime('now')`),
|
|
26
|
-
insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
|
|
30
|
+
insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
|
|
27
31
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
28
32
|
updateMeta: this.db.prepare(`UPDATE symbols SET role = ?, heat = ? WHERE id = ?`),
|
|
29
33
|
upsertZone: this.db.prepare(`UPDATE file_hashes SET zone = ? WHERE path = ?`),
|
|
30
|
-
insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
|
|
34
|
+
insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
|
|
31
35
|
VALUES (?, ?, ?, ?, ?, ?)`),
|
|
32
|
-
searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
|
|
33
|
-
JOIN symbols s ON s.rowid = f.rowid
|
|
34
|
-
WHERE symbol_fts MATCH ?
|
|
36
|
+
searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
|
|
37
|
+
JOIN symbols s ON s.rowid = f.rowid
|
|
38
|
+
WHERE symbol_fts MATCH ?
|
|
35
39
|
ORDER BY rank LIMIT ?`),
|
|
36
40
|
byName: this.db.prepare('SELECT * FROM symbols WHERE name = ?'),
|
|
37
41
|
byId: this.db.prepare('SELECT * FROM symbols WHERE id = ?'),
|
|
38
42
|
byFile: this.db.prepare('SELECT * FROM symbols WHERE file_path = ?'),
|
|
39
43
|
linksIn: this.db.prepare('SELECT * FROM links WHERE to_id = ?'),
|
|
40
44
|
linksOut: this.db.prepare('SELECT * FROM links WHERE from_id = ?'),
|
|
41
|
-
upstream: this.db.prepare(`
|
|
42
|
-
WITH RECURSIVE upstream(id, depth, via) AS (
|
|
43
|
-
SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
44
|
-
UNION
|
|
45
|
-
SELECT l.from_id, u.depth + 1, l.type
|
|
46
|
-
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
47
|
-
WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
48
|
-
)
|
|
49
|
-
SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
45
|
+
upstream: this.db.prepare(`
|
|
46
|
+
WITH RECURSIVE upstream(id, depth, via) AS (
|
|
47
|
+
SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
48
|
+
UNION
|
|
49
|
+
SELECT l.from_id, u.depth + 1, l.type
|
|
50
|
+
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
51
|
+
WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
52
|
+
)
|
|
53
|
+
SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
50
54
|
`),
|
|
51
|
-
downstream: this.db.prepare(`
|
|
52
|
-
WITH RECURSIVE downstream(id, depth, via) AS (
|
|
53
|
-
SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
54
|
-
UNION
|
|
55
|
-
SELECT l.to_id, d.depth + 1, l.type
|
|
56
|
-
FROM links l JOIN downstream d ON l.from_id = d.id
|
|
57
|
-
WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
58
|
-
)
|
|
59
|
-
SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
55
|
+
downstream: this.db.prepare(`
|
|
56
|
+
WITH RECURSIVE downstream(id, depth, via) AS (
|
|
57
|
+
SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
58
|
+
UNION
|
|
59
|
+
SELECT l.to_id, d.depth + 1, l.type
|
|
60
|
+
FROM links l JOIN downstream d ON l.from_id = d.id
|
|
61
|
+
WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
62
|
+
)
|
|
63
|
+
SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
60
64
|
`),
|
|
61
65
|
countSymbols: this.db.prepare('SELECT COUNT(*) as c FROM symbols'),
|
|
62
66
|
countLinks: this.db.prepare('SELECT COUNT(*) as c FROM links'),
|
|
63
67
|
countFiles: this.db.prepare('SELECT COUNT(*) as c FROM file_hashes'),
|
|
64
|
-
deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?)
|
|
68
|
+
deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?)'),
|
|
65
69
|
deleteFileSymbols: this.db.prepare('DELETE FROM symbols WHERE file_path = ?'),
|
|
70
|
+
topHubs: this.db.prepare('SELECT * FROM symbols WHERE exported = 1 ORDER BY heat DESC LIMIT ?'),
|
|
71
|
+
testCoverageGaps: this.db.prepare(`
|
|
72
|
+
SELECT s.* FROM symbols s
|
|
73
|
+
WHERE s.exported = 1 AND s.heat > 0
|
|
74
|
+
AND s.id NOT IN (
|
|
75
|
+
SELECT DISTINCT l.to_id FROM links l
|
|
76
|
+
JOIN symbols src ON src.id = l.from_id
|
|
77
|
+
WHERE (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
78
|
+
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
79
|
+
)
|
|
80
|
+
ORDER BY s.heat DESC
|
|
81
|
+
LIMIT ?
|
|
82
|
+
`),
|
|
83
|
+
annotationCount: this.db.prepare('SELECT COUNT(*) as c FROM annotations'),
|
|
66
84
|
};
|
|
67
85
|
}
|
|
68
86
|
applySchema() {
|
|
@@ -153,35 +171,6 @@ export class Database {
|
|
|
153
171
|
const rows = this.stmts.linksOut.all(symbolId);
|
|
154
172
|
return rows.map(rowToLink);
|
|
155
173
|
}
|
|
156
|
-
/** Batch link counts for multiple symbol IDs (avoids N+1 queries) */
|
|
157
|
-
getLinkCountsForSymbols(symbolIds) {
|
|
158
|
-
if (symbolIds.length === 0)
|
|
159
|
-
return new Map();
|
|
160
|
-
const placeholders = symbolIds.map(() => '?').join(',');
|
|
161
|
-
const inRows = this.db.prepare(`SELECT to_id as id, COUNT(*) as c FROM links WHERE to_id IN (${placeholders}) AND type != 'contains' GROUP BY to_id`).all(...symbolIds);
|
|
162
|
-
const outRows = this.db.prepare(`SELECT from_id as id, COUNT(*) as c FROM links WHERE from_id IN (${placeholders}) AND type != 'contains' GROUP BY from_id`).all(...symbolIds);
|
|
163
|
-
const result = new Map();
|
|
164
|
-
for (const id of symbolIds)
|
|
165
|
-
result.set(id, { incoming: 0, outgoing: 0 });
|
|
166
|
-
for (const r of inRows)
|
|
167
|
-
result.get(r.id).incoming = r.c;
|
|
168
|
-
for (const r of outRows)
|
|
169
|
-
result.get(r.id).outgoing = r.c;
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
/** Batch: get symbol IDs that have at least one incoming link from a test file. */
|
|
173
|
-
getTestedSymbolIds(symbolIds, isTestFile) {
|
|
174
|
-
if (symbolIds.length === 0)
|
|
175
|
-
return new Set();
|
|
176
|
-
const placeholders = symbolIds.map(() => '?').join(',');
|
|
177
|
-
const rows = this.db.prepare(`SELECT DISTINCT l.to_id, s.file_path FROM links l JOIN symbols s ON s.id = l.from_id WHERE l.to_id IN (${placeholders})`).all(...symbolIds);
|
|
178
|
-
const tested = new Set();
|
|
179
|
-
for (const r of rows) {
|
|
180
|
-
if (isTestFile(r.file_path))
|
|
181
|
-
tested.add(r.to_id);
|
|
182
|
-
}
|
|
183
|
-
return tested;
|
|
184
|
-
}
|
|
185
174
|
findUpstream(symbolId, maxDepth = 3) {
|
|
186
175
|
const rows = this.stmts.upstream.all(symbolId, maxDepth);
|
|
187
176
|
return rows.map(r => ({ symbol: rowToSymbol(r), depth: r.depth, via: r.via }));
|
|
@@ -200,47 +189,25 @@ export class Database {
|
|
|
200
189
|
const rows = this.db.prepare('SELECT * FROM symbols').all();
|
|
201
190
|
return rows.map(rowToSymbol);
|
|
202
191
|
}
|
|
203
|
-
getTopSymbolsByHeat(limit = 10) {
|
|
204
|
-
const rows = this.db.prepare(`SELECT * FROM symbols WHERE heat > 0 AND kind != 'section' ORDER BY heat DESC LIMIT ?`).all(limit);
|
|
205
|
-
return rows.map(rowToSymbol);
|
|
206
|
-
}
|
|
207
192
|
getAllLinks() {
|
|
208
193
|
const rows = this.db.prepare('SELECT * FROM links').all();
|
|
209
194
|
return rows.map(rowToLink);
|
|
210
195
|
}
|
|
211
196
|
findDeadCode(kind, limit = 50) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
AND s.file_path NOT LIKE 'test/fixtures/%'
|
|
217
|
-
AND s.file_path NOT LIKE '%/page.ts' AND s.file_path NOT LIKE '%/page.tsx'
|
|
218
|
-
AND s.file_path NOT LIKE '%/page.js' AND s.file_path NOT LIKE '%/page.jsx'
|
|
219
|
-
AND s.file_path NOT LIKE '%/layout.ts' AND s.file_path NOT LIKE '%/layout.tsx'
|
|
220
|
-
AND s.file_path NOT LIKE '%/layout.js' AND s.file_path NOT LIKE '%/layout.jsx'
|
|
221
|
-
AND s.file_path NOT LIKE '%/loading.ts' AND s.file_path NOT LIKE '%/loading.tsx'
|
|
222
|
-
AND s.file_path NOT LIKE '%/error.ts' AND s.file_path NOT LIKE '%/error.tsx'
|
|
223
|
-
AND s.file_path NOT LIKE '%/not-found.ts' AND s.file_path NOT LIKE '%/not-found.tsx'
|
|
224
|
-
AND s.file_path NOT LIKE '%/template.ts' AND s.file_path NOT LIKE '%/template.tsx'
|
|
225
|
-
AND s.file_path NOT LIKE '%/route.ts' AND s.file_path NOT LIKE '%/route.tsx'
|
|
226
|
-
AND s.file_path NOT LIKE '%/route.js' AND s.file_path NOT LIKE '%/route.jsx'
|
|
227
|
-
AND s.file_path NOT LIKE '%/default.ts' AND s.file_path NOT LIKE '%/default.tsx'
|
|
228
|
-
AND s.file_path NOT LIKE '%/global-error.ts' AND s.file_path NOT LIKE '%/global-error.tsx'
|
|
229
|
-
AND s.file_path NOT LIKE '%/middleware.ts' AND s.file_path NOT LIKE '%/middleware.js'
|
|
230
|
-
AND s.file_path NOT LIKE '%.config.ts' AND s.file_path NOT LIKE '%.config.js'
|
|
231
|
-
AND s.file_path NOT LIKE '%.config.mjs' AND s.file_path NOT LIKE '%.config.cjs'
|
|
232
|
-
AND s.file_path NOT LIKE '%/+page.svelte' AND s.file_path NOT LIKE '%/+page.ts'
|
|
233
|
-
AND s.file_path NOT LIKE '%/+page.server.ts' AND s.file_path NOT LIKE '%/+layout.svelte'
|
|
234
|
-
AND s.file_path NOT LIKE '%/+layout.ts' AND s.file_path NOT LIKE '%/+layout.server.ts'
|
|
235
|
-
AND s.file_path NOT LIKE '%/+server.ts'`;
|
|
197
|
+
const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE 'app/%/layout.%'
|
|
198
|
+
AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE 'app/layout.%'
|
|
199
|
+
AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE 'jest.config.%'
|
|
200
|
+
AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE 'src/routes/+layout.%'`;
|
|
236
201
|
const sql = kind
|
|
237
|
-
? `SELECT s.* FROM symbols s
|
|
238
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
239
|
-
WHERE s.exported = 1 AND s.kind = ?
|
|
202
|
+
? `SELECT s.* FROM symbols s
|
|
203
|
+
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
204
|
+
WHERE s.exported = 1 AND s.kind = ? AND l.id IS NULL
|
|
205
|
+
${frameworkExclude}
|
|
240
206
|
LIMIT ?`
|
|
241
|
-
: `SELECT s.* FROM symbols s
|
|
242
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
243
|
-
WHERE s.exported = 1
|
|
207
|
+
: `SELECT s.* FROM symbols s
|
|
208
|
+
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
209
|
+
WHERE s.exported = 1 AND l.id IS NULL
|
|
210
|
+
${frameworkExclude}
|
|
244
211
|
LIMIT ?`;
|
|
245
212
|
const rows = kind
|
|
246
213
|
? this.db.prepare(sql).all(kind, limit)
|
|
@@ -248,25 +215,25 @@ export class Database {
|
|
|
248
215
|
return rows.map(rowToSymbol);
|
|
249
216
|
}
|
|
250
217
|
getTypeHierarchy(symbolId) {
|
|
251
|
-
const ancestors = this.db.prepare(`
|
|
252
|
-
WITH RECURSIVE up(id, depth) AS (
|
|
253
|
-
SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
|
|
254
|
-
UNION
|
|
255
|
-
SELECT l.to_id, u.depth + 1
|
|
256
|
-
FROM links l JOIN up u ON l.from_id = u.id
|
|
257
|
-
WHERE l.type IN ('extends', 'implements') AND u.depth < 10
|
|
258
|
-
)
|
|
259
|
-
SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
218
|
+
const ancestors = this.db.prepare(`
|
|
219
|
+
WITH RECURSIVE up(id, depth) AS (
|
|
220
|
+
SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
|
|
221
|
+
UNION
|
|
222
|
+
SELECT l.to_id, u.depth + 1
|
|
223
|
+
FROM links l JOIN up u ON l.from_id = u.id
|
|
224
|
+
WHERE l.type IN ('extends', 'implements') AND u.depth < 10
|
|
225
|
+
)
|
|
226
|
+
SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
260
227
|
`).all(symbolId);
|
|
261
|
-
const descendants = this.db.prepare(`
|
|
262
|
-
WITH RECURSIVE down(id, depth) AS (
|
|
263
|
-
SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
|
|
264
|
-
UNION
|
|
265
|
-
SELECT l.from_id, d.depth + 1
|
|
266
|
-
FROM links l JOIN down d ON l.to_id = d.id
|
|
267
|
-
WHERE l.type IN ('extends', 'implements') AND d.depth < 10
|
|
268
|
-
)
|
|
269
|
-
SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
228
|
+
const descendants = this.db.prepare(`
|
|
229
|
+
WITH RECURSIVE down(id, depth) AS (
|
|
230
|
+
SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
|
|
231
|
+
UNION
|
|
232
|
+
SELECT l.from_id, d.depth + 1
|
|
233
|
+
FROM links l JOIN down d ON l.to_id = d.id
|
|
234
|
+
WHERE l.type IN ('extends', 'implements') AND d.depth < 10
|
|
235
|
+
)
|
|
236
|
+
SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
270
237
|
`).all(symbolId);
|
|
271
238
|
return {
|
|
272
239
|
ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
|
|
@@ -280,40 +247,27 @@ export class Database {
|
|
|
280
247
|
return null;
|
|
281
248
|
const fromId = fromSyms[0].id;
|
|
282
249
|
const toIds = new Set(toSyms.map(s => s.id));
|
|
283
|
-
// BFS
|
|
284
|
-
const rows = this.db.prepare(`
|
|
285
|
-
WITH RECURSIVE path(id, depth, via
|
|
286
|
-
SELECT to_id, 1, type
|
|
287
|
-
UNION
|
|
288
|
-
SELECT l.to_id, p.depth + 1, l.type
|
|
289
|
-
FROM links l JOIN path p ON l.from_id = p.id
|
|
290
|
-
WHERE l.type != 'contains' AND p.depth < ?
|
|
291
|
-
)
|
|
292
|
-
SELECT s.*, p.depth, p.via
|
|
250
|
+
// BFS outgoing from source
|
|
251
|
+
const rows = this.db.prepare(`
|
|
252
|
+
WITH RECURSIVE path(id, depth, via) AS (
|
|
253
|
+
SELECT to_id, 1, type FROM links WHERE from_id = ? AND type != 'contains'
|
|
254
|
+
UNION
|
|
255
|
+
SELECT l.to_id, p.depth + 1, l.type
|
|
256
|
+
FROM links l JOIN path p ON l.from_id = p.id
|
|
257
|
+
WHERE l.type != 'contains' AND p.depth < ?
|
|
258
|
+
)
|
|
259
|
+
SELECT DISTINCT s.*, p.depth, p.via FROM path p JOIN symbols s ON s.id = p.id ORDER BY p.depth
|
|
293
260
|
`).all(fromId, maxDepth);
|
|
294
|
-
|
|
295
|
-
const targetRow = rows.find(r => toIds.has(r.id));
|
|
296
|
-
if (!targetRow)
|
|
297
|
-
return null;
|
|
298
|
-
// Build lookup: id → { row, parent_id }
|
|
299
|
-
const byId = new Map();
|
|
261
|
+
const result = [];
|
|
300
262
|
for (const r of rows) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
263
|
+
result.push({ symbol: rowToSymbol(r), depth: r.depth, via: r.via });
|
|
264
|
+
if (toIds.has(r.id))
|
|
265
|
+
break;
|
|
304
266
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
while (cur && cur.id !== fromId && !visited.has(cur.id)) {
|
|
310
|
-
visited.add(cur.id);
|
|
311
|
-
chain.push({ symbol: rowToSymbol(cur), depth: cur.depth, via: cur.via });
|
|
312
|
-
const parent = byId.get(cur.parent_id);
|
|
313
|
-
cur = parent ? parent.row : null;
|
|
314
|
-
}
|
|
315
|
-
chain.reverse();
|
|
316
|
-
return chain;
|
|
267
|
+
const found = result.find(r => toIds.has(r.symbol.id));
|
|
268
|
+
if (!found)
|
|
269
|
+
return null;
|
|
270
|
+
return result.filter(r => r.depth <= found.depth);
|
|
317
271
|
}
|
|
318
272
|
getChangedFiles() {
|
|
319
273
|
const rows = this.db.prepare('SELECT DISTINCT file_path FROM symbols').all();
|
|
@@ -321,7 +275,7 @@ export class Database {
|
|
|
321
275
|
}
|
|
322
276
|
// ── Maintenance ──
|
|
323
277
|
deleteFileData(filePath) {
|
|
324
|
-
this.stmts.deleteFileLinks.run(filePath
|
|
278
|
+
this.stmts.deleteFileLinks.run(filePath);
|
|
325
279
|
this.stmts.deleteFileSymbols.run(filePath);
|
|
326
280
|
}
|
|
327
281
|
rebuildSearch() {
|
|
@@ -355,14 +309,14 @@ export class Database {
|
|
|
355
309
|
};
|
|
356
310
|
}
|
|
357
311
|
getConfidenceDistribution() {
|
|
358
|
-
const rows = this.db.prepare(`
|
|
359
|
-
SELECT
|
|
360
|
-
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
361
|
-
SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
362
|
-
SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
|
|
363
|
-
COUNT(*) as total
|
|
364
|
-
FROM links
|
|
365
|
-
WHERE type != 'contains'
|
|
312
|
+
const rows = this.db.prepare(`
|
|
313
|
+
SELECT
|
|
314
|
+
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
315
|
+
SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
316
|
+
SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
|
|
317
|
+
COUNT(*) as total
|
|
318
|
+
FROM links
|
|
319
|
+
WHERE type != 'contains'
|
|
366
320
|
`).get();
|
|
367
321
|
return {
|
|
368
322
|
high: rows?.high ?? 0,
|
|
@@ -376,14 +330,6 @@ export class Database {
|
|
|
376
330
|
// Walk upstream following only 'calls' links to find paths from entrypoints
|
|
377
331
|
const paths = [];
|
|
378
332
|
const visited = new Set();
|
|
379
|
-
const symCache = new Map();
|
|
380
|
-
const cachedFindSymbol = (id) => {
|
|
381
|
-
if (symCache.has(id))
|
|
382
|
-
return symCache.get(id);
|
|
383
|
-
const sym = this.findSymbolById(id);
|
|
384
|
-
symCache.set(id, sym);
|
|
385
|
-
return sym;
|
|
386
|
-
};
|
|
387
333
|
const dfs = (currentId, currentPath, depth) => {
|
|
388
334
|
if (depth > maxDepth)
|
|
389
335
|
return;
|
|
@@ -391,7 +337,7 @@ export class Database {
|
|
|
391
337
|
return;
|
|
392
338
|
visited.add(currentId);
|
|
393
339
|
const incoming = this.getIncomingLinks(currentId).filter(l => l.type === 'calls' || l.type === 'imports');
|
|
394
|
-
const sym =
|
|
340
|
+
const sym = this.findSymbolById(currentId);
|
|
395
341
|
if (incoming.length === 0 && sym?.exported) {
|
|
396
342
|
// Reached an entrypoint — save this path
|
|
397
343
|
paths.push({ path: [...currentPath] });
|
|
@@ -399,10 +345,17 @@ export class Database {
|
|
|
399
345
|
return;
|
|
400
346
|
}
|
|
401
347
|
for (const link of incoming) {
|
|
402
|
-
const fromSym =
|
|
348
|
+
const fromSym = this.findSymbolById(link.fromId);
|
|
403
349
|
if (!fromSym)
|
|
404
350
|
continue;
|
|
405
|
-
|
|
351
|
+
// Skip module-level _top imports — go to their real callers
|
|
352
|
+
if (fromSym.name === '_top' && fromSym.kind === 'module') {
|
|
353
|
+
// Recurse from the _top module's incoming callers
|
|
354
|
+
dfs(link.fromId, [{ symbol: fromSym, via: link.type }, ...currentPath], depth + 1);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
dfs(link.fromId, [{ symbol: fromSym, via: link.type }, ...currentPath], depth + 1);
|
|
358
|
+
}
|
|
406
359
|
}
|
|
407
360
|
visited.delete(currentId);
|
|
408
361
|
};
|
|
@@ -416,34 +369,34 @@ export class Database {
|
|
|
416
369
|
// ── Route/endpoint detection via link patterns ──
|
|
417
370
|
getEntrypoints() {
|
|
418
371
|
// Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
|
|
419
|
-
const rows = this.db.prepare(`
|
|
420
|
-
SELECT s.* FROM symbols s
|
|
421
|
-
WHERE s.exported = 1
|
|
422
|
-
AND s.role = 'entrypoint'
|
|
423
|
-
ORDER BY s.heat DESC
|
|
424
|
-
LIMIT 50
|
|
372
|
+
const rows = this.db.prepare(`
|
|
373
|
+
SELECT s.* FROM symbols s
|
|
374
|
+
WHERE s.exported = 1
|
|
375
|
+
AND s.role = 'entrypoint'
|
|
376
|
+
ORDER BY s.heat DESC
|
|
377
|
+
LIMIT 50
|
|
425
378
|
`).all();
|
|
426
379
|
return rows.map(rowToSymbol);
|
|
427
380
|
}
|
|
428
381
|
// ── Domain clustering stats ──
|
|
429
382
|
getDomainStats() {
|
|
430
|
-
const rows = this.db.prepare(`
|
|
431
|
-
SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
|
|
432
|
-
COUNT(s.id) AS symbol_count
|
|
433
|
-
FROM file_hashes fh
|
|
434
|
-
LEFT JOIN symbols s ON s.file_path = fh.path
|
|
435
|
-
WHERE fh.zone IS NOT NULL
|
|
436
|
-
GROUP BY fh.zone
|
|
437
|
-
ORDER BY symbol_count DESC
|
|
383
|
+
const rows = this.db.prepare(`
|
|
384
|
+
SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
|
|
385
|
+
COUNT(s.id) AS symbol_count
|
|
386
|
+
FROM file_hashes fh
|
|
387
|
+
LEFT JOIN symbols s ON s.file_path = fh.path
|
|
388
|
+
WHERE fh.zone IS NOT NULL
|
|
389
|
+
GROUP BY fh.zone
|
|
390
|
+
ORDER BY symbol_count DESC
|
|
438
391
|
`).all();
|
|
439
392
|
return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
|
|
440
393
|
}
|
|
441
394
|
// ── Staleness detection ──
|
|
442
395
|
getStaleFiles(hoursOld = 24) {
|
|
443
|
-
const rows = this.db.prepare(`
|
|
444
|
-
SELECT path FROM file_hashes
|
|
445
|
-
WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
|
|
446
|
-
ORDER BY analyzed_at ASC
|
|
396
|
+
const rows = this.db.prepare(`
|
|
397
|
+
SELECT path FROM file_hashes
|
|
398
|
+
WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
|
|
399
|
+
ORDER BY analyzed_at ASC
|
|
447
400
|
`).all(hoursOld);
|
|
448
401
|
return rows.map((r) => r.path);
|
|
449
402
|
}
|
|
@@ -466,40 +419,46 @@ export class Database {
|
|
|
466
419
|
}
|
|
467
420
|
// ── Tool usage tracking ──
|
|
468
421
|
logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
|
|
469
|
-
this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
|
|
422
|
+
this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
|
|
470
423
|
VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
|
|
471
424
|
}
|
|
472
|
-
getToolUsageStats() {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
COALESCE(SUM(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
425
|
+
getToolUsageStats(repo) {
|
|
426
|
+
const repoFilter = repo ? `WHERE repo = ?` : '';
|
|
427
|
+
const repoParam = repo ? [repo] : [];
|
|
428
|
+
const totals = this.db.prepare(`
|
|
429
|
+
SELECT COUNT(*) as total_calls,
|
|
430
|
+
COALESCE(SUM(tokens_saved), 0) as total_saved,
|
|
431
|
+
COALESCE(SUM(tokens_out), 0) as total_out,
|
|
432
|
+
COALESCE(SUM(duration_ms), 0) as total_ms
|
|
433
|
+
FROM tool_usage
|
|
434
|
+
${repoFilter}
|
|
435
|
+
`).get(...repoParam);
|
|
436
|
+
const byTool = this.db.prepare(`
|
|
437
|
+
SELECT tool, COUNT(*) as calls,
|
|
438
|
+
COALESCE(SUM(tokens_saved), 0) as tokens_saved,
|
|
439
|
+
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
|
440
|
+
CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
|
|
441
|
+
FROM tool_usage
|
|
442
|
+
${repoFilter}
|
|
443
|
+
GROUP BY tool
|
|
444
|
+
ORDER BY calls DESC
|
|
445
|
+
`).all(...repoParam);
|
|
446
|
+
const byDay = this.db.prepare(`
|
|
447
|
+
SELECT date(called_at) as date, COUNT(*) as calls,
|
|
448
|
+
COALESCE(SUM(tokens_saved), 0) as tokens_saved
|
|
449
|
+
FROM tool_usage
|
|
450
|
+
${repoFilter}
|
|
451
|
+
GROUP BY date(called_at)
|
|
452
|
+
ORDER BY date DESC
|
|
453
|
+
LIMIT 30
|
|
454
|
+
`).all(...repoParam);
|
|
455
|
+
const recentCalls = this.db.prepare(`
|
|
456
|
+
SELECT tool, called_at, duration_ms, tokens_saved
|
|
457
|
+
FROM tool_usage
|
|
458
|
+
${repoFilter}
|
|
459
|
+
ORDER BY id DESC
|
|
460
|
+
LIMIT 50
|
|
461
|
+
`).all(...repoParam);
|
|
503
462
|
return {
|
|
504
463
|
totalCalls: totals.total_calls,
|
|
505
464
|
totalTokensSaved: totals.total_saved,
|
|
@@ -515,16 +474,168 @@ export class Database {
|
|
|
515
474
|
})),
|
|
516
475
|
};
|
|
517
476
|
}
|
|
518
|
-
// ──
|
|
477
|
+
// ── Test file detection ──
|
|
478
|
+
isTestFile(filePath) {
|
|
479
|
+
return /[/\\]test[/\\]/.test(filePath) || /\.(test|spec)\./.test(filePath);
|
|
480
|
+
}
|
|
481
|
+
// ── Heat / hubs ──
|
|
482
|
+
getTopHubs(limit) {
|
|
483
|
+
const rows = this.stmts.topHubs.all(limit);
|
|
484
|
+
return rows.map(rowToSymbol);
|
|
485
|
+
}
|
|
486
|
+
// ── Test coverage ──
|
|
487
|
+
getSymbolTestCoverage(symbolId) {
|
|
488
|
+
const incomingLinks = this.getIncomingLinks(symbolId);
|
|
489
|
+
for (const link of incomingLinks) {
|
|
490
|
+
const sourceSymbol = this.findSymbolById(link.fromId);
|
|
491
|
+
if (sourceSymbol && this.isTestFile(sourceSymbol.filePath)) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
getTestedSymbolIds(candidateIds, isTestFileFn) {
|
|
498
|
+
if (candidateIds.length === 0)
|
|
499
|
+
return new Set();
|
|
500
|
+
const placeholders = candidateIds.map(() => '?').join(',');
|
|
501
|
+
const rows = this.db.prepare(`
|
|
502
|
+
SELECT DISTINCT l.to_id
|
|
503
|
+
FROM links l JOIN symbols src ON src.id = l.from_id
|
|
504
|
+
WHERE l.to_id IN (${placeholders}) AND l.type != 'contains'
|
|
505
|
+
`).all(...candidateIds);
|
|
506
|
+
const testedIds = new Set();
|
|
507
|
+
for (const row of rows) {
|
|
508
|
+
const src = this.findSymbolById(row.to_id);
|
|
509
|
+
if (src && isTestFileFn(src.filePath))
|
|
510
|
+
continue;
|
|
511
|
+
// Check each from_id for test files
|
|
512
|
+
const links = this.getIncomingLinks(row.to_id);
|
|
513
|
+
for (const l of links) {
|
|
514
|
+
if (l.type === 'contains')
|
|
515
|
+
continue;
|
|
516
|
+
const from = this.findSymbolById(l.fromId);
|
|
517
|
+
if (from && isTestFileFn(from.filePath)) {
|
|
518
|
+
testedIds.add(row.to_id);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Also include ids not in the result set (no incoming links)
|
|
524
|
+
const inResult = new Set(rows.map(r => r.to_id));
|
|
525
|
+
for (const id of candidateIds) {
|
|
526
|
+
if (!inResult.has(id))
|
|
527
|
+
continue; // doesn't exist
|
|
528
|
+
}
|
|
529
|
+
return testedIds;
|
|
530
|
+
}
|
|
531
|
+
getTestCoverageGaps(limit) {
|
|
532
|
+
const rows = this.stmts.testCoverageGaps.all(limit);
|
|
533
|
+
return rows.map(rowToSymbol);
|
|
534
|
+
}
|
|
535
|
+
getTestImpact(changedSymbolIds) {
|
|
536
|
+
if (changedSymbolIds.length === 0)
|
|
537
|
+
return { testFiles: [], changedSymbols: [] };
|
|
538
|
+
const placeholders = changedSymbolIds.map(() => '?').join(',');
|
|
539
|
+
const testFilesDirect = this.db.prepare(`
|
|
540
|
+
SELECT DISTINCT src.file_path
|
|
541
|
+
FROM links l
|
|
542
|
+
JOIN symbols src ON src.id = l.from_id
|
|
543
|
+
WHERE l.to_id IN (${placeholders})
|
|
544
|
+
AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
545
|
+
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
546
|
+
`).all(...changedSymbolIds);
|
|
547
|
+
const testFilesUpstream = this.db.prepare(`
|
|
548
|
+
WITH RECURSIVE upstream(id, depth) AS (
|
|
549
|
+
SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
|
|
550
|
+
AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
551
|
+
UNION
|
|
552
|
+
SELECT l.from_id, u.depth + 1
|
|
553
|
+
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
554
|
+
WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
|
|
555
|
+
)
|
|
556
|
+
SELECT DISTINCT s.file_path FROM upstream u
|
|
557
|
+
JOIN symbols s ON s.id = u.id
|
|
558
|
+
WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
|
|
559
|
+
OR s.file_path LIKE '%.test.%' OR s.file_path LIKE '%.spec.%')
|
|
560
|
+
`).all(...changedSymbolIds);
|
|
561
|
+
const allTestFiles = [...new Set([
|
|
562
|
+
...testFilesDirect.map((r) => r.file_path),
|
|
563
|
+
...testFilesUpstream.map((r) => r.file_path),
|
|
564
|
+
])];
|
|
565
|
+
const changedSymbols = changedSymbolIds.filter(id => this.getSymbolTestCoverage(id));
|
|
566
|
+
return { testFiles: allTestFiles, changedSymbols };
|
|
567
|
+
}
|
|
568
|
+
// ── Codebase summary ──
|
|
569
|
+
getCodebaseSummary() {
|
|
570
|
+
const stats = this.getStats();
|
|
571
|
+
const coverage = this.getTestCoverage();
|
|
572
|
+
const domains = this.getDomainStats();
|
|
573
|
+
const topHubs = this.getTopHubs(10);
|
|
574
|
+
const coveragePct = coverage.exportedProductionSymbols > 0
|
|
575
|
+
? Math.round((coverage.testedSymbols / coverage.exportedProductionSymbols) * 100)
|
|
576
|
+
: 0;
|
|
577
|
+
return {
|
|
578
|
+
symbols: stats.symbols,
|
|
579
|
+
links: stats.links,
|
|
580
|
+
files: stats.files,
|
|
581
|
+
coveragePct,
|
|
582
|
+
testedSymbols: coverage.testedSymbols,
|
|
583
|
+
exportedSymbols: coverage.exportedProductionSymbols,
|
|
584
|
+
domains,
|
|
585
|
+
topHubs,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// ── Topological similarity ──
|
|
589
|
+
findTopologicallySimilar(symbolId, limit = 10) {
|
|
590
|
+
const target = this.findSymbolById(symbolId);
|
|
591
|
+
if (!target)
|
|
592
|
+
return [];
|
|
593
|
+
const incoming = this.getIncomingLinks(symbolId).filter(l => l.type !== 'contains');
|
|
594
|
+
const outgoing = this.getOutgoingLinks(symbolId).filter(l => l.type !== 'contains');
|
|
595
|
+
const targetLinks = new Set();
|
|
596
|
+
for (const link of incoming)
|
|
597
|
+
targetLinks.add(link.fromId);
|
|
598
|
+
for (const link of outgoing)
|
|
599
|
+
targetLinks.add(link.toId);
|
|
600
|
+
const siblings = this.getSymbolsByFile(target.filePath).filter(s => s.id !== symbolId);
|
|
601
|
+
const results = [];
|
|
602
|
+
for (const sibling of siblings) {
|
|
603
|
+
const sibIncoming = this.getIncomingLinks(sibling.id).filter(l => l.type !== 'contains');
|
|
604
|
+
const sibOutgoing = this.getOutgoingLinks(sibling.id).filter(l => l.type !== 'contains');
|
|
605
|
+
const siblingLinks = new Set();
|
|
606
|
+
for (const link of sibIncoming)
|
|
607
|
+
siblingLinks.add(link.fromId);
|
|
608
|
+
for (const link of sibOutgoing)
|
|
609
|
+
siblingLinks.add(link.toId);
|
|
610
|
+
let intersection = 0;
|
|
611
|
+
for (const id of targetLinks) {
|
|
612
|
+
if (siblingLinks.has(id))
|
|
613
|
+
intersection++;
|
|
614
|
+
}
|
|
615
|
+
const union = new Set([...targetLinks, ...siblingLinks]).size;
|
|
616
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
617
|
+
if (similarity >= 0.3) {
|
|
618
|
+
results.push({ symbol: sibling, similarity: Math.round(similarity * 100) / 100 });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
622
|
+
}
|
|
623
|
+
// ── Annotations ──
|
|
624
|
+
getAnnotationCount() {
|
|
625
|
+
const row = this.stmts.annotationCount.get();
|
|
626
|
+
return row.c;
|
|
627
|
+
}
|
|
628
|
+
// ── Annotation/session API ──
|
|
629
|
+
getRawDb() { return this.db; }
|
|
519
630
|
addAnnotation(symbolId, key, value, agent, sessionId, ttlHours) {
|
|
520
631
|
const expiresAt = ttlHours
|
|
521
632
|
? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
|
|
522
633
|
: null;
|
|
523
|
-
const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
|
|
634
|
+
const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
|
|
524
635
|
VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
|
|
525
636
|
return result.lastInsertRowid;
|
|
526
637
|
}
|
|
527
|
-
getAnnotations(filters) {
|
|
638
|
+
getAnnotations(filters = {}) {
|
|
528
639
|
const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
|
|
529
640
|
const params = [];
|
|
530
641
|
if (filters.symbolId) {
|
|
@@ -543,47 +654,29 @@ export class Database {
|
|
|
543
654
|
clauses.push('session_id = ?');
|
|
544
655
|
params.push(filters.sessionId);
|
|
545
656
|
}
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
FROM annotations ${where}
|
|
550
|
-
ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
551
|
-
return rows.map(r => ({
|
|
552
|
-
id: r.id,
|
|
553
|
-
symbolId: r.symbol_id,
|
|
554
|
-
key: r.key,
|
|
555
|
-
value: r.value,
|
|
556
|
-
agent: r.agent,
|
|
557
|
-
sessionId: r.session_id,
|
|
558
|
-
createdAt: r.created_at,
|
|
559
|
-
}));
|
|
657
|
+
const sql = `SELECT *, symbol_id as symbolId, created_at as createdAt FROM annotations WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC` + (filters.limit ? ` LIMIT ${filters.limit}` : '');
|
|
658
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
659
|
+
return rows.map((r) => ({ id: r.id, symbolId: r.symbol_id, key: r.key, value: r.value, agent: r.agent, sessionId: r.session_id, createdAt: r.created_at }));
|
|
560
660
|
}
|
|
561
661
|
getAnnotationsForSymbol(symbolId) {
|
|
562
|
-
|
|
563
|
-
WHERE symbol_id = ? AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
564
|
-
ORDER BY created_at DESC`).all(symbolId);
|
|
565
|
-
return rows.map(r => ({ key: r.key, value: r.value, agent: r.agent, createdAt: r.created_at }));
|
|
662
|
+
return this.getAnnotations({ symbolId });
|
|
566
663
|
}
|
|
567
664
|
cleanupExpiredAnnotations() {
|
|
568
|
-
const result = this.db.prepare(
|
|
665
|
+
const result = this.db.prepare("DELETE FROM annotations WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')").run();
|
|
569
666
|
return result.changes;
|
|
570
667
|
}
|
|
571
|
-
// ── Agent sessions ──
|
|
572
668
|
startSession(id, agent, context) {
|
|
573
|
-
this.db.prepare(
|
|
669
|
+
this.db.prepare('INSERT OR REPLACE INTO agent_sessions (id, agent, status, started_at, context_json) VALUES (?, ?, ?, datetime(\'now\'), ?)')
|
|
670
|
+
.run(id, agent, 'active', context ?? null);
|
|
574
671
|
}
|
|
575
672
|
getSession(id) {
|
|
576
|
-
const row = this.db.prepare('SELECT
|
|
673
|
+
const row = this.db.prepare('SELECT *, started_at as startedAt, ended_at as endedAt, context_json as context FROM agent_sessions WHERE id = ?').get(id);
|
|
577
674
|
if (!row)
|
|
578
675
|
return null;
|
|
579
|
-
return { id: row.id, agent: row.agent,
|
|
676
|
+
return { id: row.id, agent: row.agent, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, context: row.context };
|
|
580
677
|
}
|
|
581
|
-
endSession(id, status
|
|
582
|
-
this.db.prepare(
|
|
583
|
-
}
|
|
584
|
-
/** Expose raw handle for subsystems (e.g. EmbeddingStore) that need direct access. */
|
|
585
|
-
getRawDb() {
|
|
586
|
-
return this.db;
|
|
678
|
+
endSession(id, status) {
|
|
679
|
+
this.db.prepare("UPDATE agent_sessions SET status = ?, ended_at = datetime('now') WHERE id = ?").run(status, id);
|
|
587
680
|
}
|
|
588
681
|
transaction(fn) {
|
|
589
682
|
return this.db.transaction(fn)();
|