milens 0.6.2 → 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 +524 -305
- 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.d.ts +1 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +27 -8
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/review.d.ts +23 -0
- package/dist/analyzer/review.d.ts.map +1 -0
- package/dist/analyzer/review.js +143 -0
- package/dist/analyzer/review.js.map +1 -0
- package/dist/analyzer/testplan.d.ts +59 -0
- package/dist/analyzer/testplan.d.ts.map +1 -0
- package/dist/analyzer/testplan.js +218 -0
- package/dist/analyzer/testplan.js.map +1 -0
- package/dist/cli.js +1192 -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/extract.d.ts +1 -0
- package/dist/parser/extract.d.ts.map +1 -1
- package/dist/parser/extract.js +8 -0
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-go.d.ts.map +1 -1
- package/dist/parser/lang-go.js +75 -39
- package/dist/parser/lang-go.js.map +1 -1
- package/dist/parser/lang-java.d.ts.map +1 -1
- package/dist/parser/lang-java.js +30 -29
- package/dist/parser/lang-java.js.map +1 -1
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +38 -38
- package/dist/parser/lang-py.d.ts.map +1 -1
- package/dist/parser/lang-py.js +53 -31
- package/dist/parser/lang-py.js.map +1 -1
- package/dist/parser/lang-ruby.d.ts.map +1 -1
- package/dist/parser/lang-ruby.js +15 -14
- package/dist/parser/lang-ruby.js.map +1 -1
- 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 +520 -36
- 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 -120
- package/dist/skills.js.map +1 -1
- 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 +68 -1
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +349 -139
- package/dist/store/db.js.map +1 -1
- package/dist/store/schema.sql +128 -83
- package/dist/store/vectors.d.ts +65 -0
- package/dist/store/vectors.d.ts.map +1 -0
- package/dist/store/vectors.js +212 -0
- package/dist/store/vectors.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/docs/README.md +24 -0
- package/docs/diagram2.svg +1 -1
- package/package.json +80 -65
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
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() {
|
|
@@ -176,38 +194,20 @@ export class Database {
|
|
|
176
194
|
return rows.map(rowToLink);
|
|
177
195
|
}
|
|
178
196
|
findDeadCode(kind, limit = 50) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
AND s.file_path NOT LIKE 'test/fixtures/%'
|
|
184
|
-
AND s.file_path NOT LIKE '%/page.ts' AND s.file_path NOT LIKE '%/page.tsx'
|
|
185
|
-
AND s.file_path NOT LIKE '%/page.js' AND s.file_path NOT LIKE '%/page.jsx'
|
|
186
|
-
AND s.file_path NOT LIKE '%/layout.ts' AND s.file_path NOT LIKE '%/layout.tsx'
|
|
187
|
-
AND s.file_path NOT LIKE '%/layout.js' AND s.file_path NOT LIKE '%/layout.jsx'
|
|
188
|
-
AND s.file_path NOT LIKE '%/loading.ts' AND s.file_path NOT LIKE '%/loading.tsx'
|
|
189
|
-
AND s.file_path NOT LIKE '%/error.ts' AND s.file_path NOT LIKE '%/error.tsx'
|
|
190
|
-
AND s.file_path NOT LIKE '%/not-found.ts' AND s.file_path NOT LIKE '%/not-found.tsx'
|
|
191
|
-
AND s.file_path NOT LIKE '%/template.ts' AND s.file_path NOT LIKE '%/template.tsx'
|
|
192
|
-
AND s.file_path NOT LIKE '%/route.ts' AND s.file_path NOT LIKE '%/route.tsx'
|
|
193
|
-
AND s.file_path NOT LIKE '%/route.js' AND s.file_path NOT LIKE '%/route.jsx'
|
|
194
|
-
AND s.file_path NOT LIKE '%/default.ts' AND s.file_path NOT LIKE '%/default.tsx'
|
|
195
|
-
AND s.file_path NOT LIKE '%/global-error.ts' AND s.file_path NOT LIKE '%/global-error.tsx'
|
|
196
|
-
AND s.file_path NOT LIKE '%/middleware.ts' AND s.file_path NOT LIKE '%/middleware.js'
|
|
197
|
-
AND s.file_path NOT LIKE '%.config.ts' AND s.file_path NOT LIKE '%.config.js'
|
|
198
|
-
AND s.file_path NOT LIKE '%.config.mjs' AND s.file_path NOT LIKE '%.config.cjs'
|
|
199
|
-
AND s.file_path NOT LIKE '%/+page.svelte' AND s.file_path NOT LIKE '%/+page.ts'
|
|
200
|
-
AND s.file_path NOT LIKE '%/+page.server.ts' AND s.file_path NOT LIKE '%/+layout.svelte'
|
|
201
|
-
AND s.file_path NOT LIKE '%/+layout.ts' AND s.file_path NOT LIKE '%/+layout.server.ts'
|
|
202
|
-
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.%'`;
|
|
203
201
|
const sql = kind
|
|
204
|
-
? `SELECT s.* FROM symbols s
|
|
205
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
206
|
-
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}
|
|
207
206
|
LIMIT ?`
|
|
208
|
-
: `SELECT s.* FROM symbols s
|
|
209
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
210
|
-
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}
|
|
211
211
|
LIMIT ?`;
|
|
212
212
|
const rows = kind
|
|
213
213
|
? this.db.prepare(sql).all(kind, limit)
|
|
@@ -215,25 +215,25 @@ export class Database {
|
|
|
215
215
|
return rows.map(rowToSymbol);
|
|
216
216
|
}
|
|
217
217
|
getTypeHierarchy(symbolId) {
|
|
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
|
|
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
|
|
227
227
|
`).all(symbolId);
|
|
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
|
|
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
|
|
237
237
|
`).all(symbolId);
|
|
238
238
|
return {
|
|
239
239
|
ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
|
|
@@ -248,15 +248,15 @@ export class Database {
|
|
|
248
248
|
const fromId = fromSyms[0].id;
|
|
249
249
|
const toIds = new Set(toSyms.map(s => s.id));
|
|
250
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
|
|
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
|
|
260
260
|
`).all(fromId, maxDepth);
|
|
261
261
|
const result = [];
|
|
262
262
|
for (const r of rows) {
|
|
@@ -309,14 +309,14 @@ export class Database {
|
|
|
309
309
|
};
|
|
310
310
|
}
|
|
311
311
|
getConfidenceDistribution() {
|
|
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'
|
|
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'
|
|
320
320
|
`).get();
|
|
321
321
|
return {
|
|
322
322
|
high: rows?.high ?? 0,
|
|
@@ -369,34 +369,34 @@ export class Database {
|
|
|
369
369
|
// ── Route/endpoint detection via link patterns ──
|
|
370
370
|
getEntrypoints() {
|
|
371
371
|
// Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
|
|
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
|
|
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
|
|
378
378
|
`).all();
|
|
379
379
|
return rows.map(rowToSymbol);
|
|
380
380
|
}
|
|
381
381
|
// ── Domain clustering stats ──
|
|
382
382
|
getDomainStats() {
|
|
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
|
|
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
|
|
391
391
|
`).all();
|
|
392
392
|
return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
|
|
393
393
|
}
|
|
394
394
|
// ── Staleness detection ──
|
|
395
395
|
getStaleFiles(hoursOld = 24) {
|
|
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
|
|
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
|
|
400
400
|
`).all(hoursOld);
|
|
401
401
|
return rows.map((r) => r.path);
|
|
402
402
|
}
|
|
@@ -419,40 +419,46 @@ export class Database {
|
|
|
419
419
|
}
|
|
420
420
|
// ── Tool usage tracking ──
|
|
421
421
|
logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
|
|
422
|
-
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)
|
|
423
423
|
VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
|
|
424
424
|
}
|
|
425
|
-
getToolUsageStats() {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
COALESCE(SUM(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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);
|
|
456
462
|
return {
|
|
457
463
|
totalCalls: totals.total_calls,
|
|
458
464
|
totalTokensSaved: totals.total_saved,
|
|
@@ -468,6 +474,210 @@ export class Database {
|
|
|
468
474
|
})),
|
|
469
475
|
};
|
|
470
476
|
}
|
|
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; }
|
|
630
|
+
addAnnotation(symbolId, key, value, agent, sessionId, ttlHours) {
|
|
631
|
+
const expiresAt = ttlHours
|
|
632
|
+
? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
|
|
633
|
+
: null;
|
|
634
|
+
const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
|
|
635
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
|
|
636
|
+
return result.lastInsertRowid;
|
|
637
|
+
}
|
|
638
|
+
getAnnotations(filters = {}) {
|
|
639
|
+
const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
|
|
640
|
+
const params = [];
|
|
641
|
+
if (filters.symbolId) {
|
|
642
|
+
clauses.push('symbol_id = ?');
|
|
643
|
+
params.push(filters.symbolId);
|
|
644
|
+
}
|
|
645
|
+
if (filters.key) {
|
|
646
|
+
clauses.push('key = ?');
|
|
647
|
+
params.push(filters.key);
|
|
648
|
+
}
|
|
649
|
+
if (filters.agent) {
|
|
650
|
+
clauses.push('agent = ?');
|
|
651
|
+
params.push(filters.agent);
|
|
652
|
+
}
|
|
653
|
+
if (filters.sessionId) {
|
|
654
|
+
clauses.push('session_id = ?');
|
|
655
|
+
params.push(filters.sessionId);
|
|
656
|
+
}
|
|
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 }));
|
|
660
|
+
}
|
|
661
|
+
getAnnotationsForSymbol(symbolId) {
|
|
662
|
+
return this.getAnnotations({ symbolId });
|
|
663
|
+
}
|
|
664
|
+
cleanupExpiredAnnotations() {
|
|
665
|
+
const result = this.db.prepare("DELETE FROM annotations WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')").run();
|
|
666
|
+
return result.changes;
|
|
667
|
+
}
|
|
668
|
+
startSession(id, agent, context) {
|
|
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);
|
|
671
|
+
}
|
|
672
|
+
getSession(id) {
|
|
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);
|
|
674
|
+
if (!row)
|
|
675
|
+
return null;
|
|
676
|
+
return { id: row.id, agent: row.agent, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, context: row.context };
|
|
677
|
+
}
|
|
678
|
+
endSession(id, status) {
|
|
679
|
+
this.db.prepare("UPDATE agent_sessions SET status = ?, ended_at = datetime('now') WHERE id = ?").run(status, id);
|
|
680
|
+
}
|
|
471
681
|
transaction(fn) {
|
|
472
682
|
return this.db.transaction(fn)();
|
|
473
683
|
}
|