milens 0.7.1 → 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 -54
- package/.agents/skills/analyzer/SKILL.md +83 -78
- package/.agents/skills/apps/SKILL.md +65 -60
- package/.agents/skills/docs/SKILL.md +63 -58
- package/.agents/skills/milens/SKILL.md +322 -319
- package/.agents/skills/milens-security-review/SKILL.md +224 -224
- package/.agents/skills/orchestrator/SKILL.md +64 -59
- package/.agents/skills/parser/SKILL.md +86 -81
- package/.agents/skills/root/SKILL.md +89 -84
- package/.agents/skills/scripts/SKILL.md +56 -51
- package/.agents/skills/security/SKILL.md +67 -62
- package/.agents/skills/server/SKILL.md +84 -77
- package/.agents/skills/store/SKILL.md +77 -72
- package/.agents/skills/test/SKILL.md +155 -148
- package/.agents/skills/ui/SKILL.md +56 -51
- 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 -79
- package/adapters/claude-code/hooks/hooks.json +27 -0
- 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/agents-md.d.ts +1 -0
- package/dist/agents-md.js +45 -1
- package/dist/agents-md.js.map +1 -1
- package/dist/analyzer/config.js +32 -0
- package/dist/analyzer/config.js.map +1 -1
- package/dist/analyzer/engine.js +26 -2
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/enrich.js +68 -1
- package/dist/analyzer/enrich.js.map +1 -1
- package/dist/analyzer/resolver.d.ts +4 -1
- package/dist/analyzer/resolver.js +241 -38
- package/dist/analyzer/resolver.js.map +1 -1
- package/dist/analyzer/review.js +32 -2
- package/dist/analyzer/review.js.map +1 -1
- package/dist/analyzer/scanner.js +3 -0
- package/dist/analyzer/scanner.js.map +1 -1
- package/dist/analyzer/scope-resolver.js +1 -1
- package/dist/analyzer/scope-resolver.js.map +1 -1
- package/dist/build-info.d.ts +2 -2
- package/dist/build-info.js +2 -2
- package/dist/cli.js +566 -497
- package/dist/cli.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +1 -1
- package/dist/orchestrator/orchestrator.js +14 -6
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/parser/extract.d.ts +7 -0
- package/dist/parser/extract.js +23 -1
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-css.js +18 -6
- package/dist/parser/lang-css.js.map +1 -1
- 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 +120 -109
- package/dist/parser/lang-js.js.map +1 -1
- package/dist/parser/lang-md.js +1 -0
- package/dist/parser/lang-md.js.map +1 -1
- 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 +230 -196
- package/dist/parser/lang-ts.js.map +1 -1
- package/dist/parser/lang-vue.js +0 -21
- package/dist/parser/lang-vue.js.map +1 -1
- package/dist/security/rules.js +22 -23
- package/dist/security/rules.js.map +1 -1
- package/dist/server/guard-hook.d.ts +10 -0
- package/dist/server/guard-hook.js +179 -0
- package/dist/server/guard-hook.js.map +1 -0
- package/dist/server/hooks.js +2 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/mcp-prompts.js +502 -502
- package/dist/server/mcp.js +228 -86
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/test-plan.d.ts +1 -0
- package/dist/server/test-plan.js +25 -7
- package/dist/server/test-plan.js.map +1 -1
- package/dist/server/tools/findings-report.d.ts +3 -0
- package/dist/server/tools/findings-report.js +170 -0
- package/dist/server/tools/findings-report.js.map +1 -0
- package/dist/server/tools/security.js +61 -5
- package/dist/server/tools/security.js.map +1 -1
- package/dist/server/tools/session.js +37 -17
- package/dist/server/tools/session.js.map +1 -1
- package/dist/server/tools/testing.js +58 -11
- package/dist/server/tools/testing.js.map +1 -1
- package/dist/skills.js +307 -301
- package/dist/skills.js.map +1 -1
- package/dist/store/annotations.d.ts +5 -0
- package/dist/store/annotations.js +45 -9
- package/dist/store/annotations.js.map +1 -1
- package/dist/store/db.d.ts +13 -0
- package/dist/store/db.js +318 -205
- package/dist/store/db.js.map +1 -1
- package/dist/store/registry.d.ts +1 -0
- package/dist/store/registry.js +17 -6
- package/dist/store/registry.js.map +1 -1
- package/dist/store/schema.sql +144 -143
- package/dist/store/vectors.js +2 -2
- package/dist/types.d.ts +16 -1
- package/dist/uninstall.d.ts +1 -0
- package/dist/uninstall.js +33 -10
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +0 -0
- package/dist/utils.js.map +1 -1
- package/package.json +85 -85
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
|
};
|
|
@@ -131,6 +131,9 @@ export class Database {
|
|
|
131
131
|
if (!annNames.has('updated_at')) {
|
|
132
132
|
this.db.exec(`ALTER TABLE annotations ADD COLUMN updated_at TEXT NOT NULL DEFAULT (datetime('now'))`);
|
|
133
133
|
}
|
|
134
|
+
if (!annNames.has('symbol_hash')) {
|
|
135
|
+
this.db.exec(`ALTER TABLE annotations ADD COLUMN symbol_hash TEXT`);
|
|
136
|
+
}
|
|
134
137
|
// Rebuild index if column was renamed
|
|
135
138
|
if (annNames.has('symbol_id')) {
|
|
136
139
|
this.db.exec(`DROP INDEX IF EXISTS idx_annotations_symbol`);
|
|
@@ -258,20 +261,49 @@ export class Database {
|
|
|
258
261
|
return rows.map(rowToLink);
|
|
259
262
|
}
|
|
260
263
|
findDeadCode(kind, limit = 50) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
// Next.js/SvelteKit/api conventions are matched both at the repo root AND nested
|
|
265
|
+
// under a package subdirectory (e.g. monorepos with frontend/app/... instead of
|
|
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/%'
|
|
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
|
+
// Vue SFC root components imported via <Component/> template tags get their
|
|
287
|
+
// import link on _top [module], not on the [class] root symbol. Treat the class
|
|
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')
|
|
294
|
+
))`;
|
|
265
295
|
const sql = kind
|
|
266
|
-
? `SELECT s.* FROM symbols s
|
|
267
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
268
|
-
WHERE s.exported = 1 AND s.kind = ? AND s.kind != 'section' AND l.id IS NULL
|
|
269
|
-
${frameworkExclude}
|
|
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}
|
|
270
301
|
LIMIT ?`
|
|
271
|
-
: `SELECT s.* FROM symbols s
|
|
272
|
-
LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
273
|
-
WHERE s.exported = 1 AND s.kind != 'section' AND l.id IS NULL
|
|
274
|
-
${frameworkExclude}
|
|
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}
|
|
275
307
|
LIMIT ?`;
|
|
276
308
|
const rows = kind
|
|
277
309
|
? this.db.prepare(sql).all(kind, limit)
|
|
@@ -285,20 +317,38 @@ export class Database {
|
|
|
285
317
|
* findDeadCode (which requires zero incoming links of any kind).
|
|
286
318
|
*/
|
|
287
319
|
findTestOnlyReferenced(limit = 50) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
// Next.js/SvelteKit/api conventions are matched both at the repo root AND nested
|
|
321
|
+
// under a package subdirectory (e.g. monorepos with frontend/app/... instead of
|
|
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/%'
|
|
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'))`;
|
|
292
342
|
// Get ALL exported symbols that HAVE at least one incoming link (not caught by findDeadCode).
|
|
293
343
|
// No SQL LIMIT here: the JS post-filter below narrows this down to test-only-referenced
|
|
294
344
|
// symbols, which can be a small minority of low-heat candidates — applying `limit` before
|
|
295
345
|
// that filter would silently drop real orphans that don't happen to rank in the top N by heat.
|
|
296
|
-
const sql = `SELECT s.*, COUNT(l.id) as incoming_count FROM symbols s
|
|
297
|
-
JOIN links l ON l.to_id = s.id AND l.type != 'contains'
|
|
298
|
-
WHERE s.exported = 1 AND s.kind != 'section'
|
|
299
|
-
${frameworkExclude}
|
|
300
|
-
GROUP BY s.id
|
|
301
|
-
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
|
|
302
352
|
ORDER BY s.heat DESC`;
|
|
303
353
|
const rows = this.db.prepare(sql).all();
|
|
304
354
|
const candidates = rows.map(rowToSymbol);
|
|
@@ -319,25 +369,25 @@ export class Database {
|
|
|
319
369
|
return results.slice(0, limit);
|
|
320
370
|
}
|
|
321
371
|
getTypeHierarchy(symbolId) {
|
|
322
|
-
const ancestors = this.db.prepare(`
|
|
323
|
-
WITH RECURSIVE up(id, depth) AS (
|
|
324
|
-
SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
|
|
325
|
-
UNION
|
|
326
|
-
SELECT l.to_id, u.depth + 1
|
|
327
|
-
FROM links l JOIN up u ON l.from_id = u.id
|
|
328
|
-
WHERE l.type IN ('extends', 'implements') AND u.depth < 10
|
|
329
|
-
)
|
|
330
|
-
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
|
|
331
381
|
`).all(symbolId);
|
|
332
|
-
const descendants = this.db.prepare(`
|
|
333
|
-
WITH RECURSIVE down(id, depth) AS (
|
|
334
|
-
SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
|
|
335
|
-
UNION
|
|
336
|
-
SELECT l.from_id, d.depth + 1
|
|
337
|
-
FROM links l JOIN down d ON l.to_id = d.id
|
|
338
|
-
WHERE l.type IN ('extends', 'implements') AND d.depth < 10
|
|
339
|
-
)
|
|
340
|
-
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
|
|
341
391
|
`).all(symbolId);
|
|
342
392
|
return {
|
|
343
393
|
ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
|
|
@@ -349,21 +399,52 @@ export class Database {
|
|
|
349
399
|
const toSyms = this.findSymbolByName(toName);
|
|
350
400
|
if (fromSyms.length === 0 || toSyms.length === 0)
|
|
351
401
|
return null;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
402
|
+
return this.findPathFromId(fromSyms[0].id, toSyms.map(s => s.id), maxDepth);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Same as findPath, but takes an already-resolved source symbol id instead
|
|
406
|
+
* of re-resolving by name. Needed when the caller has disambiguated among
|
|
407
|
+
* several same-named symbols (e.g. picking the right file-scoped `_top`
|
|
408
|
+
* module out of many) — passing the name back through findPath() would
|
|
409
|
+
* silently re-resolve to an arbitrary same-named symbol and discard that
|
|
410
|
+
* disambiguation.
|
|
411
|
+
*/
|
|
412
|
+
findPathFromId(fromId, toNameOrIds, maxDepth = 5) {
|
|
413
|
+
const toIds = Array.isArray(toNameOrIds)
|
|
414
|
+
? new Set(toNameOrIds)
|
|
415
|
+
: new Set(this.findSymbolByName(toNameOrIds).map(s => s.id));
|
|
416
|
+
if (toIds.size === 0)
|
|
417
|
+
return null;
|
|
418
|
+
// Anchor branch 2 seeds the search by descending ONE 'contains' hop from the exact
|
|
419
|
+
// origin symbol into its own contained methods, then taking one real edge from there.
|
|
420
|
+
// This only ever applies to the literal starting symbol (later branches never re-descend
|
|
421
|
+
// into contains children mid-path) — a class/struct/trait passed as `fromId` typically
|
|
422
|
+
// has zero direct calls/imports edges of its own (its real behavior lives in its
|
|
423
|
+
// methods), so without this seed the search dead-ends immediately at depth 0 for any
|
|
424
|
+
// class-to-X query. Safe: it can't produce a false "relationship" between unrelated
|
|
425
|
+
// sibling methods, since it's scoped to fromId's own children only, never applied again
|
|
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
|
|
447
|
+
`).all(fromId, fromId, maxDepth, maxDepth);
|
|
367
448
|
// Find the first (shortest-depth) row matching the target
|
|
368
449
|
const targetRow = rows.find(r => toIds.has(r.node_id));
|
|
369
450
|
if (!targetRow)
|
|
@@ -376,12 +457,21 @@ export class Database {
|
|
|
376
457
|
const sym = this.findSymbolById(idChain[i]);
|
|
377
458
|
if (!sym)
|
|
378
459
|
continue;
|
|
379
|
-
// Determine via for this hop: lookup link from idChain[i] → idChain[i+1]
|
|
460
|
+
// Determine via for this hop: lookup link from idChain[i] → idChain[i+1]. Prefer a
|
|
461
|
+
// real calls/imports/etc. edge when one exists between the two nodes; only report
|
|
462
|
+
// 'contains' (in either direction — the traversal now allows both a class-to-method
|
|
463
|
+
// seed step and a method-to-class arrival step) when that's the only relationship.
|
|
380
464
|
let via = 'calls';
|
|
381
465
|
if (i < idChain.length - 1) {
|
|
382
466
|
const linkRow = this.db.prepare('SELECT type FROM links WHERE from_id = ? AND to_id = ? AND type != ? LIMIT 1').get(idChain[i], idChain[i + 1], 'contains');
|
|
383
|
-
if (linkRow)
|
|
467
|
+
if (linkRow) {
|
|
384
468
|
via = linkRow.type;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
const containsRow = this.db.prepare('SELECT type FROM links WHERE ((from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?)) AND type = ? LIMIT 1').get(idChain[i], idChain[i + 1], idChain[i + 1], idChain[i], 'contains');
|
|
472
|
+
if (containsRow)
|
|
473
|
+
via = containsRow.type;
|
|
474
|
+
}
|
|
385
475
|
}
|
|
386
476
|
result.push({ symbol: sym, depth: i, via });
|
|
387
477
|
}
|
|
@@ -427,14 +517,14 @@ export class Database {
|
|
|
427
517
|
};
|
|
428
518
|
}
|
|
429
519
|
getConfidenceDistribution() {
|
|
430
|
-
const rows = this.db.prepare(`
|
|
431
|
-
SELECT
|
|
432
|
-
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
433
|
-
SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
434
|
-
SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
|
|
435
|
-
COUNT(*) as total
|
|
436
|
-
FROM links
|
|
437
|
-
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'
|
|
438
528
|
`).get();
|
|
439
529
|
return {
|
|
440
530
|
high: rows?.high ?? 0,
|
|
@@ -459,7 +549,16 @@ export class Database {
|
|
|
459
549
|
if (incoming.length === 0) {
|
|
460
550
|
// Reached an entrypoint — save this path
|
|
461
551
|
// _top modules represent file-level entrypoints (e.g., top-level code execution)
|
|
462
|
-
if
|
|
552
|
+
// Class methods: if the method is contained by an exported class, treat it as
|
|
553
|
+
// a de facto entrypoint (framework-invoked handlers like NestJS controllers).
|
|
554
|
+
const isExportedClassMember = !sym?.exported && sym?.kind === 'method' &&
|
|
555
|
+
this.getIncomingLinks(currentId).some(l => {
|
|
556
|
+
if (l.type !== 'contains')
|
|
557
|
+
return false;
|
|
558
|
+
const containingClass = this.findSymbolById(l.fromId);
|
|
559
|
+
return containingClass?.exported === true;
|
|
560
|
+
});
|
|
561
|
+
if (sym?.exported || (sym?.kind === 'module' && sym?.name === '_top') || isExportedClassMember) {
|
|
463
562
|
paths.push({ path: [...currentPath] });
|
|
464
563
|
}
|
|
465
564
|
visited.delete(currentId);
|
|
@@ -490,34 +589,34 @@ export class Database {
|
|
|
490
589
|
// ── Route/endpoint detection via link patterns ──
|
|
491
590
|
getEntrypoints() {
|
|
492
591
|
// Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
|
|
493
|
-
const rows = this.db.prepare(`
|
|
494
|
-
SELECT s.* FROM symbols s
|
|
495
|
-
WHERE s.exported = 1
|
|
496
|
-
AND s.role = 'entrypoint'
|
|
497
|
-
ORDER BY s.heat DESC
|
|
498
|
-
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
|
|
499
598
|
`).all();
|
|
500
599
|
return rows.map(rowToSymbol);
|
|
501
600
|
}
|
|
502
601
|
// ── Domain clustering stats ──
|
|
503
602
|
getDomainStats() {
|
|
504
|
-
const rows = this.db.prepare(`
|
|
505
|
-
SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
|
|
506
|
-
COUNT(s.id) AS symbol_count
|
|
507
|
-
FROM file_hashes fh
|
|
508
|
-
LEFT JOIN symbols s ON s.file_path = fh.path
|
|
509
|
-
WHERE fh.zone IS NOT NULL
|
|
510
|
-
GROUP BY fh.zone
|
|
511
|
-
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
|
|
512
611
|
`).all();
|
|
513
612
|
return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
|
|
514
613
|
}
|
|
515
614
|
// ── Staleness detection ──
|
|
516
615
|
getStaleFiles(hoursOld = 24) {
|
|
517
|
-
const rows = this.db.prepare(`
|
|
518
|
-
SELECT path FROM file_hashes
|
|
519
|
-
WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
|
|
520
|
-
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
|
|
521
620
|
`).all(hoursOld);
|
|
522
621
|
return rows.map((r) => r.path);
|
|
523
622
|
}
|
|
@@ -543,14 +642,14 @@ export class Database {
|
|
|
543
642
|
return;
|
|
544
643
|
const placeholders = filePaths.map(() => '?').join(',');
|
|
545
644
|
// Delete links where either end references a symbol in the changed files
|
|
546
|
-
this.db.prepare(`
|
|
547
|
-
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}))
|
|
548
647
|
`).run(...filePaths);
|
|
549
|
-
this.db.prepare(`
|
|
550
|
-
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}))
|
|
551
650
|
`).run(...filePaths);
|
|
552
|
-
this.db.prepare(`
|
|
553
|
-
DELETE FROM symbols WHERE file_path IN (${placeholders})
|
|
651
|
+
this.db.prepare(`
|
|
652
|
+
DELETE FROM symbols WHERE file_path IN (${placeholders})
|
|
554
653
|
`).run(...filePaths);
|
|
555
654
|
}
|
|
556
655
|
/** Delete file_hashes rows for paths not in the given set (orphan cleanup after incremental analyze) */
|
|
@@ -558,52 +657,52 @@ export class Database {
|
|
|
558
657
|
if (knownPaths.length === 0)
|
|
559
658
|
return 0;
|
|
560
659
|
const placeholders = knownPaths.map(() => '?').join(',');
|
|
561
|
-
const result = this.db.prepare(`
|
|
562
|
-
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})
|
|
563
662
|
`).run(...knownPaths);
|
|
564
663
|
return result.changes;
|
|
565
664
|
}
|
|
566
665
|
// ── Tool usage tracking ──
|
|
567
666
|
logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
|
|
568
|
-
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)
|
|
569
668
|
VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
|
|
570
669
|
}
|
|
571
670
|
getToolUsageStats(repo) {
|
|
572
671
|
const repoFilter = repo ? `WHERE repo = ?` : '';
|
|
573
672
|
const repoParam = repo ? [repo] : [];
|
|
574
|
-
const totals = this.db.prepare(`
|
|
575
|
-
SELECT COUNT(*) as total_calls,
|
|
576
|
-
COALESCE(SUM(tokens_saved), 0) as total_saved,
|
|
577
|
-
COALESCE(SUM(tokens_out), 0) as total_out,
|
|
578
|
-
COALESCE(SUM(duration_ms), 0) as total_ms
|
|
579
|
-
FROM tool_usage
|
|
580
|
-
${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}
|
|
581
680
|
`).get(...repoParam);
|
|
582
|
-
const byTool = this.db.prepare(`
|
|
583
|
-
SELECT tool, COUNT(*) as calls,
|
|
584
|
-
COALESCE(SUM(tokens_saved), 0) as tokens_saved,
|
|
585
|
-
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
|
586
|
-
CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
|
|
587
|
-
FROM tool_usage
|
|
588
|
-
${repoFilter}
|
|
589
|
-
GROUP BY tool
|
|
590
|
-
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
|
|
591
690
|
`).all(...repoParam);
|
|
592
|
-
const byDay = this.db.prepare(`
|
|
593
|
-
SELECT date(called_at) as date, COUNT(*) as calls,
|
|
594
|
-
COALESCE(SUM(tokens_saved), 0) as tokens_saved
|
|
595
|
-
FROM tool_usage
|
|
596
|
-
${repoFilter}
|
|
597
|
-
GROUP BY date(called_at)
|
|
598
|
-
ORDER BY date DESC
|
|
599
|
-
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
|
|
600
699
|
`).all(...repoParam);
|
|
601
|
-
const recentCalls = this.db.prepare(`
|
|
602
|
-
SELECT tool, called_at, duration_ms, tokens_saved
|
|
603
|
-
FROM tool_usage
|
|
604
|
-
${repoFilter}
|
|
605
|
-
ORDER BY id DESC
|
|
606
|
-
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
|
|
607
706
|
`).all(...repoParam);
|
|
608
707
|
return {
|
|
609
708
|
totalCalls: totals.total_calls,
|
|
@@ -622,7 +721,10 @@ export class Database {
|
|
|
622
721
|
}
|
|
623
722
|
// ── Test file detection ──
|
|
624
723
|
isTestFile(filePath) {
|
|
625
|
-
|
|
724
|
+
const codeExt = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|java|php|rs)$/i;
|
|
725
|
+
if (!codeExt.test(filePath))
|
|
726
|
+
return false;
|
|
727
|
+
return /[/\\]tests?[/\\]/.test(filePath) || /\.(test|spec)\./i.test(filePath);
|
|
626
728
|
}
|
|
627
729
|
// ── Heat / hubs ──
|
|
628
730
|
getTopHubs(limit) {
|
|
@@ -644,10 +746,10 @@ export class Database {
|
|
|
644
746
|
if (candidateIds.length === 0)
|
|
645
747
|
return new Set();
|
|
646
748
|
const placeholders = candidateIds.map(() => '?').join(',');
|
|
647
|
-
const rows = this.db.prepare(`
|
|
648
|
-
SELECT DISTINCT l.to_id
|
|
649
|
-
FROM links l JOIN symbols src ON src.id = l.from_id
|
|
650
|
-
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'
|
|
651
753
|
`).all(...candidateIds);
|
|
652
754
|
const testedIds = new Set();
|
|
653
755
|
for (const row of rows) {
|
|
@@ -682,27 +784,27 @@ export class Database {
|
|
|
682
784
|
if (changedSymbolIds.length === 0)
|
|
683
785
|
return { testFiles: [], changedSymbols: [] };
|
|
684
786
|
const placeholders = changedSymbolIds.map(() => '?').join(',');
|
|
685
|
-
const testFilesDirect = this.db.prepare(`
|
|
686
|
-
SELECT DISTINCT src.file_path
|
|
687
|
-
FROM links l
|
|
688
|
-
JOIN symbols src ON src.id = l.from_id
|
|
689
|
-
WHERE l.to_id IN (${placeholders})
|
|
690
|
-
AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
|
|
691
|
-
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.%')
|
|
692
794
|
`).all(...changedSymbolIds);
|
|
693
|
-
const testFilesUpstream = this.db.prepare(`
|
|
694
|
-
WITH RECURSIVE upstream(id, depth) AS (
|
|
695
|
-
SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
|
|
696
|
-
AND type IN ('calls', 'imports', 'extends', 'implements')
|
|
697
|
-
UNION
|
|
698
|
-
SELECT l.from_id, u.depth + 1
|
|
699
|
-
FROM links l JOIN upstream u ON l.to_id = u.id
|
|
700
|
-
WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
|
|
701
|
-
)
|
|
702
|
-
SELECT DISTINCT s.file_path FROM upstream u
|
|
703
|
-
JOIN symbols s ON s.id = u.id
|
|
704
|
-
WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
|
|
705
|
-
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.%')
|
|
706
808
|
`).all(...changedSymbolIds);
|
|
707
809
|
const allTestFiles = [...new Set([
|
|
708
810
|
...testFilesDirect.map((r) => r.file_path),
|
|
@@ -736,30 +838,41 @@ export class Database {
|
|
|
736
838
|
const target = this.findSymbolById(symbolId);
|
|
737
839
|
if (!target)
|
|
738
840
|
return [];
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
841
|
+
const CONTAINER_KINDS = new Set(['class', 'struct', 'trait']);
|
|
842
|
+
const buildSig = (id) => {
|
|
843
|
+
const sig = new Set();
|
|
844
|
+
const sym = this.findSymbolById(id);
|
|
845
|
+
const incoming = this.getIncomingLinks(id).filter(l => l.type !== 'contains');
|
|
846
|
+
const outgoing = this.getOutgoingLinks(id).filter(l => l.type !== 'contains');
|
|
847
|
+
for (const link of incoming)
|
|
848
|
+
sig.add(link.fromId);
|
|
849
|
+
for (const link of outgoing)
|
|
850
|
+
sig.add(link.toId);
|
|
851
|
+
if (sym && CONTAINER_KINDS.has(sym.kind)) {
|
|
852
|
+
const contained = this.getOutgoingLinks(id).filter(l => l.type === 'contains');
|
|
853
|
+
for (const cl of contained) {
|
|
854
|
+
const mIn = this.getIncomingLinks(cl.toId).filter(l => l.type !== 'contains');
|
|
855
|
+
const mOut = this.getOutgoingLinks(cl.toId).filter(l => l.type !== 'contains');
|
|
856
|
+
for (const link of mIn)
|
|
857
|
+
sig.add(link.fromId);
|
|
858
|
+
for (const link of mOut)
|
|
859
|
+
sig.add(link.toId);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return sig;
|
|
863
|
+
};
|
|
864
|
+
const targetSig = buildSig(symbolId);
|
|
746
865
|
// Search across all exported symbols, not just same-file siblings
|
|
747
866
|
const allSymbols = this.getAllSymbols().filter(s => s.id !== symbolId && s.exported);
|
|
748
867
|
const results = [];
|
|
749
868
|
for (const candidate of allSymbols) {
|
|
750
|
-
const
|
|
751
|
-
const candOutgoing = this.getOutgoingLinks(candidate.id).filter(l => l.type !== 'contains');
|
|
752
|
-
const candidateLinks = new Set();
|
|
753
|
-
for (const link of candIncoming)
|
|
754
|
-
candidateLinks.add(link.fromId);
|
|
755
|
-
for (const link of candOutgoing)
|
|
756
|
-
candidateLinks.add(link.toId);
|
|
869
|
+
const candidateSig = buildSig(candidate.id);
|
|
757
870
|
let intersection = 0;
|
|
758
|
-
for (const id of
|
|
759
|
-
if (
|
|
871
|
+
for (const id of targetSig) {
|
|
872
|
+
if (candidateSig.has(id))
|
|
760
873
|
intersection++;
|
|
761
874
|
}
|
|
762
|
-
const union = new Set([...
|
|
875
|
+
const union = new Set([...targetSig, ...candidateSig]).size;
|
|
763
876
|
const similarity = union > 0 ? intersection / union : 0;
|
|
764
877
|
if (similarity >= 0.15) {
|
|
765
878
|
results.push({ symbol: candidate, similarity: Math.round(similarity * 100) / 100 });
|
|
@@ -778,7 +891,7 @@ export class Database {
|
|
|
778
891
|
const expiresAt = ttlHours
|
|
779
892
|
? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
|
|
780
893
|
: null;
|
|
781
|
-
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)
|
|
782
895
|
VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
|
|
783
896
|
return result.lastInsertRowid;
|
|
784
897
|
}
|
|
@@ -833,12 +946,12 @@ export class Database {
|
|
|
833
946
|
this.db.prepare('INSERT INTO metric_history (metric_name, value) VALUES (?, ?)').run(name, value);
|
|
834
947
|
}
|
|
835
948
|
getMetricHistory(name, daysBack = 30) {
|
|
836
|
-
return this.db.prepare(`SELECT value, recorded_at as recordedAt FROM metric_history
|
|
837
|
-
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', ?)
|
|
838
951
|
ORDER BY recorded_at DESC`).all(name, `-${daysBack} days`);
|
|
839
952
|
}
|
|
840
953
|
getMetricTrend(name) {
|
|
841
|
-
const rows = this.db.prepare(`SELECT value FROM metric_history
|
|
954
|
+
const rows = this.db.prepare(`SELECT value FROM metric_history
|
|
842
955
|
WHERE metric_name = ? ORDER BY recorded_at DESC LIMIT 2`).all(name);
|
|
843
956
|
if (rows.length === 0)
|
|
844
957
|
return { current: 0, previous: null, change: null };
|