milens 0.7.2 → 0.7.3
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 +59 -59
- package/.agents/skills/analyzer/SKILL.md +83 -83
- package/.agents/skills/apps/SKILL.md +65 -65
- package/.agents/skills/docs/SKILL.md +63 -63
- package/.agents/skills/milens/SKILL.md +322 -322
- package/.agents/skills/milens-security-review/SKILL.md +224 -224
- package/.agents/skills/orchestrator/SKILL.md +64 -64
- package/.agents/skills/parser/SKILL.md +86 -86
- package/.agents/skills/root/SKILL.md +89 -89
- package/.agents/skills/scripts/SKILL.md +56 -56
- package/.agents/skills/security/SKILL.md +67 -67
- package/.agents/skills/server/SKILL.md +84 -84
- package/.agents/skills/store/SKILL.md +77 -77
- package/.agents/skills/test/SKILL.md +155 -155
- package/.agents/skills/ui/SKILL.md +56 -56
- package/README.md +577 -577
- package/adapters/README.md +144 -144
- package/adapters/claude-code/.claude/mcp.json +9 -9
- package/adapters/claude-code/.claude/settings.json.hooks-snippet.json +26 -26
- package/adapters/claude-code/.claude-plugin/plugin.json +11 -11
- package/adapters/claude-code/.mcp.json +9 -9
- package/adapters/claude-code/CLAUDE.md +81 -81
- package/adapters/claude-code/hooks/hooks.json +27 -27
- package/adapters/codex/.codex/config.toml +3 -3
- package/adapters/copilot/.vscode/mcp.json +10 -10
- package/adapters/cursor/.cursor/mcp.json +9 -9
- package/adapters/cursor/.cursorrules +69 -69
- package/adapters/gemini/.gemini/context.md +81 -81
- package/adapters/gemini/.gemini/settings.json +9 -9
- package/adapters/opencode/.opencode/config.json +8 -8
- package/adapters/zed/.zed/settings.json +8 -8
- package/dist/build-info.d.ts +2 -2
- package/dist/build-info.js +2 -2
- package/dist/cli.js +482 -482
- package/dist/parser/lang-css.js +9 -9
- package/dist/parser/lang-go.js +58 -58
- package/dist/parser/lang-html.js +3 -3
- package/dist/parser/lang-java.js +37 -37
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +45 -45
- package/dist/parser/lang-py.js +43 -43
- package/dist/parser/lang-ruby.js +28 -28
- package/dist/parser/lang-rust.js +48 -48
- package/dist/parser/lang-ts.js +210 -210
- package/dist/server/mcp-prompts.js +502 -502
- package/dist/server/mcp.js +56 -56
- package/dist/skills.js +296 -296
- package/dist/store/annotations.js +1 -1
- package/dist/store/db.js +224 -224
- package/dist/store/schema.sql +144 -144
- package/dist/store/vectors.js +2 -2
- package/package.json +85 -85
|
@@ -12,7 +12,7 @@ export class AnnotationStore {
|
|
|
12
12
|
}
|
|
13
13
|
prepareStatements() {
|
|
14
14
|
return {
|
|
15
|
-
insertAnnotation: this.db.prepare(`INSERT OR REPLACE INTO annotations (id, symbol, key, value, agent, session_id, confidence, symbol_hash, created_at, updated_at)
|
|
15
|
+
insertAnnotation: this.db.prepare(`INSERT OR REPLACE INTO annotations (id, symbol, key, value, agent, session_id, confidence, symbol_hash, created_at, updated_at)
|
|
16
16
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
17
17
|
findBySymbolKey: this.db.prepare('SELECT * FROM annotations WHERE symbol = ? AND key = ?'),
|
|
18
18
|
updateAnnotation: this.db.prepare('UPDATE annotations SET value = ?, confidence = ?, agent = ?, session_id = ?, symbol_hash = ?, updated_at = datetime(\'now\') WHERE id = ?'),
|
package/dist/store/db.js
CHANGED
|
@@ -34,43 +34,43 @@ export class Database {
|
|
|
34
34
|
prepareStatements() {
|
|
35
35
|
return {
|
|
36
36
|
checkHash: this.db.prepare('SELECT hash FROM file_hashes WHERE path = ?'),
|
|
37
|
-
upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
|
|
37
|
+
upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
|
|
38
38
|
ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, analyzed_at = datetime('now')`),
|
|
39
|
-
insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
|
|
39
|
+
insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
|
|
40
40
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
41
41
|
updateMeta: this.db.prepare(`UPDATE symbols SET role = ?, heat = ? WHERE id = ?`),
|
|
42
42
|
upsertZone: this.db.prepare(`UPDATE file_hashes SET zone = ? WHERE path = ?`),
|
|
43
|
-
insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
|
|
43
|
+
insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
|
|
44
44
|
VALUES (?, ?, ?, ?, ?, ?)`),
|
|
45
|
-
searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
|
|
46
|
-
JOIN symbols s ON s.rowid = f.rowid
|
|
47
|
-
WHERE symbol_fts MATCH ?
|
|
45
|
+
searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
|
|
46
|
+
JOIN symbols s ON s.rowid = f.rowid
|
|
47
|
+
WHERE symbol_fts MATCH ?
|
|
48
48
|
ORDER BY rank LIMIT ?`),
|
|
49
49
|
byName: this.db.prepare('SELECT * FROM symbols WHERE name = ?'),
|
|
50
50
|
byId: this.db.prepare('SELECT * FROM symbols WHERE id = ?'),
|
|
51
51
|
byFile: this.db.prepare('SELECT * FROM symbols WHERE file_path = ?'),
|
|
52
52
|
linksIn: this.db.prepare('SELECT * FROM links WHERE to_id = ?'),
|
|
53
53
|
linksOut: this.db.prepare('SELECT * FROM links WHERE from_id = ?'),
|
|
54
|
-
upstream: this.db.prepare(`
|
|
55
|
-
WITH RECURSIVE upstream(id, depth, via) AS (
|
|
56
|
-
SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
57
|
-
UNION
|
|
58
|
-
SELECT l.from_id, u.depth + 1, l.type
|
|
59
|
-
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
60
|
-
WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
61
|
-
)
|
|
62
|
-
SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
54
|
+
upstream: this.db.prepare(`
|
|
55
|
+
WITH RECURSIVE upstream(id, depth, via) AS (
|
|
56
|
+
SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
57
|
+
UNION
|
|
58
|
+
SELECT l.from_id, u.depth + 1, l.type
|
|
59
|
+
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
60
|
+
WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
61
|
+
)
|
|
62
|
+
SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
63
63
|
`),
|
|
64
|
-
downstream: this.db.prepare(`
|
|
65
|
-
WITH RECURSIVE downstream(id, depth, via, path) AS (
|
|
66
|
-
SELECT to_id, 1, type, ',' || to_id || ',' FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
67
|
-
UNION
|
|
68
|
-
SELECT l.to_id, d.depth + 1, l.type, d.path || l.to_id || ','
|
|
69
|
-
FROM links l JOIN downstream d ON l.from_id = d.id
|
|
70
|
-
WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
71
|
-
AND d.path NOT LIKE '%,' || l.to_id || ',%'
|
|
72
|
-
)
|
|
73
|
-
SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
64
|
+
downstream: this.db.prepare(`
|
|
65
|
+
WITH RECURSIVE downstream(id, depth, via, path) AS (
|
|
66
|
+
SELECT to_id, 1, type, ',' || to_id || ',' FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
67
|
+
UNION
|
|
68
|
+
SELECT l.to_id, d.depth + 1, l.type, d.path || l.to_id || ','
|
|
69
|
+
FROM links l JOIN downstream d ON l.from_id = d.id
|
|
70
|
+
WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
|
|
71
|
+
AND d.path NOT LIKE '%,' || l.to_id || ',%'
|
|
72
|
+
)
|
|
73
|
+
SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
74
74
|
`),
|
|
75
75
|
countSymbols: this.db.prepare('SELECT COUNT(*) as c FROM symbols'),
|
|
76
76
|
countLinks: this.db.prepare('SELECT COUNT(*) as c FROM links'),
|
|
@@ -78,17 +78,17 @@ export class Database {
|
|
|
78
78
|
deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?)'),
|
|
79
79
|
deleteFileSymbols: this.db.prepare('DELETE FROM symbols WHERE file_path = ?'),
|
|
80
80
|
topHubs: this.db.prepare('SELECT * FROM symbols WHERE exported = 1 ORDER BY heat DESC LIMIT ?'),
|
|
81
|
-
testCoverageGaps: this.db.prepare(`
|
|
82
|
-
SELECT s.* FROM symbols s
|
|
83
|
-
WHERE s.exported = 1 AND s.heat > 0
|
|
84
|
-
AND s.id NOT IN (
|
|
85
|
-
SELECT DISTINCT l.to_id FROM links l
|
|
86
|
-
JOIN symbols src ON src.id = l.from_id
|
|
87
|
-
WHERE (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
88
|
-
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
89
|
-
)
|
|
90
|
-
ORDER BY s.heat DESC
|
|
91
|
-
LIMIT ?
|
|
81
|
+
testCoverageGaps: this.db.prepare(`
|
|
82
|
+
SELECT s.* FROM symbols s
|
|
83
|
+
WHERE s.exported = 1 AND s.heat > 0
|
|
84
|
+
AND s.id NOT IN (
|
|
85
|
+
SELECT DISTINCT l.to_id FROM links l
|
|
86
|
+
JOIN symbols src ON src.id = l.from_id
|
|
87
|
+
WHERE (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
88
|
+
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
89
|
+
)
|
|
90
|
+
ORDER BY s.heat DESC
|
|
91
|
+
LIMIT ?
|
|
92
92
|
`),
|
|
93
93
|
annotationCount: this.db.prepare('SELECT COUNT(*) as c FROM annotations'),
|
|
94
94
|
};
|
|
@@ -264,46 +264,46 @@ export class Database {
|
|
|
264
264
|
// Next.js/SvelteKit/api conventions are matched both at the repo root AND nested
|
|
265
265
|
// under a package subdirectory (e.g. monorepos with frontend/app/... instead of
|
|
266
266
|
// app/...) — a root-only 'app/%' pattern silently misses every monorepo layout.
|
|
267
|
-
const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE '%/app/%/page.%'
|
|
268
|
-
AND s.file_path NOT LIKE 'app/%/layout.%' AND s.file_path NOT LIKE '%/app/%/layout.%'
|
|
269
|
-
AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE '%/app/page.%'
|
|
270
|
-
AND s.file_path NOT LIKE 'app/layout.%' AND s.file_path NOT LIKE '%/app/layout.%'
|
|
271
|
-
AND s.file_path NOT LIKE 'app/%/loading.%' AND s.file_path NOT LIKE '%/app/%/loading.%'
|
|
272
|
-
AND s.file_path NOT LIKE 'app/%/error.%' AND s.file_path NOT LIKE '%/app/%/error.%'
|
|
273
|
-
AND s.file_path NOT LIKE 'app/%/not-found.%' AND s.file_path NOT LIKE '%/app/%/not-found.%'
|
|
274
|
-
AND s.file_path NOT LIKE 'app/%/template.%' AND s.file_path NOT LIKE '%/app/%/template.%'
|
|
275
|
-
AND s.file_path NOT LIKE 'app/%/global-error.%' AND s.file_path NOT LIKE '%/app/%/global-error.%'
|
|
276
|
-
AND s.file_path NOT LIKE 'app/%/default.%' AND s.file_path NOT LIKE '%/app/%/default.%'
|
|
277
|
-
AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE '%/app/api/%/route.%'
|
|
278
|
-
AND s.file_path NOT LIKE 'app/%/route.%' AND s.file_path NOT LIKE '%/app/%/route.%'
|
|
279
|
-
AND s.file_path NOT LIKE 'jest.config.%'
|
|
280
|
-
AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE '%/src/routes/+page.%'
|
|
281
|
-
AND s.file_path NOT LIKE 'src/routes/+layout.%' AND s.file_path NOT LIKE '%/src/routes/+layout.%'
|
|
282
|
-
AND s.file_path NOT LIKE '%/alembic/versions/%'
|
|
283
|
-
AND s.file_path NOT LIKE '%/migrations/%'
|
|
284
|
-
AND s.file_path NOT LIKE 'api/%' AND s.file_path NOT LIKE '%/api/%'
|
|
267
|
+
const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE '%/app/%/page.%'
|
|
268
|
+
AND s.file_path NOT LIKE 'app/%/layout.%' AND s.file_path NOT LIKE '%/app/%/layout.%'
|
|
269
|
+
AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE '%/app/page.%'
|
|
270
|
+
AND s.file_path NOT LIKE 'app/layout.%' AND s.file_path NOT LIKE '%/app/layout.%'
|
|
271
|
+
AND s.file_path NOT LIKE 'app/%/loading.%' AND s.file_path NOT LIKE '%/app/%/loading.%'
|
|
272
|
+
AND s.file_path NOT LIKE 'app/%/error.%' AND s.file_path NOT LIKE '%/app/%/error.%'
|
|
273
|
+
AND s.file_path NOT LIKE 'app/%/not-found.%' AND s.file_path NOT LIKE '%/app/%/not-found.%'
|
|
274
|
+
AND s.file_path NOT LIKE 'app/%/template.%' AND s.file_path NOT LIKE '%/app/%/template.%'
|
|
275
|
+
AND s.file_path NOT LIKE 'app/%/global-error.%' AND s.file_path NOT LIKE '%/app/%/global-error.%'
|
|
276
|
+
AND s.file_path NOT LIKE 'app/%/default.%' AND s.file_path NOT LIKE '%/app/%/default.%'
|
|
277
|
+
AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE '%/app/api/%/route.%'
|
|
278
|
+
AND s.file_path NOT LIKE 'app/%/route.%' AND s.file_path NOT LIKE '%/app/%/route.%'
|
|
279
|
+
AND s.file_path NOT LIKE 'jest.config.%'
|
|
280
|
+
AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE '%/src/routes/+page.%'
|
|
281
|
+
AND s.file_path NOT LIKE 'src/routes/+layout.%' AND s.file_path NOT LIKE '%/src/routes/+layout.%'
|
|
282
|
+
AND s.file_path NOT LIKE '%/alembic/versions/%'
|
|
283
|
+
AND s.file_path NOT LIKE '%/migrations/%'
|
|
284
|
+
AND s.file_path NOT LIKE 'api/%' AND s.file_path NOT LIKE '%/api/%'
|
|
285
285
|
AND NOT ((s.file_path LIKE 'app/%' OR s.file_path LIKE '%/app/%') AND s.name IN ('generateStaticParams', 'metadata', 'generateMetadata', 'viewport', 'generateViewport', 'revalidate', 'dynamic', 'fetchCache', 'runtime', 'preferredRegion', 'maxDuration'))`;
|
|
286
286
|
// Vue SFC root components imported via <Component/> template tags get their
|
|
287
287
|
// import link on _top [module], not on the [class] root symbol. Treat the class
|
|
288
288
|
// as referenced if its file's _top module has incoming links from other files.
|
|
289
|
-
const vueRootGuard = `AND NOT (s.file_path LIKE '%.vue' AND s.kind = 'class' AND EXISTS (
|
|
290
|
-
SELECT 1 FROM symbols s2
|
|
291
|
-
JOIN links l2 ON l2.to_id = s2.id AND l2.type != 'contains'
|
|
292
|
-
WHERE s2.file_path = s.file_path AND s2.kind = 'module' AND s2.name = '_top'
|
|
293
|
-
AND EXISTS (SELECT 1 FROM links l3 WHERE l3.to_id = s2.id AND l3.type = 'imports')
|
|
289
|
+
const vueRootGuard = `AND NOT (s.file_path LIKE '%.vue' AND s.kind = 'class' AND EXISTS (
|
|
290
|
+
SELECT 1 FROM symbols s2
|
|
291
|
+
JOIN links l2 ON l2.to_id = s2.id AND l2.type != 'contains'
|
|
292
|
+
WHERE s2.file_path = s.file_path AND s2.kind = 'module' AND s2.name = '_top'
|
|
293
|
+
AND EXISTS (SELECT 1 FROM links l3 WHERE l3.to_id = s2.id AND l3.type = 'imports')
|
|
294
294
|
))`;
|
|
295
295
|
const sql = kind
|
|
296
|
-
? `SELECT s.* FROM symbols s
|
|
297
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
298
|
-
WHERE s.exported = 1 AND s.kind = ? AND s.kind != 'section' AND l.id IS NULL
|
|
299
|
-
${frameworkExclude}
|
|
300
|
-
${vueRootGuard}
|
|
296
|
+
? `SELECT s.* FROM symbols s
|
|
297
|
+
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
298
|
+
WHERE s.exported = 1 AND s.kind = ? AND s.kind != 'section' AND l.id IS NULL
|
|
299
|
+
${frameworkExclude}
|
|
300
|
+
${vueRootGuard}
|
|
301
301
|
LIMIT ?`
|
|
302
|
-
: `SELECT s.* FROM symbols s
|
|
303
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
304
|
-
WHERE s.exported = 1 AND s.kind != 'section' AND l.id IS NULL
|
|
305
|
-
${frameworkExclude}
|
|
306
|
-
${vueRootGuard}
|
|
302
|
+
: `SELECT s.* FROM symbols s
|
|
303
|
+
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
304
|
+
WHERE s.exported = 1 AND s.kind != 'section' AND l.id IS NULL
|
|
305
|
+
${frameworkExclude}
|
|
306
|
+
${vueRootGuard}
|
|
307
307
|
LIMIT ?`;
|
|
308
308
|
const rows = kind
|
|
309
309
|
? this.db.prepare(sql).all(kind, limit)
|
|
@@ -320,35 +320,35 @@ export class Database {
|
|
|
320
320
|
// Next.js/SvelteKit/api conventions are matched both at the repo root AND nested
|
|
321
321
|
// under a package subdirectory (e.g. monorepos with frontend/app/... instead of
|
|
322
322
|
// app/...) — a root-only 'app/%' pattern silently misses every monorepo layout.
|
|
323
|
-
const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE '%/app/%/page.%'
|
|
324
|
-
AND s.file_path NOT LIKE 'app/%/layout.%' AND s.file_path NOT LIKE '%/app/%/layout.%'
|
|
325
|
-
AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE '%/app/page.%'
|
|
326
|
-
AND s.file_path NOT LIKE 'app/layout.%' AND s.file_path NOT LIKE '%/app/layout.%'
|
|
327
|
-
AND s.file_path NOT LIKE 'app/%/loading.%' AND s.file_path NOT LIKE '%/app/%/loading.%'
|
|
328
|
-
AND s.file_path NOT LIKE 'app/%/error.%' AND s.file_path NOT LIKE '%/app/%/error.%'
|
|
329
|
-
AND s.file_path NOT LIKE 'app/%/not-found.%' AND s.file_path NOT LIKE '%/app/%/not-found.%'
|
|
330
|
-
AND s.file_path NOT LIKE 'app/%/template.%' AND s.file_path NOT LIKE '%/app/%/template.%'
|
|
331
|
-
AND s.file_path NOT LIKE 'app/%/global-error.%' AND s.file_path NOT LIKE '%/app/%/global-error.%'
|
|
332
|
-
AND s.file_path NOT LIKE 'app/%/default.%' AND s.file_path NOT LIKE '%/app/%/default.%'
|
|
333
|
-
AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE '%/app/api/%/route.%'
|
|
334
|
-
AND s.file_path NOT LIKE 'app/%/route.%' AND s.file_path NOT LIKE '%/app/%/route.%'
|
|
335
|
-
AND s.file_path NOT LIKE 'jest.config.%'
|
|
336
|
-
AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE '%/src/routes/+page.%'
|
|
337
|
-
AND s.file_path NOT LIKE 'src/routes/+layout.%' AND s.file_path NOT LIKE '%/src/routes/+layout.%'
|
|
338
|
-
AND s.file_path NOT LIKE '%/alembic/versions/%'
|
|
339
|
-
AND s.file_path NOT LIKE '%/migrations/%'
|
|
340
|
-
AND s.file_path NOT LIKE 'api/%' AND s.file_path NOT LIKE '%/api/%'
|
|
323
|
+
const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE '%/app/%/page.%'
|
|
324
|
+
AND s.file_path NOT LIKE 'app/%/layout.%' AND s.file_path NOT LIKE '%/app/%/layout.%'
|
|
325
|
+
AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE '%/app/page.%'
|
|
326
|
+
AND s.file_path NOT LIKE 'app/layout.%' AND s.file_path NOT LIKE '%/app/layout.%'
|
|
327
|
+
AND s.file_path NOT LIKE 'app/%/loading.%' AND s.file_path NOT LIKE '%/app/%/loading.%'
|
|
328
|
+
AND s.file_path NOT LIKE 'app/%/error.%' AND s.file_path NOT LIKE '%/app/%/error.%'
|
|
329
|
+
AND s.file_path NOT LIKE 'app/%/not-found.%' AND s.file_path NOT LIKE '%/app/%/not-found.%'
|
|
330
|
+
AND s.file_path NOT LIKE 'app/%/template.%' AND s.file_path NOT LIKE '%/app/%/template.%'
|
|
331
|
+
AND s.file_path NOT LIKE 'app/%/global-error.%' AND s.file_path NOT LIKE '%/app/%/global-error.%'
|
|
332
|
+
AND s.file_path NOT LIKE 'app/%/default.%' AND s.file_path NOT LIKE '%/app/%/default.%'
|
|
333
|
+
AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE '%/app/api/%/route.%'
|
|
334
|
+
AND s.file_path NOT LIKE 'app/%/route.%' AND s.file_path NOT LIKE '%/app/%/route.%'
|
|
335
|
+
AND s.file_path NOT LIKE 'jest.config.%'
|
|
336
|
+
AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE '%/src/routes/+page.%'
|
|
337
|
+
AND s.file_path NOT LIKE 'src/routes/+layout.%' AND s.file_path NOT LIKE '%/src/routes/+layout.%'
|
|
338
|
+
AND s.file_path NOT LIKE '%/alembic/versions/%'
|
|
339
|
+
AND s.file_path NOT LIKE '%/migrations/%'
|
|
340
|
+
AND s.file_path NOT LIKE 'api/%' AND s.file_path NOT LIKE '%/api/%'
|
|
341
341
|
AND NOT ((s.file_path LIKE 'app/%' OR s.file_path LIKE '%/app/%') AND s.name IN ('generateStaticParams', 'metadata', 'generateMetadata', 'viewport', 'generateViewport', 'revalidate', 'dynamic', 'fetchCache', 'runtime', 'preferredRegion', 'maxDuration'))`;
|
|
342
342
|
// Get ALL exported symbols that HAVE at least one incoming link (not caught by findDeadCode).
|
|
343
343
|
// No SQL LIMIT here: the JS post-filter below narrows this down to test-only-referenced
|
|
344
344
|
// symbols, which can be a small minority of low-heat candidates — applying `limit` before
|
|
345
345
|
// that filter would silently drop real orphans that don't happen to rank in the top N by heat.
|
|
346
|
-
const sql = `SELECT s.*, COUNT(l.id) as incoming_count FROM symbols s
|
|
347
|
-
JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
348
|
-
WHERE s.exported = 1 AND s.kind != 'section'
|
|
349
|
-
${frameworkExclude}
|
|
350
|
-
GROUP BY s.id
|
|
351
|
-
HAVING incoming_count > 0
|
|
346
|
+
const sql = `SELECT s.*, COUNT(l.id) as incoming_count FROM symbols s
|
|
347
|
+
JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
348
|
+
WHERE s.exported = 1 AND s.kind != 'section'
|
|
349
|
+
${frameworkExclude}
|
|
350
|
+
GROUP BY s.id
|
|
351
|
+
HAVING incoming_count > 0
|
|
352
352
|
ORDER BY s.heat DESC`;
|
|
353
353
|
const rows = this.db.prepare(sql).all();
|
|
354
354
|
const candidates = rows.map(rowToSymbol);
|
|
@@ -369,25 +369,25 @@ export class Database {
|
|
|
369
369
|
return results.slice(0, limit);
|
|
370
370
|
}
|
|
371
371
|
getTypeHierarchy(symbolId) {
|
|
372
|
-
const ancestors = this.db.prepare(`
|
|
373
|
-
WITH RECURSIVE up(id, depth) AS (
|
|
374
|
-
SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
|
|
375
|
-
UNION
|
|
376
|
-
SELECT l.to_id, u.depth + 1
|
|
377
|
-
FROM links l JOIN up u ON l.from_id = u.id
|
|
378
|
-
WHERE l.type IN ('extends', 'implements') AND u.depth < 10
|
|
379
|
-
)
|
|
380
|
-
SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
372
|
+
const ancestors = this.db.prepare(`
|
|
373
|
+
WITH RECURSIVE up(id, depth) AS (
|
|
374
|
+
SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
|
|
375
|
+
UNION
|
|
376
|
+
SELECT l.to_id, u.depth + 1
|
|
377
|
+
FROM links l JOIN up u ON l.from_id = u.id
|
|
378
|
+
WHERE l.type IN ('extends', 'implements') AND u.depth < 10
|
|
379
|
+
)
|
|
380
|
+
SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
|
|
381
381
|
`).all(symbolId);
|
|
382
|
-
const descendants = this.db.prepare(`
|
|
383
|
-
WITH RECURSIVE down(id, depth) AS (
|
|
384
|
-
SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
|
|
385
|
-
UNION
|
|
386
|
-
SELECT l.from_id, d.depth + 1
|
|
387
|
-
FROM links l JOIN down d ON l.to_id = d.id
|
|
388
|
-
WHERE l.type IN ('extends', 'implements') AND d.depth < 10
|
|
389
|
-
)
|
|
390
|
-
SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
382
|
+
const descendants = this.db.prepare(`
|
|
383
|
+
WITH RECURSIVE down(id, depth) AS (
|
|
384
|
+
SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
|
|
385
|
+
UNION
|
|
386
|
+
SELECT l.from_id, d.depth + 1
|
|
387
|
+
FROM links l JOIN down d ON l.to_id = d.id
|
|
388
|
+
WHERE l.type IN ('extends', 'implements') AND d.depth < 10
|
|
389
|
+
)
|
|
390
|
+
SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
|
|
391
391
|
`).all(symbolId);
|
|
392
392
|
return {
|
|
393
393
|
ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
|
|
@@ -424,26 +424,26 @@ export class Database {
|
|
|
424
424
|
// class-to-X query. Safe: it can't produce a false "relationship" between unrelated
|
|
425
425
|
// sibling methods, since it's scoped to fromId's own children only, never applied again
|
|
426
426
|
// to a class reached later in the chain.
|
|
427
|
-
const rows = this.db.prepare(`
|
|
428
|
-
WITH RECURSIVE chain(node_id, depth, via, path_ids) AS (
|
|
429
|
-
SELECT l.to_id, 1, l.type, ',' || l.from_id || ',' || l.to_id || ','
|
|
430
|
-
FROM links l WHERE l.from_id = ? AND l.type != 'contains'
|
|
431
|
-
UNION ALL
|
|
432
|
-
SELECT l2.to_id, 2, l2.type, ',' || l.from_id || ',' || l.to_id || ',' || l2.to_id || ','
|
|
433
|
-
FROM links l JOIN links l2 ON l2.from_id = l.to_id
|
|
434
|
-
WHERE l.from_id = ? AND l.type = 'contains' AND l2.type != 'contains'
|
|
435
|
-
UNION ALL
|
|
436
|
-
SELECT l.to_id, c.depth + 1, l.type, c.path_ids || l.to_id || ','
|
|
437
|
-
FROM links l JOIN chain c ON l.from_id = c.node_id
|
|
438
|
-
WHERE l.type != 'contains' AND c.depth < ?
|
|
439
|
-
AND c.path_ids NOT LIKE '%,' || l.to_id || ',%'
|
|
440
|
-
UNION ALL
|
|
441
|
-
SELECT l.from_id, c.depth + 1, l.type, c.path_ids || l.from_id || ','
|
|
442
|
-
FROM links l JOIN chain c ON l.to_id = c.node_id
|
|
443
|
-
WHERE l.type = 'contains' AND c.depth < ?
|
|
444
|
-
AND c.path_ids NOT LIKE '%,' || l.from_id || ',%'
|
|
445
|
-
)
|
|
446
|
-
SELECT node_id, depth, via, path_ids FROM chain ORDER BY depth
|
|
427
|
+
const rows = this.db.prepare(`
|
|
428
|
+
WITH RECURSIVE chain(node_id, depth, via, path_ids) AS (
|
|
429
|
+
SELECT l.to_id, 1, l.type, ',' || l.from_id || ',' || l.to_id || ','
|
|
430
|
+
FROM links l WHERE l.from_id = ? AND l.type != 'contains'
|
|
431
|
+
UNION ALL
|
|
432
|
+
SELECT l2.to_id, 2, l2.type, ',' || l.from_id || ',' || l.to_id || ',' || l2.to_id || ','
|
|
433
|
+
FROM links l JOIN links l2 ON l2.from_id = l.to_id
|
|
434
|
+
WHERE l.from_id = ? AND l.type = 'contains' AND l2.type != 'contains'
|
|
435
|
+
UNION ALL
|
|
436
|
+
SELECT l.to_id, c.depth + 1, l.type, c.path_ids || l.to_id || ','
|
|
437
|
+
FROM links l JOIN chain c ON l.from_id = c.node_id
|
|
438
|
+
WHERE l.type != 'contains' AND c.depth < ?
|
|
439
|
+
AND c.path_ids NOT LIKE '%,' || l.to_id || ',%'
|
|
440
|
+
UNION ALL
|
|
441
|
+
SELECT l.from_id, c.depth + 1, l.type, c.path_ids || l.from_id || ','
|
|
442
|
+
FROM links l JOIN chain c ON l.to_id = c.node_id
|
|
443
|
+
WHERE l.type = 'contains' AND c.depth < ?
|
|
444
|
+
AND c.path_ids NOT LIKE '%,' || l.from_id || ',%'
|
|
445
|
+
)
|
|
446
|
+
SELECT node_id, depth, via, path_ids FROM chain ORDER BY depth
|
|
447
447
|
`).all(fromId, fromId, maxDepth, maxDepth);
|
|
448
448
|
// Find the first (shortest-depth) row matching the target
|
|
449
449
|
const targetRow = rows.find(r => toIds.has(r.node_id));
|
|
@@ -517,14 +517,14 @@ export class Database {
|
|
|
517
517
|
};
|
|
518
518
|
}
|
|
519
519
|
getConfidenceDistribution() {
|
|
520
|
-
const rows = this.db.prepare(`
|
|
521
|
-
SELECT
|
|
522
|
-
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
523
|
-
SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
524
|
-
SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
|
|
525
|
-
COUNT(*) as total
|
|
526
|
-
FROM links
|
|
527
|
-
WHERE type != 'contains'
|
|
520
|
+
const rows = this.db.prepare(`
|
|
521
|
+
SELECT
|
|
522
|
+
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
523
|
+
SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
524
|
+
SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
|
|
525
|
+
COUNT(*) as total
|
|
526
|
+
FROM links
|
|
527
|
+
WHERE type != 'contains'
|
|
528
528
|
`).get();
|
|
529
529
|
return {
|
|
530
530
|
high: rows?.high ?? 0,
|
|
@@ -589,34 +589,34 @@ export class Database {
|
|
|
589
589
|
// ── Route/endpoint detection via link patterns ──
|
|
590
590
|
getEntrypoints() {
|
|
591
591
|
// Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
|
|
592
|
-
const rows = this.db.prepare(`
|
|
593
|
-
SELECT s.* FROM symbols s
|
|
594
|
-
WHERE s.exported = 1
|
|
595
|
-
AND s.role = 'entrypoint'
|
|
596
|
-
ORDER BY s.heat DESC
|
|
597
|
-
LIMIT 50
|
|
592
|
+
const rows = this.db.prepare(`
|
|
593
|
+
SELECT s.* FROM symbols s
|
|
594
|
+
WHERE s.exported = 1
|
|
595
|
+
AND s.role = 'entrypoint'
|
|
596
|
+
ORDER BY s.heat DESC
|
|
597
|
+
LIMIT 50
|
|
598
598
|
`).all();
|
|
599
599
|
return rows.map(rowToSymbol);
|
|
600
600
|
}
|
|
601
601
|
// ── Domain clustering stats ──
|
|
602
602
|
getDomainStats() {
|
|
603
|
-
const rows = this.db.prepare(`
|
|
604
|
-
SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
|
|
605
|
-
COUNT(s.id) AS symbol_count
|
|
606
|
-
FROM file_hashes fh
|
|
607
|
-
LEFT JOIN symbols s ON s.file_path = fh.path
|
|
608
|
-
WHERE fh.zone IS NOT NULL
|
|
609
|
-
GROUP BY fh.zone
|
|
610
|
-
ORDER BY symbol_count DESC
|
|
603
|
+
const rows = this.db.prepare(`
|
|
604
|
+
SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
|
|
605
|
+
COUNT(s.id) AS symbol_count
|
|
606
|
+
FROM file_hashes fh
|
|
607
|
+
LEFT JOIN symbols s ON s.file_path = fh.path
|
|
608
|
+
WHERE fh.zone IS NOT NULL
|
|
609
|
+
GROUP BY fh.zone
|
|
610
|
+
ORDER BY symbol_count DESC
|
|
611
611
|
`).all();
|
|
612
612
|
return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
|
|
613
613
|
}
|
|
614
614
|
// ── Staleness detection ──
|
|
615
615
|
getStaleFiles(hoursOld = 24) {
|
|
616
|
-
const rows = this.db.prepare(`
|
|
617
|
-
SELECT path FROM file_hashes
|
|
618
|
-
WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
|
|
619
|
-
ORDER BY analyzed_at ASC
|
|
616
|
+
const rows = this.db.prepare(`
|
|
617
|
+
SELECT path FROM file_hashes
|
|
618
|
+
WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
|
|
619
|
+
ORDER BY analyzed_at ASC
|
|
620
620
|
`).all(hoursOld);
|
|
621
621
|
return rows.map((r) => r.path);
|
|
622
622
|
}
|
|
@@ -642,14 +642,14 @@ export class Database {
|
|
|
642
642
|
return;
|
|
643
643
|
const placeholders = filePaths.map(() => '?').join(',');
|
|
644
644
|
// Delete links where either end references a symbol in the changed files
|
|
645
|
-
this.db.prepare(`
|
|
646
|
-
DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path IN (${placeholders}))
|
|
645
|
+
this.db.prepare(`
|
|
646
|
+
DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path IN (${placeholders}))
|
|
647
647
|
`).run(...filePaths);
|
|
648
|
-
this.db.prepare(`
|
|
649
|
-
DELETE FROM links WHERE to_id IN (SELECT id FROM symbols WHERE file_path IN (${placeholders}))
|
|
648
|
+
this.db.prepare(`
|
|
649
|
+
DELETE FROM links WHERE to_id IN (SELECT id FROM symbols WHERE file_path IN (${placeholders}))
|
|
650
650
|
`).run(...filePaths);
|
|
651
|
-
this.db.prepare(`
|
|
652
|
-
DELETE FROM symbols WHERE file_path IN (${placeholders})
|
|
651
|
+
this.db.prepare(`
|
|
652
|
+
DELETE FROM symbols WHERE file_path IN (${placeholders})
|
|
653
653
|
`).run(...filePaths);
|
|
654
654
|
}
|
|
655
655
|
/** Delete file_hashes rows for paths not in the given set (orphan cleanup after incremental analyze) */
|
|
@@ -657,52 +657,52 @@ export class Database {
|
|
|
657
657
|
if (knownPaths.length === 0)
|
|
658
658
|
return 0;
|
|
659
659
|
const placeholders = knownPaths.map(() => '?').join(',');
|
|
660
|
-
const result = this.db.prepare(`
|
|
661
|
-
DELETE FROM file_hashes WHERE path NOT IN (${placeholders})
|
|
660
|
+
const result = this.db.prepare(`
|
|
661
|
+
DELETE FROM file_hashes WHERE path NOT IN (${placeholders})
|
|
662
662
|
`).run(...knownPaths);
|
|
663
663
|
return result.changes;
|
|
664
664
|
}
|
|
665
665
|
// ── Tool usage tracking ──
|
|
666
666
|
logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
|
|
667
|
-
this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
|
|
667
|
+
this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
|
|
668
668
|
VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
|
|
669
669
|
}
|
|
670
670
|
getToolUsageStats(repo) {
|
|
671
671
|
const repoFilter = repo ? `WHERE repo = ?` : '';
|
|
672
672
|
const repoParam = repo ? [repo] : [];
|
|
673
|
-
const totals = this.db.prepare(`
|
|
674
|
-
SELECT COUNT(*) as total_calls,
|
|
675
|
-
COALESCE(SUM(tokens_saved), 0) as total_saved,
|
|
676
|
-
COALESCE(SUM(tokens_out), 0) as total_out,
|
|
677
|
-
COALESCE(SUM(duration_ms), 0) as total_ms
|
|
678
|
-
FROM tool_usage
|
|
679
|
-
${repoFilter}
|
|
673
|
+
const totals = this.db.prepare(`
|
|
674
|
+
SELECT COUNT(*) as total_calls,
|
|
675
|
+
COALESCE(SUM(tokens_saved), 0) as total_saved,
|
|
676
|
+
COALESCE(SUM(tokens_out), 0) as total_out,
|
|
677
|
+
COALESCE(SUM(duration_ms), 0) as total_ms
|
|
678
|
+
FROM tool_usage
|
|
679
|
+
${repoFilter}
|
|
680
680
|
`).get(...repoParam);
|
|
681
|
-
const byTool = this.db.prepare(`
|
|
682
|
-
SELECT tool, COUNT(*) as calls,
|
|
683
|
-
COALESCE(SUM(tokens_saved), 0) as tokens_saved,
|
|
684
|
-
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
|
685
|
-
CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
|
|
686
|
-
FROM tool_usage
|
|
687
|
-
${repoFilter}
|
|
688
|
-
GROUP BY tool
|
|
689
|
-
ORDER BY calls DESC
|
|
681
|
+
const byTool = this.db.prepare(`
|
|
682
|
+
SELECT tool, COUNT(*) as calls,
|
|
683
|
+
COALESCE(SUM(tokens_saved), 0) as tokens_saved,
|
|
684
|
+
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
|
685
|
+
CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
|
|
686
|
+
FROM tool_usage
|
|
687
|
+
${repoFilter}
|
|
688
|
+
GROUP BY tool
|
|
689
|
+
ORDER BY calls DESC
|
|
690
690
|
`).all(...repoParam);
|
|
691
|
-
const byDay = this.db.prepare(`
|
|
692
|
-
SELECT date(called_at) as date, COUNT(*) as calls,
|
|
693
|
-
COALESCE(SUM(tokens_saved), 0) as tokens_saved
|
|
694
|
-
FROM tool_usage
|
|
695
|
-
${repoFilter}
|
|
696
|
-
GROUP BY date(called_at)
|
|
697
|
-
ORDER BY date DESC
|
|
698
|
-
LIMIT 30
|
|
691
|
+
const byDay = this.db.prepare(`
|
|
692
|
+
SELECT date(called_at) as date, COUNT(*) as calls,
|
|
693
|
+
COALESCE(SUM(tokens_saved), 0) as tokens_saved
|
|
694
|
+
FROM tool_usage
|
|
695
|
+
${repoFilter}
|
|
696
|
+
GROUP BY date(called_at)
|
|
697
|
+
ORDER BY date DESC
|
|
698
|
+
LIMIT 30
|
|
699
699
|
`).all(...repoParam);
|
|
700
|
-
const recentCalls = this.db.prepare(`
|
|
701
|
-
SELECT tool, called_at, duration_ms, tokens_saved
|
|
702
|
-
FROM tool_usage
|
|
703
|
-
${repoFilter}
|
|
704
|
-
ORDER BY id DESC
|
|
705
|
-
LIMIT 50
|
|
700
|
+
const recentCalls = this.db.prepare(`
|
|
701
|
+
SELECT tool, called_at, duration_ms, tokens_saved
|
|
702
|
+
FROM tool_usage
|
|
703
|
+
${repoFilter}
|
|
704
|
+
ORDER BY id DESC
|
|
705
|
+
LIMIT 50
|
|
706
706
|
`).all(...repoParam);
|
|
707
707
|
return {
|
|
708
708
|
totalCalls: totals.total_calls,
|
|
@@ -746,10 +746,10 @@ export class Database {
|
|
|
746
746
|
if (candidateIds.length === 0)
|
|
747
747
|
return new Set();
|
|
748
748
|
const placeholders = candidateIds.map(() => '?').join(',');
|
|
749
|
-
const rows = this.db.prepare(`
|
|
750
|
-
SELECT DISTINCT l.to_id
|
|
751
|
-
FROM links l JOIN symbols src ON src.id = l.from_id
|
|
752
|
-
WHERE l.to_id IN (${placeholders}) AND l.type != 'contains'
|
|
749
|
+
const rows = this.db.prepare(`
|
|
750
|
+
SELECT DISTINCT l.to_id
|
|
751
|
+
FROM links l JOIN symbols src ON src.id = l.from_id
|
|
752
|
+
WHERE l.to_id IN (${placeholders}) AND l.type != 'contains'
|
|
753
753
|
`).all(...candidateIds);
|
|
754
754
|
const testedIds = new Set();
|
|
755
755
|
for (const row of rows) {
|
|
@@ -784,27 +784,27 @@ export class Database {
|
|
|
784
784
|
if (changedSymbolIds.length === 0)
|
|
785
785
|
return { testFiles: [], changedSymbols: [] };
|
|
786
786
|
const placeholders = changedSymbolIds.map(() => '?').join(',');
|
|
787
|
-
const testFilesDirect = this.db.prepare(`
|
|
788
|
-
SELECT DISTINCT src.file_path
|
|
789
|
-
FROM links l
|
|
790
|
-
JOIN symbols src ON src.id = l.from_id
|
|
791
|
-
WHERE l.to_id IN (${placeholders})
|
|
792
|
-
AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
793
|
-
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
787
|
+
const testFilesDirect = this.db.prepare(`
|
|
788
|
+
SELECT DISTINCT src.file_path
|
|
789
|
+
FROM links l
|
|
790
|
+
JOIN symbols src ON src.id = l.from_id
|
|
791
|
+
WHERE l.to_id IN (${placeholders})
|
|
792
|
+
AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
793
|
+
OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
|
|
794
794
|
`).all(...changedSymbolIds);
|
|
795
|
-
const testFilesUpstream = this.db.prepare(`
|
|
796
|
-
WITH RECURSIVE upstream(id, depth) AS (
|
|
797
|
-
SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
|
|
798
|
-
AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
799
|
-
UNION
|
|
800
|
-
SELECT l.from_id, u.depth + 1
|
|
801
|
-
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
802
|
-
WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
|
|
803
|
-
)
|
|
804
|
-
SELECT DISTINCT s.file_path FROM upstream u
|
|
805
|
-
JOIN symbols s ON s.id = u.id
|
|
806
|
-
WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
|
|
807
|
-
OR s.file_path LIKE '%.test.%' OR s.file_path LIKE '%.spec.%')
|
|
795
|
+
const testFilesUpstream = this.db.prepare(`
|
|
796
|
+
WITH RECURSIVE upstream(id, depth) AS (
|
|
797
|
+
SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
|
|
798
|
+
AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
799
|
+
UNION
|
|
800
|
+
SELECT l.from_id, u.depth + 1
|
|
801
|
+
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
802
|
+
WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
|
|
803
|
+
)
|
|
804
|
+
SELECT DISTINCT s.file_path FROM upstream u
|
|
805
|
+
JOIN symbols s ON s.id = u.id
|
|
806
|
+
WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
|
|
807
|
+
OR s.file_path LIKE '%.test.%' OR s.file_path LIKE '%.spec.%')
|
|
808
808
|
`).all(...changedSymbolIds);
|
|
809
809
|
const allTestFiles = [...new Set([
|
|
810
810
|
...testFilesDirect.map((r) => r.file_path),
|
|
@@ -891,7 +891,7 @@ export class Database {
|
|
|
891
891
|
const expiresAt = ttlHours
|
|
892
892
|
? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
|
|
893
893
|
: null;
|
|
894
|
-
const result = this.db.prepare(`INSERT INTO annotations (symbol, key, value, agent, session_id, expires_at)
|
|
894
|
+
const result = this.db.prepare(`INSERT INTO annotations (symbol, key, value, agent, session_id, expires_at)
|
|
895
895
|
VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
|
|
896
896
|
return result.lastInsertRowid;
|
|
897
897
|
}
|
|
@@ -946,12 +946,12 @@ export class Database {
|
|
|
946
946
|
this.db.prepare('INSERT INTO metric_history (metric_name, value) VALUES (?, ?)').run(name, value);
|
|
947
947
|
}
|
|
948
948
|
getMetricHistory(name, daysBack = 30) {
|
|
949
|
-
return this.db.prepare(`SELECT value, recorded_at as recordedAt FROM metric_history
|
|
950
|
-
WHERE metric_name = ? AND recorded_at >= datetime('now', ?)
|
|
949
|
+
return this.db.prepare(`SELECT value, recorded_at as recordedAt FROM metric_history
|
|
950
|
+
WHERE metric_name = ? AND recorded_at >= datetime('now', ?)
|
|
951
951
|
ORDER BY recorded_at DESC`).all(name, `-${daysBack} days`);
|
|
952
952
|
}
|
|
953
953
|
getMetricTrend(name) {
|
|
954
|
-
const rows = this.db.prepare(`SELECT value FROM metric_history
|
|
954
|
+
const rows = this.db.prepare(`SELECT value FROM metric_history
|
|
955
955
|
WHERE metric_name = ? ORDER BY recorded_at DESC LIMIT 2`).all(name);
|
|
956
956
|
if (rows.length === 0)
|
|
957
957
|
return { current: 0, previous: null, change: null };
|