@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.
- package/README.md +225 -50
- package/dist/api/server.d.ts +19 -0
- package/dist/api/server.js +281 -0
- package/dist/api/server.js.map +1 -0
- package/dist/brain.d.ts +3 -0
- package/dist/brain.js +45 -8
- package/dist/brain.js.map +1 -1
- package/dist/cli/commands/dashboard.js +2 -0
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/explain.d.ts +2 -0
- package/dist/cli/commands/explain.js +76 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/code/analyzer.d.ts +6 -0
- package/dist/code/analyzer.js +35 -0
- package/dist/code/analyzer.js.map +1 -1
- package/dist/code/matcher.d.ts +11 -1
- package/dist/code/matcher.js +49 -0
- package/dist/code/matcher.js.map +1 -1
- package/dist/code/scorer.d.ts +1 -0
- package/dist/code/scorer.js +15 -1
- package/dist/code/scorer.js.map +1 -1
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.js +124 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db/migrations/007_feedback.d.ts +2 -0
- package/dist/db/migrations/007_feedback.js +12 -0
- package/dist/db/migrations/007_feedback.js.map +1 -0
- package/dist/db/migrations/008_git_integration.d.ts +2 -0
- package/dist/db/migrations/008_git_integration.js +37 -0
- package/dist/db/migrations/008_git_integration.js.map +1 -0
- package/dist/db/migrations/009_embeddings.d.ts +2 -0
- package/dist/db/migrations/009_embeddings.js +7 -0
- package/dist/db/migrations/009_embeddings.js.map +1 -0
- package/dist/db/migrations/index.js +6 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/repositories/code-module.repository.d.ts +16 -0
- package/dist/db/repositories/code-module.repository.js +42 -0
- package/dist/db/repositories/code-module.repository.js.map +1 -1
- package/dist/db/repositories/error.repository.d.ts +5 -0
- package/dist/db/repositories/error.repository.js +27 -0
- package/dist/db/repositories/error.repository.js.map +1 -1
- package/dist/db/repositories/insight.repository.d.ts +2 -0
- package/dist/db/repositories/insight.repository.js +13 -0
- package/dist/db/repositories/insight.repository.js.map +1 -1
- package/dist/embeddings/engine.d.ts +42 -0
- package/dist/embeddings/engine.js +166 -0
- package/dist/embeddings/engine.js.map +1 -0
- package/dist/hooks/post-tool-use.js +2 -0
- package/dist/hooks/post-tool-use.js.map +1 -1
- package/dist/hooks/post-write.js +11 -0
- package/dist/hooks/post-write.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc/router.d.ts +2 -0
- package/dist/ipc/router.js +13 -0
- package/dist/ipc/router.js.map +1 -1
- package/dist/learning/confidence-scorer.d.ts +16 -0
- package/dist/learning/confidence-scorer.js +20 -0
- package/dist/learning/confidence-scorer.js.map +1 -1
- package/dist/learning/learning-engine.js +12 -5
- package/dist/learning/learning-engine.js.map +1 -1
- package/dist/matching/error-matcher.d.ts +9 -1
- package/dist/matching/error-matcher.js +50 -5
- package/dist/matching/error-matcher.js.map +1 -1
- package/dist/mcp/http-server.d.ts +14 -0
- package/dist/mcp/http-server.js +117 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/tools.d.ts +4 -0
- package/dist/mcp/tools.js +41 -14
- package/dist/mcp/tools.js.map +1 -1
- package/dist/services/analytics.service.d.ts +39 -0
- package/dist/services/analytics.service.js +111 -0
- package/dist/services/analytics.service.js.map +1 -1
- package/dist/services/code.service.d.ts +2 -0
- package/dist/services/code.service.js +62 -4
- package/dist/services/code.service.js.map +1 -1
- package/dist/services/error.service.d.ts +17 -1
- package/dist/services/error.service.js +90 -12
- package/dist/services/error.service.js.map +1 -1
- package/dist/services/git.service.d.ts +49 -0
- package/dist/services/git.service.js +112 -0
- package/dist/services/git.service.js.map +1 -0
- package/dist/services/prevention.service.d.ts +7 -0
- package/dist/services/prevention.service.js +38 -0
- package/dist/services/prevention.service.js.map +1 -1
- package/dist/services/research.service.d.ts +1 -0
- package/dist/services/research.service.js +4 -0
- package/dist/services/research.service.js.map +1 -1
- package/dist/services/solution.service.d.ts +10 -0
- package/dist/services/solution.service.js +48 -0
- package/dist/services/solution.service.js.map +1 -1
- package/dist/types/config.types.d.ts +21 -0
- package/dist/types/synapse.types.d.ts +1 -1
- package/package.json +8 -3
- package/src/api/server.ts +321 -0
- package/src/brain.ts +50 -8
- package/src/cli/commands/dashboard.ts +2 -0
- package/src/cli/commands/explain.ts +83 -0
- package/src/code/analyzer.ts +40 -0
- package/src/code/matcher.ts +67 -2
- package/src/code/scorer.ts +13 -1
- package/src/config.ts +24 -0
- package/src/dashboard/server.ts +142 -0
- package/src/db/migrations/007_feedback.ts +13 -0
- package/src/db/migrations/008_git_integration.ts +38 -0
- package/src/db/migrations/009_embeddings.ts +8 -0
- package/src/db/migrations/index.ts +6 -0
- package/src/db/repositories/code-module.repository.ts +53 -0
- package/src/db/repositories/error.repository.ts +40 -0
- package/src/db/repositories/insight.repository.ts +21 -0
- package/src/embeddings/engine.ts +217 -0
- package/src/hooks/post-tool-use.ts +2 -0
- package/src/hooks/post-write.ts +12 -0
- package/src/index.ts +3 -1
- package/src/ipc/router.ts +16 -0
- package/src/learning/confidence-scorer.ts +33 -0
- package/src/learning/learning-engine.ts +13 -5
- package/src/matching/error-matcher.ts +55 -4
- package/src/mcp/http-server.ts +137 -0
- package/src/mcp/tools.ts +52 -14
- package/src/services/analytics.service.ts +136 -0
- package/src/services/code.service.ts +87 -4
- package/src/services/error.service.ts +114 -13
- package/src/services/git.service.ts +132 -0
- package/src/services/prevention.service.ts +40 -0
- package/src/services/research.service.ts +5 -0
- package/src/services/solution.service.ts +58 -0
- package/src/types/config.types.ts +24 -0
- package/src/types/synapse.types.ts +1 -0
package/src/code/matcher.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/code/scorer.ts
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
|
@@ -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
|
}
|