milens 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/skills/adapters/SKILL.md +31 -0
  2. package/.agents/skills/analyzer/SKILL.md +55 -0
  3. package/.agents/skills/apps/SKILL.md +42 -0
  4. package/.agents/skills/docs/SKILL.md +46 -0
  5. package/.agents/skills/milens/SKILL.md +168 -0
  6. package/.agents/skills/milens-code-review/SKILL.md +186 -0
  7. package/.agents/skills/milens-eval/SKILL.md +221 -0
  8. package/.agents/skills/milens-plan/SKILL.md +227 -0
  9. package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
  10. package/.agents/skills/milens-security-review/SKILL.md +224 -0
  11. package/.agents/skills/milens-tdd/SKILL.md +156 -0
  12. package/.agents/skills/parser/SKILL.md +60 -0
  13. package/.agents/skills/root/SKILL.md +64 -0
  14. package/.agents/skills/scripts/SKILL.md +27 -0
  15. package/.agents/skills/security/SKILL.md +44 -0
  16. package/.agents/skills/server/SKILL.md +46 -0
  17. package/.agents/skills/store/SKILL.md +53 -0
  18. package/.agents/skills/test/SKILL.md +73 -0
  19. package/LICENSE +75 -75
  20. package/README.md +524 -305
  21. package/adapters/README.md +107 -0
  22. package/adapters/claude-code/.claude/mcp.json +9 -0
  23. package/adapters/claude-code/CLAUDE.md +58 -0
  24. package/adapters/codex/.codex/codex.md +52 -0
  25. package/adapters/copilot/.github/copilot-instructions.md +62 -0
  26. package/adapters/cursor/.cursorrules +9 -0
  27. package/adapters/gemini/.gemini/context.md +58 -0
  28. package/adapters/opencode/.opencode/config.json +9 -0
  29. package/adapters/opencode/AGENTS.md +58 -0
  30. package/adapters/zed/.zed/settings.json +8 -0
  31. package/dist/agents-md.d.ts +3 -0
  32. package/dist/agents-md.d.ts.map +1 -0
  33. package/dist/agents-md.js +112 -0
  34. package/dist/agents-md.js.map +1 -0
  35. package/dist/analyzer/engine.d.ts +1 -0
  36. package/dist/analyzer/engine.d.ts.map +1 -1
  37. package/dist/analyzer/engine.js +27 -8
  38. package/dist/analyzer/engine.js.map +1 -1
  39. package/dist/analyzer/review.d.ts +23 -0
  40. package/dist/analyzer/review.d.ts.map +1 -0
  41. package/dist/analyzer/review.js +143 -0
  42. package/dist/analyzer/review.js.map +1 -0
  43. package/dist/analyzer/testplan.d.ts +59 -0
  44. package/dist/analyzer/testplan.d.ts.map +1 -0
  45. package/dist/analyzer/testplan.js +218 -0
  46. package/dist/analyzer/testplan.js.map +1 -0
  47. package/dist/cli.js +1192 -401
  48. package/dist/cli.js.map +1 -1
  49. package/dist/metrics.d.ts +51 -0
  50. package/dist/metrics.d.ts.map +1 -0
  51. package/dist/metrics.js +64 -0
  52. package/dist/metrics.js.map +1 -0
  53. package/dist/parser/extract.d.ts +1 -0
  54. package/dist/parser/extract.d.ts.map +1 -1
  55. package/dist/parser/extract.js +8 -0
  56. package/dist/parser/extract.js.map +1 -1
  57. package/dist/parser/lang-go.d.ts.map +1 -1
  58. package/dist/parser/lang-go.js +75 -39
  59. package/dist/parser/lang-go.js.map +1 -1
  60. package/dist/parser/lang-java.d.ts.map +1 -1
  61. package/dist/parser/lang-java.js +30 -29
  62. package/dist/parser/lang-java.js.map +1 -1
  63. package/dist/parser/lang-js.js +105 -105
  64. package/dist/parser/lang-php.js +38 -38
  65. package/dist/parser/lang-py.d.ts.map +1 -1
  66. package/dist/parser/lang-py.js +53 -31
  67. package/dist/parser/lang-py.js.map +1 -1
  68. package/dist/parser/lang-ruby.d.ts.map +1 -1
  69. package/dist/parser/lang-ruby.js +15 -14
  70. package/dist/parser/lang-ruby.js.map +1 -1
  71. package/dist/parser/lang-rust.js +30 -30
  72. package/dist/parser/lang-ts.js +191 -191
  73. package/dist/security/deps.d.ts +38 -0
  74. package/dist/security/deps.d.ts.map +1 -0
  75. package/dist/security/deps.js +685 -0
  76. package/dist/security/deps.js.map +1 -0
  77. package/dist/security/rules.d.ts +42 -0
  78. package/dist/security/rules.d.ts.map +1 -0
  79. package/dist/security/rules.js +940 -0
  80. package/dist/security/rules.js.map +1 -0
  81. package/dist/server/hooks.d.ts +26 -0
  82. package/dist/server/hooks.d.ts.map +1 -0
  83. package/dist/server/hooks.js +253 -0
  84. package/dist/server/hooks.js.map +1 -0
  85. package/dist/server/mcp-prompts.d.ts +277 -0
  86. package/dist/server/mcp-prompts.d.ts.map +1 -0
  87. package/dist/server/mcp-prompts.js +627 -0
  88. package/dist/server/mcp-prompts.js.map +1 -0
  89. package/dist/server/mcp.d.ts.map +1 -1
  90. package/dist/server/mcp.js +520 -36
  91. package/dist/server/mcp.js.map +1 -1
  92. package/dist/server/test-plan.d.ts +20 -0
  93. package/dist/server/test-plan.d.ts.map +1 -0
  94. package/dist/server/test-plan.js +100 -0
  95. package/dist/server/test-plan.js.map +1 -0
  96. package/dist/skills.js +152 -120
  97. package/dist/skills.js.map +1 -1
  98. package/dist/store/annotations.d.ts +41 -0
  99. package/dist/store/annotations.d.ts.map +1 -0
  100. package/dist/store/annotations.js +192 -0
  101. package/dist/store/annotations.js.map +1 -0
  102. package/dist/store/confidence.d.ts +18 -0
  103. package/dist/store/confidence.d.ts.map +1 -0
  104. package/dist/store/confidence.js +82 -0
  105. package/dist/store/confidence.js.map +1 -0
  106. package/dist/store/db.d.ts +68 -1
  107. package/dist/store/db.d.ts.map +1 -1
  108. package/dist/store/db.js +349 -139
  109. package/dist/store/db.js.map +1 -1
  110. package/dist/store/schema.sql +128 -83
  111. package/dist/store/vectors.d.ts +65 -0
  112. package/dist/store/vectors.d.ts.map +1 -0
  113. package/dist/store/vectors.js +212 -0
  114. package/dist/store/vectors.js.map +1 -0
  115. package/dist/types.d.ts +101 -0
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/utils.d.ts +3 -0
  118. package/dist/utils.d.ts.map +1 -0
  119. package/dist/utils.js +9 -0
  120. package/dist/utils.js.map +1 -0
  121. package/docs/README.md +24 -0
  122. package/docs/diagram2.svg +1 -1
  123. package/package.json +80 -65
package/dist/store/db.js CHANGED
@@ -7,6 +7,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  export class Database {
8
8
  db;
9
9
  stmts;
10
+ /** Access the raw better-sqlite3 connection (for AnnotationStore etc.) */
11
+ get connection() {
12
+ return this.db;
13
+ }
10
14
  constructor(dbPath) {
11
15
  this.db = new BetterSqlite3(dbPath);
12
16
  this.db.pragma('journal_mode = WAL');
@@ -21,48 +25,62 @@ export class Database {
21
25
  prepareStatements() {
22
26
  return {
23
27
  checkHash: this.db.prepare('SELECT hash FROM file_hashes WHERE path = ?'),
24
- upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
28
+ upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
25
29
  ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, analyzed_at = datetime('now')`),
26
- insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
30
+ insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
27
31
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
28
32
  updateMeta: this.db.prepare(`UPDATE symbols SET role = ?, heat = ? WHERE id = ?`),
29
33
  upsertZone: this.db.prepare(`UPDATE file_hashes SET zone = ? WHERE path = ?`),
30
- insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
34
+ insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
31
35
  VALUES (?, ?, ?, ?, ?, ?)`),
32
- searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
33
- JOIN symbols s ON s.rowid = f.rowid
34
- WHERE symbol_fts MATCH ?
36
+ searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
37
+ JOIN symbols s ON s.rowid = f.rowid
38
+ WHERE symbol_fts MATCH ?
35
39
  ORDER BY rank LIMIT ?`),
36
40
  byName: this.db.prepare('SELECT * FROM symbols WHERE name = ?'),
37
41
  byId: this.db.prepare('SELECT * FROM symbols WHERE id = ?'),
38
42
  byFile: this.db.prepare('SELECT * FROM symbols WHERE file_path = ?'),
39
43
  linksIn: this.db.prepare('SELECT * FROM links WHERE to_id = ?'),
40
44
  linksOut: this.db.prepare('SELECT * FROM links WHERE from_id = ?'),
41
- upstream: this.db.prepare(`
42
- WITH RECURSIVE upstream(id, depth, via) AS (
43
- SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
44
- UNION
45
- SELECT l.from_id, u.depth + 1, l.type
46
- FROM links l JOIN upstream u ON l.to_id = u.id
47
- WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
48
- )
49
- SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
45
+ upstream: this.db.prepare(`
46
+ WITH RECURSIVE upstream(id, depth, via) AS (
47
+ SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
48
+ UNION
49
+ SELECT l.from_id, u.depth + 1, l.type
50
+ FROM links l JOIN upstream u ON l.to_id = u.id
51
+ WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
52
+ )
53
+ SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
50
54
  `),
51
- downstream: this.db.prepare(`
52
- WITH RECURSIVE downstream(id, depth, via) AS (
53
- SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
54
- UNION
55
- SELECT l.to_id, d.depth + 1, l.type
56
- FROM links l JOIN downstream d ON l.from_id = d.id
57
- WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
58
- )
59
- SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
55
+ downstream: this.db.prepare(`
56
+ WITH RECURSIVE downstream(id, depth, via) AS (
57
+ SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
58
+ UNION
59
+ SELECT l.to_id, d.depth + 1, l.type
60
+ FROM links l JOIN downstream d ON l.from_id = d.id
61
+ WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
62
+ )
63
+ SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
60
64
  `),
61
65
  countSymbols: this.db.prepare('SELECT COUNT(*) as c FROM symbols'),
62
66
  countLinks: this.db.prepare('SELECT COUNT(*) as c FROM links'),
63
67
  countFiles: this.db.prepare('SELECT COUNT(*) as c FROM file_hashes'),
64
68
  deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?)'),
65
69
  deleteFileSymbols: this.db.prepare('DELETE FROM symbols WHERE file_path = ?'),
70
+ topHubs: this.db.prepare('SELECT * FROM symbols WHERE exported = 1 ORDER BY heat DESC LIMIT ?'),
71
+ testCoverageGaps: this.db.prepare(`
72
+ SELECT s.* FROM symbols s
73
+ WHERE s.exported = 1 AND s.heat > 0
74
+ AND s.id NOT IN (
75
+ SELECT DISTINCT l.to_id FROM links l
76
+ JOIN symbols src ON src.id = l.from_id
77
+ WHERE (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
78
+ OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
79
+ )
80
+ ORDER BY s.heat DESC
81
+ LIMIT ?
82
+ `),
83
+ annotationCount: this.db.prepare('SELECT COUNT(*) as c FROM annotations'),
66
84
  };
67
85
  }
68
86
  applySchema() {
@@ -176,38 +194,20 @@ export class Database {
176
194
  return rows.map(rowToLink);
177
195
  }
178
196
  findDeadCode(kind, limit = 50) {
179
- // Exclude: section symbols (markdown headings), test fixtures (not real production code),
180
- // framework entry-point files (consumed by runtime, not imported by project code),
181
- // and config files (consumed by CLI tooling)
182
- const excludeClause = `AND s.kind != 'section'
183
- AND s.file_path NOT LIKE 'test/fixtures/%'
184
- AND s.file_path NOT LIKE '%/page.ts' AND s.file_path NOT LIKE '%/page.tsx'
185
- AND s.file_path NOT LIKE '%/page.js' AND s.file_path NOT LIKE '%/page.jsx'
186
- AND s.file_path NOT LIKE '%/layout.ts' AND s.file_path NOT LIKE '%/layout.tsx'
187
- AND s.file_path NOT LIKE '%/layout.js' AND s.file_path NOT LIKE '%/layout.jsx'
188
- AND s.file_path NOT LIKE '%/loading.ts' AND s.file_path NOT LIKE '%/loading.tsx'
189
- AND s.file_path NOT LIKE '%/error.ts' AND s.file_path NOT LIKE '%/error.tsx'
190
- AND s.file_path NOT LIKE '%/not-found.ts' AND s.file_path NOT LIKE '%/not-found.tsx'
191
- AND s.file_path NOT LIKE '%/template.ts' AND s.file_path NOT LIKE '%/template.tsx'
192
- AND s.file_path NOT LIKE '%/route.ts' AND s.file_path NOT LIKE '%/route.tsx'
193
- AND s.file_path NOT LIKE '%/route.js' AND s.file_path NOT LIKE '%/route.jsx'
194
- AND s.file_path NOT LIKE '%/default.ts' AND s.file_path NOT LIKE '%/default.tsx'
195
- AND s.file_path NOT LIKE '%/global-error.ts' AND s.file_path NOT LIKE '%/global-error.tsx'
196
- AND s.file_path NOT LIKE '%/middleware.ts' AND s.file_path NOT LIKE '%/middleware.js'
197
- AND s.file_path NOT LIKE '%.config.ts' AND s.file_path NOT LIKE '%.config.js'
198
- AND s.file_path NOT LIKE '%.config.mjs' AND s.file_path NOT LIKE '%.config.cjs'
199
- AND s.file_path NOT LIKE '%/+page.svelte' AND s.file_path NOT LIKE '%/+page.ts'
200
- AND s.file_path NOT LIKE '%/+page.server.ts' AND s.file_path NOT LIKE '%/+layout.svelte'
201
- AND s.file_path NOT LIKE '%/+layout.ts' AND s.file_path NOT LIKE '%/+layout.server.ts'
202
- AND s.file_path NOT LIKE '%/+server.ts'`;
197
+ const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE 'app/%/layout.%'
198
+ AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE 'app/layout.%'
199
+ AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE 'jest.config.%'
200
+ AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE 'src/routes/+layout.%'`;
203
201
  const sql = kind
204
- ? `SELECT s.* FROM symbols s
205
- LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
206
- WHERE s.exported = 1 AND s.kind = ? ${excludeClause} AND l.id IS NULL
202
+ ? `SELECT s.* FROM symbols s
203
+ LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
204
+ WHERE s.exported = 1 AND s.kind = ? AND l.id IS NULL
205
+ ${frameworkExclude}
207
206
  LIMIT ?`
208
- : `SELECT s.* FROM symbols s
209
- LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
210
- WHERE s.exported = 1 ${excludeClause} AND l.id IS NULL
207
+ : `SELECT s.* FROM symbols s
208
+ LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
209
+ WHERE s.exported = 1 AND l.id IS NULL
210
+ ${frameworkExclude}
211
211
  LIMIT ?`;
212
212
  const rows = kind
213
213
  ? this.db.prepare(sql).all(kind, limit)
@@ -215,25 +215,25 @@ export class Database {
215
215
  return rows.map(rowToSymbol);
216
216
  }
217
217
  getTypeHierarchy(symbolId) {
218
- const ancestors = this.db.prepare(`
219
- WITH RECURSIVE up(id, depth) AS (
220
- SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
221
- UNION
222
- SELECT l.to_id, u.depth + 1
223
- FROM links l JOIN up u ON l.from_id = u.id
224
- WHERE l.type IN ('extends', 'implements') AND u.depth < 10
225
- )
226
- SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
218
+ const ancestors = this.db.prepare(`
219
+ WITH RECURSIVE up(id, depth) AS (
220
+ SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
221
+ UNION
222
+ SELECT l.to_id, u.depth + 1
223
+ FROM links l JOIN up u ON l.from_id = u.id
224
+ WHERE l.type IN ('extends', 'implements') AND u.depth < 10
225
+ )
226
+ SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
227
227
  `).all(symbolId);
228
- const descendants = this.db.prepare(`
229
- WITH RECURSIVE down(id, depth) AS (
230
- SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
231
- UNION
232
- SELECT l.from_id, d.depth + 1
233
- FROM links l JOIN down d ON l.to_id = d.id
234
- WHERE l.type IN ('extends', 'implements') AND d.depth < 10
235
- )
236
- SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
228
+ const descendants = this.db.prepare(`
229
+ WITH RECURSIVE down(id, depth) AS (
230
+ SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
231
+ UNION
232
+ SELECT l.from_id, d.depth + 1
233
+ FROM links l JOIN down d ON l.to_id = d.id
234
+ WHERE l.type IN ('extends', 'implements') AND d.depth < 10
235
+ )
236
+ SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
237
237
  `).all(symbolId);
238
238
  return {
239
239
  ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
@@ -248,15 +248,15 @@ export class Database {
248
248
  const fromId = fromSyms[0].id;
249
249
  const toIds = new Set(toSyms.map(s => s.id));
250
250
  // BFS outgoing from source
251
- const rows = this.db.prepare(`
252
- WITH RECURSIVE path(id, depth, via) AS (
253
- SELECT to_id, 1, type FROM links WHERE from_id = ? AND type != 'contains'
254
- UNION
255
- SELECT l.to_id, p.depth + 1, l.type
256
- FROM links l JOIN path p ON l.from_id = p.id
257
- WHERE l.type != 'contains' AND p.depth < ?
258
- )
259
- SELECT DISTINCT s.*, p.depth, p.via FROM path p JOIN symbols s ON s.id = p.id ORDER BY p.depth
251
+ const rows = this.db.prepare(`
252
+ WITH RECURSIVE path(id, depth, via) AS (
253
+ SELECT to_id, 1, type FROM links WHERE from_id = ? AND type != 'contains'
254
+ UNION
255
+ SELECT l.to_id, p.depth + 1, l.type
256
+ FROM links l JOIN path p ON l.from_id = p.id
257
+ WHERE l.type != 'contains' AND p.depth < ?
258
+ )
259
+ SELECT DISTINCT s.*, p.depth, p.via FROM path p JOIN symbols s ON s.id = p.id ORDER BY p.depth
260
260
  `).all(fromId, maxDepth);
261
261
  const result = [];
262
262
  for (const r of rows) {
@@ -309,14 +309,14 @@ export class Database {
309
309
  };
310
310
  }
311
311
  getConfidenceDistribution() {
312
- const rows = this.db.prepare(`
313
- SELECT
314
- SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
315
- SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
316
- SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
317
- COUNT(*) as total
318
- FROM links
319
- WHERE type != 'contains'
312
+ const rows = this.db.prepare(`
313
+ SELECT
314
+ SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
315
+ SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
316
+ SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
317
+ COUNT(*) as total
318
+ FROM links
319
+ WHERE type != 'contains'
320
320
  `).get();
321
321
  return {
322
322
  high: rows?.high ?? 0,
@@ -369,34 +369,34 @@ export class Database {
369
369
  // ── Route/endpoint detection via link patterns ──
370
370
  getEntrypoints() {
371
371
  // Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
372
- const rows = this.db.prepare(`
373
- SELECT s.* FROM symbols s
374
- WHERE s.exported = 1
375
- AND s.role = 'entrypoint'
376
- ORDER BY s.heat DESC
377
- LIMIT 50
372
+ const rows = this.db.prepare(`
373
+ SELECT s.* FROM symbols s
374
+ WHERE s.exported = 1
375
+ AND s.role = 'entrypoint'
376
+ ORDER BY s.heat DESC
377
+ LIMIT 50
378
378
  `).all();
379
379
  return rows.map(rowToSymbol);
380
380
  }
381
381
  // ── Domain clustering stats ──
382
382
  getDomainStats() {
383
- const rows = this.db.prepare(`
384
- SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
385
- COUNT(s.id) AS symbol_count
386
- FROM file_hashes fh
387
- LEFT JOIN symbols s ON s.file_path = fh.path
388
- WHERE fh.zone IS NOT NULL
389
- GROUP BY fh.zone
390
- ORDER BY symbol_count DESC
383
+ const rows = this.db.prepare(`
384
+ SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
385
+ COUNT(s.id) AS symbol_count
386
+ FROM file_hashes fh
387
+ LEFT JOIN symbols s ON s.file_path = fh.path
388
+ WHERE fh.zone IS NOT NULL
389
+ GROUP BY fh.zone
390
+ ORDER BY symbol_count DESC
391
391
  `).all();
392
392
  return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
393
393
  }
394
394
  // ── Staleness detection ──
395
395
  getStaleFiles(hoursOld = 24) {
396
- const rows = this.db.prepare(`
397
- SELECT path FROM file_hashes
398
- WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
399
- ORDER BY analyzed_at ASC
396
+ const rows = this.db.prepare(`
397
+ SELECT path FROM file_hashes
398
+ WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
399
+ ORDER BY analyzed_at ASC
400
400
  `).all(hoursOld);
401
401
  return rows.map((r) => r.path);
402
402
  }
@@ -419,40 +419,46 @@ export class Database {
419
419
  }
420
420
  // ── Tool usage tracking ──
421
421
  logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
422
- this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
422
+ this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
423
423
  VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
424
424
  }
425
- getToolUsageStats() {
426
- const totals = this.db.prepare(`
427
- SELECT COUNT(*) as total_calls,
428
- COALESCE(SUM(tokens_saved), 0) as total_saved,
429
- COALESCE(SUM(tokens_out), 0) as total_out,
430
- COALESCE(SUM(duration_ms), 0) as total_ms
431
- FROM tool_usage
432
- `).get();
433
- const byTool = this.db.prepare(`
434
- SELECT tool, COUNT(*) as calls,
435
- COALESCE(SUM(tokens_saved), 0) as tokens_saved,
436
- COALESCE(SUM(tokens_out), 0) as tokens_out,
437
- CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
438
- FROM tool_usage
439
- GROUP BY tool
440
- ORDER BY calls DESC
441
- `).all();
442
- const byDay = this.db.prepare(`
443
- SELECT date(called_at) as date, COUNT(*) as calls,
444
- COALESCE(SUM(tokens_saved), 0) as tokens_saved
445
- FROM tool_usage
446
- GROUP BY date(called_at)
447
- ORDER BY date DESC
448
- LIMIT 30
449
- `).all();
450
- const recentCalls = this.db.prepare(`
451
- SELECT tool, called_at, duration_ms, tokens_saved
452
- FROM tool_usage
453
- ORDER BY id DESC
454
- LIMIT 50
455
- `).all();
425
+ getToolUsageStats(repo) {
426
+ const repoFilter = repo ? `WHERE repo = ?` : '';
427
+ const repoParam = repo ? [repo] : [];
428
+ const totals = this.db.prepare(`
429
+ SELECT COUNT(*) as total_calls,
430
+ COALESCE(SUM(tokens_saved), 0) as total_saved,
431
+ COALESCE(SUM(tokens_out), 0) as total_out,
432
+ COALESCE(SUM(duration_ms), 0) as total_ms
433
+ FROM tool_usage
434
+ ${repoFilter}
435
+ `).get(...repoParam);
436
+ const byTool = this.db.prepare(`
437
+ SELECT tool, COUNT(*) as calls,
438
+ COALESCE(SUM(tokens_saved), 0) as tokens_saved,
439
+ COALESCE(SUM(tokens_out), 0) as tokens_out,
440
+ CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
441
+ FROM tool_usage
442
+ ${repoFilter}
443
+ GROUP BY tool
444
+ ORDER BY calls DESC
445
+ `).all(...repoParam);
446
+ const byDay = this.db.prepare(`
447
+ SELECT date(called_at) as date, COUNT(*) as calls,
448
+ COALESCE(SUM(tokens_saved), 0) as tokens_saved
449
+ FROM tool_usage
450
+ ${repoFilter}
451
+ GROUP BY date(called_at)
452
+ ORDER BY date DESC
453
+ LIMIT 30
454
+ `).all(...repoParam);
455
+ const recentCalls = this.db.prepare(`
456
+ SELECT tool, called_at, duration_ms, tokens_saved
457
+ FROM tool_usage
458
+ ${repoFilter}
459
+ ORDER BY id DESC
460
+ LIMIT 50
461
+ `).all(...repoParam);
456
462
  return {
457
463
  totalCalls: totals.total_calls,
458
464
  totalTokensSaved: totals.total_saved,
@@ -468,6 +474,210 @@ export class Database {
468
474
  })),
469
475
  };
470
476
  }
477
+ // ── Test file detection ──
478
+ isTestFile(filePath) {
479
+ return /[/\\]test[/\\]/.test(filePath) || /\.(test|spec)\./.test(filePath);
480
+ }
481
+ // ── Heat / hubs ──
482
+ getTopHubs(limit) {
483
+ const rows = this.stmts.topHubs.all(limit);
484
+ return rows.map(rowToSymbol);
485
+ }
486
+ // ── Test coverage ──
487
+ getSymbolTestCoverage(symbolId) {
488
+ const incomingLinks = this.getIncomingLinks(symbolId);
489
+ for (const link of incomingLinks) {
490
+ const sourceSymbol = this.findSymbolById(link.fromId);
491
+ if (sourceSymbol && this.isTestFile(sourceSymbol.filePath)) {
492
+ return true;
493
+ }
494
+ }
495
+ return false;
496
+ }
497
+ getTestedSymbolIds(candidateIds, isTestFileFn) {
498
+ if (candidateIds.length === 0)
499
+ return new Set();
500
+ const placeholders = candidateIds.map(() => '?').join(',');
501
+ const rows = this.db.prepare(`
502
+ SELECT DISTINCT l.to_id
503
+ FROM links l JOIN symbols src ON src.id = l.from_id
504
+ WHERE l.to_id IN (${placeholders}) AND l.type != 'contains'
505
+ `).all(...candidateIds);
506
+ const testedIds = new Set();
507
+ for (const row of rows) {
508
+ const src = this.findSymbolById(row.to_id);
509
+ if (src && isTestFileFn(src.filePath))
510
+ continue;
511
+ // Check each from_id for test files
512
+ const links = this.getIncomingLinks(row.to_id);
513
+ for (const l of links) {
514
+ if (l.type === 'contains')
515
+ continue;
516
+ const from = this.findSymbolById(l.fromId);
517
+ if (from && isTestFileFn(from.filePath)) {
518
+ testedIds.add(row.to_id);
519
+ break;
520
+ }
521
+ }
522
+ }
523
+ // Also include ids not in the result set (no incoming links)
524
+ const inResult = new Set(rows.map(r => r.to_id));
525
+ for (const id of candidateIds) {
526
+ if (!inResult.has(id))
527
+ continue; // doesn't exist
528
+ }
529
+ return testedIds;
530
+ }
531
+ getTestCoverageGaps(limit) {
532
+ const rows = this.stmts.testCoverageGaps.all(limit);
533
+ return rows.map(rowToSymbol);
534
+ }
535
+ getTestImpact(changedSymbolIds) {
536
+ if (changedSymbolIds.length === 0)
537
+ return { testFiles: [], changedSymbols: [] };
538
+ const placeholders = changedSymbolIds.map(() => '?').join(',');
539
+ const testFilesDirect = this.db.prepare(`
540
+ SELECT DISTINCT src.file_path
541
+ FROM links l
542
+ JOIN symbols src ON src.id = l.from_id
543
+ WHERE l.to_id IN (${placeholders})
544
+ AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
545
+ OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
546
+ `).all(...changedSymbolIds);
547
+ const testFilesUpstream = this.db.prepare(`
548
+ WITH RECURSIVE upstream(id, depth) AS (
549
+ SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
550
+ AND type IN ('calls', 'imports', 'extends', 'implements')
551
+ UNION
552
+ SELECT l.from_id, u.depth + 1
553
+ FROM links l JOIN upstream u ON l.to_id = u.id
554
+ WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
555
+ )
556
+ SELECT DISTINCT s.file_path FROM upstream u
557
+ JOIN symbols s ON s.id = u.id
558
+ WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
559
+ OR s.file_path LIKE '%.test.%' OR s.file_path LIKE '%.spec.%')
560
+ `).all(...changedSymbolIds);
561
+ const allTestFiles = [...new Set([
562
+ ...testFilesDirect.map((r) => r.file_path),
563
+ ...testFilesUpstream.map((r) => r.file_path),
564
+ ])];
565
+ const changedSymbols = changedSymbolIds.filter(id => this.getSymbolTestCoverage(id));
566
+ return { testFiles: allTestFiles, changedSymbols };
567
+ }
568
+ // ── Codebase summary ──
569
+ getCodebaseSummary() {
570
+ const stats = this.getStats();
571
+ const coverage = this.getTestCoverage();
572
+ const domains = this.getDomainStats();
573
+ const topHubs = this.getTopHubs(10);
574
+ const coveragePct = coverage.exportedProductionSymbols > 0
575
+ ? Math.round((coverage.testedSymbols / coverage.exportedProductionSymbols) * 100)
576
+ : 0;
577
+ return {
578
+ symbols: stats.symbols,
579
+ links: stats.links,
580
+ files: stats.files,
581
+ coveragePct,
582
+ testedSymbols: coverage.testedSymbols,
583
+ exportedSymbols: coverage.exportedProductionSymbols,
584
+ domains,
585
+ topHubs,
586
+ };
587
+ }
588
+ // ── Topological similarity ──
589
+ findTopologicallySimilar(symbolId, limit = 10) {
590
+ const target = this.findSymbolById(symbolId);
591
+ if (!target)
592
+ return [];
593
+ const incoming = this.getIncomingLinks(symbolId).filter(l => l.type !== 'contains');
594
+ const outgoing = this.getOutgoingLinks(symbolId).filter(l => l.type !== 'contains');
595
+ const targetLinks = new Set();
596
+ for (const link of incoming)
597
+ targetLinks.add(link.fromId);
598
+ for (const link of outgoing)
599
+ targetLinks.add(link.toId);
600
+ const siblings = this.getSymbolsByFile(target.filePath).filter(s => s.id !== symbolId);
601
+ const results = [];
602
+ for (const sibling of siblings) {
603
+ const sibIncoming = this.getIncomingLinks(sibling.id).filter(l => l.type !== 'contains');
604
+ const sibOutgoing = this.getOutgoingLinks(sibling.id).filter(l => l.type !== 'contains');
605
+ const siblingLinks = new Set();
606
+ for (const link of sibIncoming)
607
+ siblingLinks.add(link.fromId);
608
+ for (const link of sibOutgoing)
609
+ siblingLinks.add(link.toId);
610
+ let intersection = 0;
611
+ for (const id of targetLinks) {
612
+ if (siblingLinks.has(id))
613
+ intersection++;
614
+ }
615
+ const union = new Set([...targetLinks, ...siblingLinks]).size;
616
+ const similarity = union > 0 ? intersection / union : 0;
617
+ if (similarity >= 0.3) {
618
+ results.push({ symbol: sibling, similarity: Math.round(similarity * 100) / 100 });
619
+ }
620
+ }
621
+ return results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
622
+ }
623
+ // ── Annotations ──
624
+ getAnnotationCount() {
625
+ const row = this.stmts.annotationCount.get();
626
+ return row.c;
627
+ }
628
+ // ── Annotation/session API ──
629
+ getRawDb() { return this.db; }
630
+ addAnnotation(symbolId, key, value, agent, sessionId, ttlHours) {
631
+ const expiresAt = ttlHours
632
+ ? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
633
+ : null;
634
+ const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
635
+ VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
636
+ return result.lastInsertRowid;
637
+ }
638
+ getAnnotations(filters = {}) {
639
+ const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
640
+ const params = [];
641
+ if (filters.symbolId) {
642
+ clauses.push('symbol_id = ?');
643
+ params.push(filters.symbolId);
644
+ }
645
+ if (filters.key) {
646
+ clauses.push('key = ?');
647
+ params.push(filters.key);
648
+ }
649
+ if (filters.agent) {
650
+ clauses.push('agent = ?');
651
+ params.push(filters.agent);
652
+ }
653
+ if (filters.sessionId) {
654
+ clauses.push('session_id = ?');
655
+ params.push(filters.sessionId);
656
+ }
657
+ const sql = `SELECT *, symbol_id as symbolId, created_at as createdAt FROM annotations WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC` + (filters.limit ? ` LIMIT ${filters.limit}` : '');
658
+ const rows = this.db.prepare(sql).all(...params);
659
+ return rows.map((r) => ({ id: r.id, symbolId: r.symbol_id, key: r.key, value: r.value, agent: r.agent, sessionId: r.session_id, createdAt: r.created_at }));
660
+ }
661
+ getAnnotationsForSymbol(symbolId) {
662
+ return this.getAnnotations({ symbolId });
663
+ }
664
+ cleanupExpiredAnnotations() {
665
+ const result = this.db.prepare("DELETE FROM annotations WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')").run();
666
+ return result.changes;
667
+ }
668
+ startSession(id, agent, context) {
669
+ this.db.prepare('INSERT OR REPLACE INTO agent_sessions (id, agent, status, started_at, context_json) VALUES (?, ?, ?, datetime(\'now\'), ?)')
670
+ .run(id, agent, 'active', context ?? null);
671
+ }
672
+ getSession(id) {
673
+ const row = this.db.prepare('SELECT *, started_at as startedAt, ended_at as endedAt, context_json as context FROM agent_sessions WHERE id = ?').get(id);
674
+ if (!row)
675
+ return null;
676
+ return { id: row.id, agent: row.agent, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, context: row.context };
677
+ }
678
+ endSession(id, status) {
679
+ this.db.prepare("UPDATE agent_sessions SET status = ?, ended_at = datetime('now') WHERE id = ?").run(status, id);
680
+ }
471
681
  transaction(fn) {
472
682
  return this.db.transaction(fn)();
473
683
  }