@timmeck/brain 1.2.0 → 1.8.0

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 (131) hide show
  1. package/README.md +225 -50
  2. package/dist/api/server.d.ts +19 -0
  3. package/dist/api/server.js +281 -0
  4. package/dist/api/server.js.map +1 -0
  5. package/dist/brain.d.ts +3 -0
  6. package/dist/brain.js +45 -8
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +2 -0
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/explain.d.ts +2 -0
  11. package/dist/cli/commands/explain.js +76 -0
  12. package/dist/cli/commands/explain.js.map +1 -0
  13. package/dist/code/analyzer.d.ts +6 -0
  14. package/dist/code/analyzer.js +35 -0
  15. package/dist/code/analyzer.js.map +1 -1
  16. package/dist/code/matcher.d.ts +11 -1
  17. package/dist/code/matcher.js +49 -0
  18. package/dist/code/matcher.js.map +1 -1
  19. package/dist/code/scorer.d.ts +1 -0
  20. package/dist/code/scorer.js +15 -1
  21. package/dist/code/scorer.js.map +1 -1
  22. package/dist/config.js +31 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/dashboard/server.d.ts +15 -0
  25. package/dist/dashboard/server.js +124 -0
  26. package/dist/dashboard/server.js.map +1 -0
  27. package/dist/db/migrations/007_feedback.d.ts +2 -0
  28. package/dist/db/migrations/007_feedback.js +12 -0
  29. package/dist/db/migrations/007_feedback.js.map +1 -0
  30. package/dist/db/migrations/008_git_integration.d.ts +2 -0
  31. package/dist/db/migrations/008_git_integration.js +37 -0
  32. package/dist/db/migrations/008_git_integration.js.map +1 -0
  33. package/dist/db/migrations/009_embeddings.d.ts +2 -0
  34. package/dist/db/migrations/009_embeddings.js +7 -0
  35. package/dist/db/migrations/009_embeddings.js.map +1 -0
  36. package/dist/db/migrations/index.js +6 -0
  37. package/dist/db/migrations/index.js.map +1 -1
  38. package/dist/db/repositories/code-module.repository.d.ts +16 -0
  39. package/dist/db/repositories/code-module.repository.js +42 -0
  40. package/dist/db/repositories/code-module.repository.js.map +1 -1
  41. package/dist/db/repositories/error.repository.d.ts +5 -0
  42. package/dist/db/repositories/error.repository.js +27 -0
  43. package/dist/db/repositories/error.repository.js.map +1 -1
  44. package/dist/db/repositories/insight.repository.d.ts +2 -0
  45. package/dist/db/repositories/insight.repository.js +13 -0
  46. package/dist/db/repositories/insight.repository.js.map +1 -1
  47. package/dist/embeddings/engine.d.ts +42 -0
  48. package/dist/embeddings/engine.js +166 -0
  49. package/dist/embeddings/engine.js.map +1 -0
  50. package/dist/hooks/post-tool-use.js +2 -0
  51. package/dist/hooks/post-tool-use.js.map +1 -1
  52. package/dist/hooks/post-write.js +11 -0
  53. package/dist/hooks/post-write.js.map +1 -1
  54. package/dist/index.js +3 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/ipc/router.d.ts +2 -0
  57. package/dist/ipc/router.js +13 -0
  58. package/dist/ipc/router.js.map +1 -1
  59. package/dist/learning/confidence-scorer.d.ts +16 -0
  60. package/dist/learning/confidence-scorer.js +20 -0
  61. package/dist/learning/confidence-scorer.js.map +1 -1
  62. package/dist/learning/learning-engine.js +12 -5
  63. package/dist/learning/learning-engine.js.map +1 -1
  64. package/dist/matching/error-matcher.d.ts +9 -1
  65. package/dist/matching/error-matcher.js +50 -5
  66. package/dist/matching/error-matcher.js.map +1 -1
  67. package/dist/mcp/http-server.d.ts +14 -0
  68. package/dist/mcp/http-server.js +117 -0
  69. package/dist/mcp/http-server.js.map +1 -0
  70. package/dist/mcp/tools.d.ts +4 -0
  71. package/dist/mcp/tools.js +41 -14
  72. package/dist/mcp/tools.js.map +1 -1
  73. package/dist/services/analytics.service.d.ts +39 -0
  74. package/dist/services/analytics.service.js +111 -0
  75. package/dist/services/analytics.service.js.map +1 -1
  76. package/dist/services/code.service.d.ts +2 -0
  77. package/dist/services/code.service.js +62 -4
  78. package/dist/services/code.service.js.map +1 -1
  79. package/dist/services/error.service.d.ts +17 -1
  80. package/dist/services/error.service.js +90 -12
  81. package/dist/services/error.service.js.map +1 -1
  82. package/dist/services/git.service.d.ts +49 -0
  83. package/dist/services/git.service.js +112 -0
  84. package/dist/services/git.service.js.map +1 -0
  85. package/dist/services/prevention.service.d.ts +7 -0
  86. package/dist/services/prevention.service.js +38 -0
  87. package/dist/services/prevention.service.js.map +1 -1
  88. package/dist/services/research.service.d.ts +1 -0
  89. package/dist/services/research.service.js +4 -0
  90. package/dist/services/research.service.js.map +1 -1
  91. package/dist/services/solution.service.d.ts +10 -0
  92. package/dist/services/solution.service.js +48 -0
  93. package/dist/services/solution.service.js.map +1 -1
  94. package/dist/types/config.types.d.ts +21 -0
  95. package/dist/types/synapse.types.d.ts +1 -1
  96. package/package.json +8 -3
  97. package/src/api/server.ts +321 -0
  98. package/src/brain.ts +50 -8
  99. package/src/cli/commands/dashboard.ts +2 -0
  100. package/src/cli/commands/explain.ts +83 -0
  101. package/src/code/analyzer.ts +40 -0
  102. package/src/code/matcher.ts +67 -2
  103. package/src/code/scorer.ts +13 -1
  104. package/src/config.ts +24 -0
  105. package/src/dashboard/server.ts +142 -0
  106. package/src/db/migrations/007_feedback.ts +13 -0
  107. package/src/db/migrations/008_git_integration.ts +38 -0
  108. package/src/db/migrations/009_embeddings.ts +8 -0
  109. package/src/db/migrations/index.ts +6 -0
  110. package/src/db/repositories/code-module.repository.ts +53 -0
  111. package/src/db/repositories/error.repository.ts +40 -0
  112. package/src/db/repositories/insight.repository.ts +21 -0
  113. package/src/embeddings/engine.ts +217 -0
  114. package/src/hooks/post-tool-use.ts +2 -0
  115. package/src/hooks/post-write.ts +12 -0
  116. package/src/index.ts +3 -1
  117. package/src/ipc/router.ts +16 -0
  118. package/src/learning/confidence-scorer.ts +33 -0
  119. package/src/learning/learning-engine.ts +13 -5
  120. package/src/matching/error-matcher.ts +55 -4
  121. package/src/mcp/http-server.ts +137 -0
  122. package/src/mcp/tools.ts +52 -14
  123. package/src/services/analytics.service.ts +136 -0
  124. package/src/services/code.service.ts +87 -4
  125. package/src/services/error.service.ts +114 -13
  126. package/src/services/git.service.ts +132 -0
  127. package/src/services/prevention.service.ts +40 -0
  128. package/src/services/research.service.ts +5 -0
  129. package/src/services/solution.service.ts +58 -0
  130. package/src/types/config.types.ts +24 -0
  131. package/src/types/synapse.types.ts +1 -0
@@ -1,12 +1,12 @@
1
1
  import type { CodeModuleRecord } from '../types/code.types.js';
2
2
  import { fingerprintCode } from './fingerprint.js';
3
3
  import { tokenize } from '../matching/tokenizer.js';
4
- import { cosineSimilarity, jaccardSimilarity } from '../matching/similarity.js';
4
+ import { cosineSimilarity } from '../matching/similarity.js';
5
5
 
6
6
  export interface CodeMatchResult {
7
7
  moduleId: number;
8
8
  score: number;
9
- matchType: 'exact' | 'structural' | 'semantic';
9
+ matchType: 'exact' | 'structural' | 'semantic' | 'vector';
10
10
  }
11
11
 
12
12
  export function findExactMatches(
@@ -62,3 +62,68 @@ export function findSemanticMatches(
62
62
  .filter(r => r.score >= threshold)
63
63
  .sort((a, b) => b.score - a.score);
64
64
  }
65
+
66
+ /**
67
+ * Find matches using pre-computed vector embeddings.
68
+ * Vector scores are computed externally (by the EmbeddingEngine) and passed in.
69
+ */
70
+ export function findVectorMatches(
71
+ vectorScores: Map<number, number>,
72
+ threshold: number = 0.5,
73
+ ): CodeMatchResult[] {
74
+ const results: CodeMatchResult[] = [];
75
+
76
+ for (const [moduleId, score] of vectorScores) {
77
+ if (score >= threshold) {
78
+ results.push({ moduleId, score, matchType: 'vector' });
79
+ }
80
+ }
81
+
82
+ return results.sort((a, b) => b.score - a.score);
83
+ }
84
+
85
+ /**
86
+ * Hybrid search: combine structural + semantic + vector matches,
87
+ * deduplicating and taking the highest score per module.
88
+ */
89
+ export function findHybridMatches(
90
+ source: string,
91
+ language: string,
92
+ description: string,
93
+ candidates: CodeModuleRecord[],
94
+ vectorScores?: Map<number, number>,
95
+ ): CodeMatchResult[] {
96
+ const scoreMap = new Map<number, CodeMatchResult>();
97
+
98
+ // Structural matches (highest priority)
99
+ for (const match of findStructuralMatches(source, language, candidates, 0.5)) {
100
+ const existing = scoreMap.get(match.moduleId);
101
+ if (!existing || match.score > existing.score) {
102
+ scoreMap.set(match.moduleId, match);
103
+ }
104
+ }
105
+
106
+ // Semantic matches
107
+ for (const match of findSemanticMatches(description, candidates, 0.3)) {
108
+ const existing = scoreMap.get(match.moduleId);
109
+ if (!existing || match.score > existing.score) {
110
+ scoreMap.set(match.moduleId, match);
111
+ }
112
+ }
113
+
114
+ // Vector matches (if available)
115
+ if (vectorScores && vectorScores.size > 0) {
116
+ for (const match of findVectorMatches(vectorScores, 0.4)) {
117
+ const existing = scoreMap.get(match.moduleId);
118
+ if (!existing) {
119
+ scoreMap.set(match.moduleId, match);
120
+ } else {
121
+ // Boost existing matches that also have high vector similarity
122
+ const vectorBoost = match.score * 0.15;
123
+ existing.score = Math.min(1.0, existing.score + vectorBoost);
124
+ }
125
+ }
126
+ }
127
+
128
+ return [...scoreMap.values()].sort((a, b) => b.score - a.score);
129
+ }
@@ -6,6 +6,7 @@ export interface CodeUnitForScoring {
6
6
  exports: ExportInfo[];
7
7
  internalDeps: string[];
8
8
  hasTypeAnnotations: boolean;
9
+ complexity?: number;
9
10
  }
10
11
 
11
12
  interface ReusabilitySignal {
@@ -17,7 +18,7 @@ interface ReusabilitySignal {
17
18
  const REUSABILITY_SIGNALS: ReusabilitySignal[] = [
18
19
  {
19
20
  name: 'single_responsibility',
20
- weight: 0.25,
21
+ weight: 0.20,
21
22
  check: (code) => {
22
23
  const count = code.exports.length;
23
24
  if (count === 0) return 0;
@@ -88,6 +89,17 @@ const REUSABILITY_SIGNALS: ReusabilitySignal[] = [
88
89
  return 0.1;
89
90
  },
90
91
  },
92
+ {
93
+ name: 'low_complexity',
94
+ weight: 0.10,
95
+ check: (code) => {
96
+ const cc = code.complexity ?? 1;
97
+ if (cc <= 5) return 1.0;
98
+ if (cc <= 10) return 0.7;
99
+ if (cc <= 20) return 0.4;
100
+ return 0.1;
101
+ },
102
+ },
91
103
  ];
92
104
 
93
105
  export const MODULE_THRESHOLD = 0.60;
package/src/config.ts CHANGED
@@ -10,6 +10,21 @@ const defaults: BrainConfig = {
10
10
  pipeName: getPipeName(),
11
11
  timeout: 5000,
12
12
  },
13
+ api: {
14
+ port: 7777,
15
+ enabled: true,
16
+ },
17
+ mcpHttp: {
18
+ port: 7778,
19
+ enabled: true,
20
+ },
21
+ embeddings: {
22
+ enabled: true,
23
+ modelName: 'Xenova/all-MiniLM-L6-v2',
24
+ cacheDir: path.join(getDataDir(), 'models'),
25
+ sweepIntervalMs: 300_000, // 5 minutes
26
+ batchSize: 50,
27
+ },
13
28
  learning: {
14
29
  intervalMs: 900_000,
15
30
  minOccurrences: 3,
@@ -27,6 +42,8 @@ const defaults: BrainConfig = {
27
42
  fingerprintFields: ['type', 'message', 'file_path'],
28
43
  similarityThreshold: 0.8,
29
44
  maxResults: 10,
45
+ crossProjectMatching: true,
46
+ crossProjectWeight: 0.7,
30
47
  },
31
48
  code: {
32
49
  supportedLanguages: ['typescript', 'javascript', 'python', 'rust', 'go'],
@@ -74,6 +91,13 @@ function applyEnvOverrides(config: BrainConfig): void {
74
91
  if (process.env['BRAIN_DB_PATH']) config.dbPath = process.env['BRAIN_DB_PATH'];
75
92
  if (process.env['BRAIN_LOG_LEVEL']) config.log.level = process.env['BRAIN_LOG_LEVEL'];
76
93
  if (process.env['BRAIN_PIPE_NAME']) config.ipc.pipeName = process.env['BRAIN_PIPE_NAME'];
94
+ if (process.env['BRAIN_API_PORT']) config.api.port = Number(process.env['BRAIN_API_PORT']);
95
+ if (process.env['BRAIN_API_ENABLED']) config.api.enabled = process.env['BRAIN_API_ENABLED'] !== 'false';
96
+ if (process.env['BRAIN_API_KEY']) config.api.apiKey = process.env['BRAIN_API_KEY'];
97
+ if (process.env['BRAIN_MCP_HTTP_PORT']) config.mcpHttp.port = Number(process.env['BRAIN_MCP_HTTP_PORT']);
98
+ if (process.env['BRAIN_MCP_HTTP_ENABLED']) config.mcpHttp.enabled = process.env['BRAIN_MCP_HTTP_ENABLED'] !== 'false';
99
+ if (process.env['BRAIN_EMBEDDINGS_ENABLED']) config.embeddings.enabled = process.env['BRAIN_EMBEDDINGS_ENABLED'] !== 'false';
100
+ if (process.env['BRAIN_EMBEDDINGS_MODEL']) config.embeddings.modelName = process.env['BRAIN_EMBEDDINGS_MODEL'];
77
101
  }
78
102
 
79
103
  function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
@@ -0,0 +1,142 @@
1
+ import http from 'node:http';
2
+ import { getEventBus } from '../utils/events.js';
3
+ import { getLogger } from '../utils/logger.js';
4
+
5
+ export interface DashboardServerOptions {
6
+ port: number;
7
+ getDashboardHtml: () => string;
8
+ getStats: () => unknown;
9
+ }
10
+
11
+ export class DashboardServer {
12
+ private server: http.Server | null = null;
13
+ private clients: Set<http.ServerResponse> = new Set();
14
+ private logger = getLogger();
15
+
16
+ constructor(private options: DashboardServerOptions) {}
17
+
18
+ start(): void {
19
+ const { port, getDashboardHtml, getStats } = this.options;
20
+ const bus = getEventBus();
21
+
22
+ this.server = http.createServer((req, res) => {
23
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
24
+
25
+ // CORS
26
+ res.setHeader('Access-Control-Allow-Origin', '*');
27
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
28
+
29
+ if (req.method === 'OPTIONS') {
30
+ res.writeHead(204);
31
+ res.end();
32
+ return;
33
+ }
34
+
35
+ if (url.pathname === '/events') {
36
+ // SSE endpoint
37
+ res.writeHead(200, {
38
+ 'Content-Type': 'text/event-stream',
39
+ 'Cache-Control': 'no-cache',
40
+ 'Connection': 'keep-alive',
41
+ });
42
+ res.write('data: {"type":"connected"}\n\n');
43
+
44
+ this.clients.add(res);
45
+ req.on('close', () => this.clients.delete(res));
46
+ return;
47
+ }
48
+
49
+ if (url.pathname === '/api/stats') {
50
+ const stats = getStats();
51
+ res.writeHead(200, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify(stats));
53
+ return;
54
+ }
55
+
56
+ if (url.pathname === '/' || url.pathname === '/dashboard') {
57
+ const html = getDashboardHtml();
58
+ // Inject SSE script into the dashboard
59
+ const sseScript = `
60
+ <script>
61
+ (function(){
62
+ const evtSource = new EventSource('/events');
63
+ evtSource.onmessage = function(e) {
64
+ try {
65
+ const data = JSON.parse(e.data);
66
+ if (data.type === 'stats_update') {
67
+ // Update stat cards
68
+ document.querySelectorAll('.stat-number').forEach(el => {
69
+ const key = el.parentElement?.querySelector('.stat-label')?.textContent?.toLowerCase();
70
+ if (key && data.stats[key] !== undefined) {
71
+ el.textContent = Number(data.stats[key]).toLocaleString();
72
+ }
73
+ });
74
+ }
75
+ if (data.type === 'event') {
76
+ // Flash the activity dot
77
+ const dot = document.querySelector('.activity-dot');
78
+ if (dot) { dot.style.background = '#ff5577'; setTimeout(() => dot.style.background = '', 500); }
79
+ }
80
+ } catch {}
81
+ };
82
+ evtSource.onerror = function() { setTimeout(() => location.reload(), 5000); };
83
+ })();
84
+ </script>`;
85
+ const liveHtml = html.replace('</body>', sseScript + '</body>');
86
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
87
+ res.end(liveHtml);
88
+ return;
89
+ }
90
+
91
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
92
+ res.end('Not Found');
93
+ });
94
+
95
+ // Forward Brain events to SSE clients
96
+ const eventNames = [
97
+ 'error:reported', 'error:resolved', 'solution:applied',
98
+ 'solution:created', 'module:registered', 'module:updated',
99
+ 'synapse:created', 'synapse:strengthened',
100
+ 'insight:created', 'rule:learned',
101
+ ] as const;
102
+
103
+ for (const eventName of eventNames) {
104
+ bus.on(eventName, (data: unknown) => {
105
+ this.broadcast({ type: 'event', event: eventName, data });
106
+ });
107
+ }
108
+
109
+ // Periodic stats broadcast (every 30s)
110
+ setInterval(() => {
111
+ if (this.clients.size > 0) {
112
+ const stats = getStats();
113
+ this.broadcast({ type: 'stats_update', stats });
114
+ }
115
+ }, 30_000);
116
+
117
+ this.server.listen(port, () => {
118
+ this.logger.info(`Dashboard server started on http://localhost:${port}`);
119
+ });
120
+ }
121
+
122
+ stop(): void {
123
+ for (const client of this.clients) {
124
+ client.end();
125
+ }
126
+ this.clients.clear();
127
+ this.server?.close();
128
+ this.server = null;
129
+ this.logger.info('Dashboard server stopped');
130
+ }
131
+
132
+ private broadcast(data: unknown): void {
133
+ const msg = `data: ${JSON.stringify(data)}\n\n`;
134
+ for (const client of this.clients) {
135
+ try {
136
+ client.write(msg);
137
+ } catch {
138
+ this.clients.delete(client);
139
+ }
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,13 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ ALTER TABLE insights ADD COLUMN rating INTEGER DEFAULT NULL;
6
+ ALTER TABLE insights ADD COLUMN rating_comment TEXT DEFAULT NULL;
7
+ ALTER TABLE insights ADD COLUMN rated_at TEXT DEFAULT NULL;
8
+
9
+ ALTER TABLE rules ADD COLUMN rating INTEGER DEFAULT NULL;
10
+ ALTER TABLE rules ADD COLUMN rating_comment TEXT DEFAULT NULL;
11
+ ALTER TABLE rules ADD COLUMN rated_at TEXT DEFAULT NULL;
12
+ `);
13
+ }
@@ -0,0 +1,38 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS git_commits (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ project_id INTEGER NOT NULL,
8
+ commit_hash TEXT NOT NULL,
9
+ message TEXT NOT NULL,
10
+ author TEXT,
11
+ timestamp TEXT NOT NULL,
12
+ files_changed INTEGER NOT NULL DEFAULT 0,
13
+ insertions INTEGER NOT NULL DEFAULT 0,
14
+ deletions INTEGER NOT NULL DEFAULT 0,
15
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
17
+ UNIQUE(project_id, commit_hash)
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS error_commits (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ error_id INTEGER NOT NULL,
23
+ commit_hash TEXT NOT NULL,
24
+ relationship TEXT NOT NULL DEFAULT 'introduced_by',
25
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ FOREIGN KEY (error_id) REFERENCES errors(id) ON DELETE CASCADE,
27
+ UNIQUE(error_id, commit_hash, relationship)
28
+ );
29
+
30
+ ALTER TABLE errors ADD COLUMN git_diff TEXT DEFAULT NULL;
31
+ ALTER TABLE errors ADD COLUMN git_branch TEXT DEFAULT NULL;
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_git_commits_project ON git_commits(project_id);
34
+ CREATE INDEX IF NOT EXISTS idx_git_commits_hash ON git_commits(commit_hash);
35
+ CREATE INDEX IF NOT EXISTS idx_error_commits_error ON error_commits(error_id);
36
+ CREATE INDEX IF NOT EXISTS idx_error_commits_hash ON error_commits(commit_hash);
37
+ `);
38
+ }
@@ -0,0 +1,8 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ ALTER TABLE errors ADD COLUMN embedding BLOB DEFAULT NULL;
6
+ ALTER TABLE code_modules ADD COLUMN embedding BLOB DEFAULT NULL;
7
+ `);
8
+ }
@@ -6,6 +6,9 @@ import { up as codeSchema } from './003_code_schema.js';
6
6
  import { up as synapsesSchema } from './004_synapses_schema.js';
7
7
  import { up as ftsIndexes } from './005_fts_indexes.js';
8
8
  import { up as synapsesPhase3 } from './006_synapses_phase3.js';
9
+ import { up as feedbackSchema } from './007_feedback.js';
10
+ import { up as gitIntegration } from './008_git_integration.js';
11
+ import { up as embeddings } from './009_embeddings.js';
9
12
 
10
13
  interface Migration {
11
14
  version: number;
@@ -20,6 +23,9 @@ const migrations: Migration[] = [
20
23
  { version: 4, name: '004_synapses_schema', up: synapsesSchema },
21
24
  { version: 5, name: '005_fts_indexes', up: ftsIndexes },
22
25
  { version: 6, name: '006_synapses_phase3', up: synapsesPhase3 },
26
+ { version: 7, name: '007_feedback', up: feedbackSchema },
27
+ { version: 8, name: '008_git_integration', up: gitIntegration },
28
+ { version: 9, name: '009_embeddings', up: embeddings },
23
29
  ];
24
30
 
25
31
  function ensureMigrationsTable(db: Database.Database): void {
@@ -26,6 +26,33 @@ export class CodeModuleRepository {
26
26
  WHERE code_modules_fts MATCH ?
27
27
  ORDER BY rank
28
28
  `),
29
+ upsertSimilarity: db.prepare(`
30
+ INSERT INTO module_similarities (module_a_id, module_b_id, similarity_score)
31
+ VALUES (@module_a_id, @module_b_id, @similarity_score)
32
+ ON CONFLICT(module_a_id, module_b_id)
33
+ DO UPDATE SET similarity_score = @similarity_score, computed_at = datetime('now')
34
+ `),
35
+ findSimilarModules: db.prepare(`
36
+ SELECT ms.*, cm.name, cm.file_path, cm.language, cm.reusability_score
37
+ FROM module_similarities ms
38
+ JOIN code_modules cm ON (
39
+ CASE WHEN ms.module_a_id = ? THEN ms.module_b_id ELSE ms.module_a_id END
40
+ ) = cm.id
41
+ WHERE ms.module_a_id = ? OR ms.module_b_id = ?
42
+ ORDER BY ms.similarity_score DESC
43
+ LIMIT ?
44
+ `),
45
+ findHighSimilarityPairs: db.prepare(`
46
+ SELECT ms.*,
47
+ a.name as a_name, a.file_path as a_path,
48
+ b.name as b_name, b.file_path as b_path
49
+ FROM module_similarities ms
50
+ JOIN code_modules a ON ms.module_a_id = a.id
51
+ JOIN code_modules b ON ms.module_b_id = b.id
52
+ WHERE ms.similarity_score >= ?
53
+ ORDER BY ms.similarity_score DESC
54
+ LIMIT ?
55
+ `),
29
56
  };
30
57
  }
31
58
 
@@ -86,4 +113,30 @@ export class CodeModuleRepository {
86
113
  countAll(): number {
87
114
  return (this.stmts.countAll.get() as { count: number }).count;
88
115
  }
116
+
117
+ upsertSimilarity(moduleAId: number, moduleBId: number, score: number): void {
118
+ // Always store with smaller id first for consistency
119
+ const [a, b] = moduleAId < moduleBId ? [moduleAId, moduleBId] : [moduleBId, moduleAId];
120
+ this.stmts.upsertSimilarity.run({
121
+ module_a_id: a,
122
+ module_b_id: b,
123
+ similarity_score: score,
124
+ });
125
+ }
126
+
127
+ findSimilarModules(moduleId: number, limit: number = 10): Array<{ module_id: number; similarity_score: number; name: string; file_path: string }> {
128
+ return this.stmts.findSimilarModules.all(moduleId, moduleId, moduleId, limit) as Array<{
129
+ module_id: number; similarity_score: number; name: string; file_path: string;
130
+ }>;
131
+ }
132
+
133
+ findHighSimilarityPairs(minScore: number = 0.75, limit: number = 50): Array<{
134
+ module_a_id: number; module_b_id: number; similarity_score: number;
135
+ a_name: string; a_path: string; b_name: string; b_path: string;
136
+ }> {
137
+ return this.stmts.findHighSimilarityPairs.all(minScore, limit) as Array<{
138
+ module_a_id: number; module_b_id: number; similarity_score: number;
139
+ a_name: string; a_path: string; b_name: string; b_path: string;
140
+ }>;
141
+ }
89
142
  }
@@ -14,6 +14,22 @@ export class ErrorRepository {
14
14
  INSERT INTO errors (project_id, terminal_id, fingerprint, type, message, raw_output, context, file_path, line_number, column_number)
15
15
  VALUES (@project_id, @terminal_id, @fingerprint, @type, @message, @raw_output, @context, @file_path, @line_number, @column_number)
16
16
  `),
17
+ createChain: this.db.prepare(`
18
+ INSERT OR IGNORE INTO error_chains (parent_error_id, child_error_id, relationship)
19
+ VALUES (@parent_error_id, @child_error_id, @relationship)
20
+ `),
21
+ findChainChildren: this.db.prepare(
22
+ 'SELECT e.* FROM errors e JOIN error_chains ec ON e.id = ec.child_error_id WHERE ec.parent_error_id = ?'
23
+ ),
24
+ findChainParents: this.db.prepare(
25
+ 'SELECT e.* FROM errors e JOIN error_chains ec ON e.id = ec.parent_error_id WHERE ec.child_error_id = ?'
26
+ ),
27
+ findRecentByProject: this.db.prepare(
28
+ 'SELECT * FROM errors WHERE project_id = ? AND first_seen >= ? ORDER BY first_seen DESC LIMIT ?'
29
+ ),
30
+ findAllPaginated: this.db.prepare(
31
+ 'SELECT * FROM errors ORDER BY last_seen DESC LIMIT ? OFFSET ?'
32
+ ),
17
33
  getById: this.db.prepare(`
18
34
  SELECT * FROM errors WHERE id = ?
19
35
  `),
@@ -146,4 +162,28 @@ export class ErrorRepository {
146
162
  incrementOccurrence(id: number): void {
147
163
  this.stmts.incrementOccurrence.run(id);
148
164
  }
165
+
166
+ createChain(parentErrorId: number, childErrorId: number, relationship: string = 'caused_by_fix'): void {
167
+ this.stmts.createChain.run({
168
+ parent_error_id: parentErrorId,
169
+ child_error_id: childErrorId,
170
+ relationship,
171
+ });
172
+ }
173
+
174
+ findChainChildren(errorId: number): ErrorRecord[] {
175
+ return this.stmts.findChainChildren.all(errorId) as ErrorRecord[];
176
+ }
177
+
178
+ findChainParents(errorId: number): ErrorRecord[] {
179
+ return this.stmts.findChainParents.all(errorId) as ErrorRecord[];
180
+ }
181
+
182
+ findRecentByProject(projectId: number, since: string, limit: number = 10): ErrorRecord[] {
183
+ return this.stmts.findRecentByProject.all(projectId, since, limit) as ErrorRecord[];
184
+ }
185
+
186
+ findAll(limit: number = 100, offset: number = 0): ErrorRecord[] {
187
+ return this.stmts.findAllPaginated.all(limit, offset) as ErrorRecord[];
188
+ }
149
189
  }
@@ -75,4 +75,25 @@ export class InsightRepository {
75
75
  const result = this.stmts.expire.run();
76
76
  return result.changes;
77
77
  }
78
+
79
+ rate(id: number, rating: number, comment?: string): boolean {
80
+ const stmt = this.db.prepare(
81
+ `UPDATE insights SET rating = ?, rating_comment = ?, rated_at = datetime('now') WHERE id = ?`
82
+ );
83
+ const result = stmt.run(rating, comment ?? null, id);
84
+ return result.changes > 0;
85
+ }
86
+
87
+ findRated(minRating?: number): InsightRecord[] {
88
+ if (minRating !== undefined) {
89
+ const stmt = this.db.prepare(
90
+ 'SELECT * FROM insights WHERE rating IS NOT NULL AND rating >= ? ORDER BY rating DESC'
91
+ );
92
+ return stmt.all(minRating) as InsightRecord[];
93
+ }
94
+ const stmt = this.db.prepare(
95
+ 'SELECT * FROM insights WHERE rating IS NOT NULL ORDER BY rating DESC'
96
+ );
97
+ return stmt.all() as InsightRecord[];
98
+ }
78
99
  }