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.
Files changed (52) hide show
  1. package/.agents/skills/adapters/SKILL.md +59 -59
  2. package/.agents/skills/analyzer/SKILL.md +83 -83
  3. package/.agents/skills/apps/SKILL.md +65 -65
  4. package/.agents/skills/docs/SKILL.md +63 -63
  5. package/.agents/skills/milens/SKILL.md +322 -322
  6. package/.agents/skills/milens-security-review/SKILL.md +224 -224
  7. package/.agents/skills/orchestrator/SKILL.md +64 -64
  8. package/.agents/skills/parser/SKILL.md +86 -86
  9. package/.agents/skills/root/SKILL.md +89 -89
  10. package/.agents/skills/scripts/SKILL.md +56 -56
  11. package/.agents/skills/security/SKILL.md +67 -67
  12. package/.agents/skills/server/SKILL.md +84 -84
  13. package/.agents/skills/store/SKILL.md +77 -77
  14. package/.agents/skills/test/SKILL.md +155 -155
  15. package/.agents/skills/ui/SKILL.md +56 -56
  16. package/README.md +577 -577
  17. package/adapters/README.md +144 -144
  18. package/adapters/claude-code/.claude/mcp.json +9 -9
  19. package/adapters/claude-code/.claude/settings.json.hooks-snippet.json +26 -26
  20. package/adapters/claude-code/.claude-plugin/plugin.json +11 -11
  21. package/adapters/claude-code/.mcp.json +9 -9
  22. package/adapters/claude-code/CLAUDE.md +81 -81
  23. package/adapters/claude-code/hooks/hooks.json +27 -27
  24. package/adapters/codex/.codex/config.toml +3 -3
  25. package/adapters/copilot/.vscode/mcp.json +10 -10
  26. package/adapters/cursor/.cursor/mcp.json +9 -9
  27. package/adapters/cursor/.cursorrules +69 -69
  28. package/adapters/gemini/.gemini/context.md +81 -81
  29. package/adapters/gemini/.gemini/settings.json +9 -9
  30. package/adapters/opencode/.opencode/config.json +8 -8
  31. package/adapters/zed/.zed/settings.json +8 -8
  32. package/dist/build-info.d.ts +2 -2
  33. package/dist/build-info.js +2 -2
  34. package/dist/cli.js +482 -482
  35. package/dist/parser/lang-css.js +9 -9
  36. package/dist/parser/lang-go.js +58 -58
  37. package/dist/parser/lang-html.js +3 -3
  38. package/dist/parser/lang-java.js +37 -37
  39. package/dist/parser/lang-js.js +105 -105
  40. package/dist/parser/lang-php.js +45 -45
  41. package/dist/parser/lang-py.js +43 -43
  42. package/dist/parser/lang-ruby.js +28 -28
  43. package/dist/parser/lang-rust.js +48 -48
  44. package/dist/parser/lang-ts.js +210 -210
  45. package/dist/server/mcp-prompts.js +502 -502
  46. package/dist/server/mcp.js +56 -56
  47. package/dist/skills.js +296 -296
  48. package/dist/store/annotations.js +1 -1
  49. package/dist/store/db.js +224 -224
  50. package/dist/store/schema.sql +144 -144
  51. package/dist/store/vectors.js +2 -2
  52. 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 };