claude-brain 0.27.3 → 0.28.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.
@@ -240,85 +240,94 @@ export class IntentClassifier {
240
240
  // Phase 19 B3: Detect temporal signals for secondary intent
241
241
  const hasTemporal = this.hasTemporalSignal(lower)
242
242
 
243
+ // SLM Phase 1A: Log classification for training data collection
244
+ const startTime = Date.now()
245
+
243
246
  // Check in priority order — first confident match wins
244
247
 
248
+ // Helper to log + return in one step
249
+ const ret = (result: ClassificationResult): ClassificationResult => {
250
+ this._logTraining(message, result, startTime)
251
+ return result
252
+ }
253
+
245
254
  // 1. no_action: very short messages, greetings, acknowledgments
246
255
  if (this.isNoAction(lower)) {
247
- return { primary: 'no_action', confidence: 0.95, secondary: [] }
256
+ return ret({ primary: 'no_action', confidence: 0.95, secondary: [] })
248
257
  }
249
258
 
250
259
  // 2. delete_memory: "forget that", "delete", "remove"
251
260
  if (this.isDeleteMemory(lower)) {
252
- return { primary: 'delete_memory', confidence: 0.90, secondary }
261
+ return ret({ primary: 'delete_memory', confidence: 0.90, secondary })
253
262
  }
254
263
 
255
264
  // 3. update_memory: "actually", "correction:", "change that to"
256
265
  if (this.isUpdateMemory(lower)) {
257
- return { primary: 'update_memory', confidence: 0.85, secondary }
266
+ return ret({ primary: 'update_memory', confidence: 0.85, secondary })
258
267
  }
259
268
 
260
269
  // 4. store_this: explicit "remember:", "save this:", "I prefer" (never for questions)
261
270
  if (this.isStoreThis(lower, message)) {
262
271
  if (this.hasDecisionSignal(lower)) secondary.push('decision_made')
263
- return { primary: 'store_this', confidence: 0.90, secondary }
272
+ return ret({ primary: 'store_this', confidence: 0.90, secondary })
264
273
  }
265
274
 
266
275
  // 5. decision_made: decision phrases + reasoning (never for questions)
267
276
  if (this.isDecisionMade(lower, message)) {
268
277
  if (this.hasComparisonSignal(lower)) secondary.push('comparison')
269
- return { primary: 'decision_made', confidence: 0.85, secondary }
278
+ return ret({ primary: 'decision_made', confidence: 0.85, secondary })
270
279
  }
271
280
 
272
281
  // 6. mistake_learned: correction/bug/lesson indicators
273
282
  if (this.isMistakeLearned(lower)) {
274
- return { primary: 'mistake_learned', confidence: 0.85, secondary }
283
+ return ret({ primary: 'mistake_learned', confidence: 0.85, secondary })
275
284
  }
276
285
 
277
286
  // 7. list_all: "list all", "what decisions", "show all"
278
287
  if (this.isListAll(lower, message)) {
279
- return { primary: 'list_all', confidence: 0.85, secondary }
288
+ return ret({ primary: 'list_all', confidence: 0.85, secondary })
280
289
  }
281
290
 
282
291
  // 8. progress_update: completed task indicators (NOT questions)
283
292
  if (this.isProgressUpdate(lower, message)) {
284
293
  if (this.hasSessionSignal(lower)) secondary.push('session_start')
285
- return { primary: 'progress_update', confidence: 0.85, secondary }
294
+ return ret({ primary: 'progress_update', confidence: 0.85, secondary })
286
295
  }
287
296
 
288
297
  // 9. comparison: vs, which is better, etc.
289
298
  if (this.isComparison(lower)) {
290
299
  if (this.isQuestion(lower, message)) secondary.push('question')
291
300
  if (hasTemporal) secondary.push('exploration')
292
- return { primary: 'comparison', confidence: 0.85, secondary }
301
+ return ret({ primary: 'comparison', confidence: 0.85, secondary })
293
302
  }
294
303
 
295
304
  // 10. pattern_found: explicit pattern documentation
296
305
  if (this.isPatternFound(lower)) {
297
- return { primary: 'pattern_found', confidence: 0.80, secondary }
306
+ return ret({ primary: 'pattern_found', confidence: 0.80, secondary })
298
307
  }
299
308
 
300
309
  // 11. session_start: starting/resuming work (Phase 19: narrowed check)
301
310
  if (this.isSessionStart(lower)) {
302
311
  secondary.push('context_needed')
303
- return { primary: 'session_start', confidence: 0.90, secondary }
312
+ return ret({ primary: 'session_start', confidence: 0.90, secondary })
304
313
  }
305
314
 
306
315
  // 12. detail_request: "details obs_abc123", "show me <id>" (before exploration to avoid misclassification)
307
316
  if (this.isDetailRequest(lower)) {
308
- return { primary: 'detail_request', confidence: 0.90, secondary }
317
+ return ret({ primary: 'detail_request', confidence: 0.90, secondary })
309
318
  }
310
319
 
311
320
  // 12b. timeline: "timeline for project", "what did I do yesterday", "recent activity" (before exploration)
312
321
  if (this.isTimeline(lower)) {
313
322
  if (hasTemporal) secondary.push('exploration')
314
- return { primary: 'timeline', confidence: 0.85, secondary }
323
+ return ret({ primary: 'timeline', confidence: 0.85, secondary })
315
324
  }
316
325
 
317
326
  // 12c. exploration: trends, graph, evolution, history (general exploration that isn't a specific timeline)
318
327
  if (this.isExploration(lower)) {
319
328
  if (this.isQuestion(lower, message)) secondary.push('question')
320
329
  if (hasTemporal) secondary.push('exploration')
321
- return { primary: 'exploration', confidence: 0.75, secondary }
330
+ return ret({ primary: 'exploration', confidence: 0.75, secondary })
322
331
  }
323
332
 
324
333
  // 13. question: starts with question word or ends with ?
@@ -330,12 +339,32 @@ export class IntentClassifier {
330
339
 
331
340
  // Phase 19 B1: ? → 0.95, question word → 0.90
332
341
  const confidence = message.trim().endsWith('?') ? 0.95 : 0.90
333
- return { primary: 'question', confidence, secondary }
342
+ return ret({ primary: 'question', confidence, secondary })
334
343
  }
335
344
 
336
345
  // 14. Default: context_needed
337
346
  if (hasTemporal) secondary.push('exploration')
338
- return { primary: 'context_needed', confidence: 0.60, secondary }
347
+ const defaultResult = { primary: 'context_needed' as Intent, confidence: 0.60, secondary }
348
+ this._logTraining(message, defaultResult, startTime)
349
+ return defaultResult
350
+ }
351
+
352
+ /**
353
+ * SLM Phase 1A: Log classification result for training data collection.
354
+ * Fire-and-forget, never blocks the main path.
355
+ */
356
+ private _logTraining(message: string, result: ClassificationResult, startTime: number): void {
357
+ try {
358
+ const { logTrainingData } = require('@/training/data-store')
359
+ logTrainingData({
360
+ task: 'intent' as const,
361
+ input: message,
362
+ output: JSON.stringify({ label: result.primary, secondary: result.secondary }),
363
+ metadata: JSON.stringify({ confidence: result.confidence, elapsed_ms: Date.now() - startTime }),
364
+ })
365
+ } catch {
366
+ // Training data logging is non-critical
367
+ }
339
368
  }
340
369
 
341
370
  private isNoAction(lower: string): boolean {
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { Logger } from 'pino'
14
14
  import { IntentClassifier, type ClassificationResult } from './intent-classifier'
15
+ import type { InferenceRouter } from '@/intelligence/inference-router'
15
16
  import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
16
17
  import { ResponseFilter, type BrainResponse, type TierResults, type FilterableResult, formatCompactResponse, formatDetailResponse, formatTimeline, groupByDay } from './response-filter'
17
18
  import { SearchEngine } from './search-engine'
@@ -62,6 +63,9 @@ export class BrainRouter {
62
63
  private searchEngine: SearchEngine
63
64
  private logger: Logger
64
65
 
66
+ /** SLM Upgrade: Optional inference router for model-based classification */
67
+ private inferenceRouter: InferenceRouter | null = null
68
+
65
69
  /** Phase 30: Optional LLM compressor for long observations */
66
70
  private compressor: ObservationCompressor | null = null
67
71
 
@@ -80,6 +84,12 @@ export class BrainRouter {
80
84
  this.logger = logger.child({ component: 'brain-router' })
81
85
  }
82
86
 
87
+ /** SLM Upgrade: Set the optional inference router for model-based classification */
88
+ setInferenceRouter(router: InferenceRouter): void {
89
+ this.inferenceRouter = router
90
+ this.entityExtractor.setInferenceRouter(router)
91
+ }
92
+
83
93
  /** Phase 30: Set the optional LLM compressor */
84
94
  setCompressor(compressor: ObservationCompressor): void {
85
95
  this.compressor = compressor
@@ -120,8 +130,10 @@ export class BrainRouter {
120
130
  }
121
131
  }
122
132
 
123
- // Classify intent
124
- const classification = this.classifier.classify(message)
133
+ // Classify intent (SLM: use inference router if available, falls back to regex)
134
+ const classification = this.inferenceRouter
135
+ ? await this.inferenceRouter.classifyIntent(message)
136
+ : this.classifier.classify(message)
125
137
  this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
126
138
 
127
139
  // Route to handler
@@ -2163,6 +2175,14 @@ let routerInstance: BrainRouter | null = null
2163
2175
  export function getBrainRouter(logger: Logger): BrainRouter {
2164
2176
  if (!routerInstance) {
2165
2177
  routerInstance = new BrainRouter(logger)
2178
+ // SLM Upgrade: Wire inference router if available
2179
+ try {
2180
+ const { getInferenceRouter } = require('@/server/services')
2181
+ const ir = getInferenceRouter()
2182
+ if (ir) routerInstance.setInferenceRouter(ir)
2183
+ } catch {
2184
+ // Services not initialized yet — will use regex fallback
2185
+ }
2166
2186
  }
2167
2187
  return routerInstance
2168
2188
  }
@@ -6,7 +6,7 @@
6
6
  import { Hono } from 'hono'
7
7
  import type { Logger } from 'pino'
8
8
  import type { Config } from '@/config'
9
- import { getMemoryService, getVaultService, isServicesInitialized } from '@/server/services'
9
+ import { getMemoryService, getVaultService, getInferenceRouter, isServicesInitialized } from '@/server/services'
10
10
  import { ResourceProvider } from '@/server/providers/resources'
11
11
  import type { MemoryManager } from '@/memory'
12
12
  import type { CapturedKnowledge, HookStats } from '@/hooks/types'
@@ -16,6 +16,7 @@ import type { CodeIndexer } from '@/code-intelligence/indexer'
16
16
  import type { CodeQuery } from '@/code-intelligence/query'
17
17
  import type { MemoryCodeLinker } from '@/code-intelligence/linker'
18
18
  import { setupWebViewer, setWebViewerCodeQuery } from '@/server/web-viewer'
19
+ import { getTrainingStats, getModelFeedbackStats, getDisagreements } from '@/training/data-store'
19
20
 
20
21
  export class HttpApiServer {
21
22
  private app: Hono
@@ -135,6 +136,12 @@ export class HttpApiServer {
135
136
 
136
137
  // Phase 23b: Expose brain://context/auto via HTTP for testability
137
138
  this.app.get('/api/context/auto', () => this.handleContextAuto())
139
+
140
+ // Phase 6A: SLM feedback & model status endpoints
141
+ this.app.get('/api/models/status', () => this.handleModelsStatus())
142
+ this.app.get('/api/models/feedback', (c) => this.handleModelsFeedback(c))
143
+ this.app.get('/api/models/disagreements', (c) => this.handleModelsDisagreements(c))
144
+ this.app.get('/api/training/stats', () => this.handleTrainingStats())
138
145
  }
139
146
 
140
147
  private async handleListProjects(): Promise<Response> {
@@ -1047,6 +1054,81 @@ export class HttpApiServer {
1047
1054
  }
1048
1055
  }
1049
1056
 
1057
+ // ─── Phase 6A: SLM Model Feedback Endpoints ────────────
1058
+
1059
+ private handleModelsStatus(): Response {
1060
+ try {
1061
+ const inferenceRouter = getInferenceRouter()
1062
+ if (!inferenceRouter) {
1063
+ return Response.json({
1064
+ success: true,
1065
+ data: { enabled: false, message: 'SLM inference not initialized' },
1066
+ })
1067
+ }
1068
+ return Response.json({ success: true, data: inferenceRouter.getStatus() })
1069
+ } catch (error) {
1070
+ this.logger.error({ error }, 'Failed to get model status')
1071
+ return Response.json(
1072
+ { success: false, error: 'Failed to get model status' },
1073
+ { status: 500 }
1074
+ )
1075
+ }
1076
+ }
1077
+
1078
+ private handleModelsFeedback(c: any): Response {
1079
+ try {
1080
+ const task = c.req.query('task')
1081
+ const stats = getModelFeedbackStats()
1082
+
1083
+ if (task) {
1084
+ const taskStats = stats[task]
1085
+ if (!taskStats) {
1086
+ return Response.json(
1087
+ { success: false, error: `Unknown task: ${task}` },
1088
+ { status: 400 }
1089
+ )
1090
+ }
1091
+ return Response.json({ success: true, data: { [task]: taskStats } })
1092
+ }
1093
+
1094
+ return Response.json({ success: true, data: stats })
1095
+ } catch (error) {
1096
+ this.logger.error({ error }, 'Failed to get model feedback stats')
1097
+ return Response.json(
1098
+ { success: false, error: 'Failed to get model feedback stats' },
1099
+ { status: 500 }
1100
+ )
1101
+ }
1102
+ }
1103
+
1104
+ private handleModelsDisagreements(c: any): Response {
1105
+ try {
1106
+ const task = c.req.query('task') || 'intent'
1107
+ const limit = parseInt(c.req.query('limit') || '50', 10)
1108
+ const disagreements = getDisagreements(task, limit)
1109
+ return Response.json({ success: true, data: disagreements })
1110
+ } catch (error) {
1111
+ this.logger.error({ error }, 'Failed to get model disagreements')
1112
+ return Response.json(
1113
+ { success: false, error: 'Failed to get model disagreements' },
1114
+ { status: 500 }
1115
+ )
1116
+ }
1117
+ }
1118
+
1119
+ private handleTrainingStats(): Response {
1120
+ try {
1121
+ const stats = getTrainingStats()
1122
+ return Response.json({ success: true, data: stats })
1123
+ } catch (error) {
1124
+ this.logger.error({ error }, 'Failed to get training stats')
1125
+ return Response.json(
1126
+ { success: false, error: 'Failed to get training stats' },
1127
+ { status: 500 }
1128
+ )
1129
+ }
1130
+ }
1131
+
1050
1132
  async start(): Promise<void> {
1051
1133
  const port = this.config.port || 3000
1052
1134
 
@@ -26,6 +26,8 @@ import { SemanticCache } from '@/intelligence/optimization/semantic-cache'
26
26
  import { PrecomputeEngine } from '@/intelligence/optimization/precompute'
27
27
  import { MemoryArchiver } from '@/memory/consolidation/archiver'
28
28
  import { ImportanceScorer } from '@/memory/consolidation/scorer'
29
+ import { ModelManager } from '@/intelligence/model-manager'
30
+ import { InferenceRouter } from '@/intelligence/inference-router'
29
31
 
30
32
  export interface KnowledgeGraphServiceContainer {
31
33
  graph: InMemoryKnowledgeGraph
@@ -53,6 +55,8 @@ export interface Services {
53
55
  codeIndexer: CodeIndexer | null
54
56
  codeQuery: CodeQuery | null
55
57
  codeLinker: MemoryCodeLinker | null
58
+ modelManager: ModelManager | null
59
+ inferenceRouter: InferenceRouter | null
56
60
  logger: Logger
57
61
  config: Config
58
62
  }
@@ -371,6 +375,25 @@ export async function initializeServices(config: Config, logger: Logger): Promis
371
375
  serviceLogger.warn({ error }, 'Failed to initialize code linker, continuing without it')
372
376
  }
373
377
 
378
+ // Initialize SLM Model Manager & Inference Router
379
+ let modelManager: ModelManager | null = null
380
+ let inferenceRouter: InferenceRouter | null = null
381
+ try {
382
+ const slmModelsDir = config.slm?.modelsDir?.replace(/^~/, require('os').homedir())
383
+ modelManager = new ModelManager(serviceLogger, slmModelsDir)
384
+ inferenceRouter = new InferenceRouter(serviceLogger, config, modelManager)
385
+ const slmEnabled = config.slm?.enabled ?? false
386
+ serviceLogger.info({ enabled: slmEnabled }, 'SLM inference router initialized')
387
+ } catch (error) {
388
+ serviceLogger.warn({ error }, 'Failed to initialize SLM inference, continuing with regex only')
389
+ }
390
+
391
+ // Wire SLM inference into PatternRecognizer (Phase 4C)
392
+ if (inferenceRouter && phase12) {
393
+ phase12.patterns.setInferenceRouter(inferenceRouter)
394
+ serviceLogger.info('SLM inference wired into PatternRecognizer')
395
+ }
396
+
374
397
  // Store services
375
398
  services = {
376
399
  memory,
@@ -388,6 +411,8 @@ export async function initializeServices(config: Config, logger: Logger): Promis
388
411
  codeIndexer,
389
412
  codeQuery,
390
413
  codeLinker,
414
+ modelManager,
415
+ inferenceRouter,
391
416
  logger,
392
417
  config
393
418
  }
@@ -526,6 +551,22 @@ export function getCodeLinker(): MemoryCodeLinker | null {
526
551
  return services?.codeLinker ?? null
527
552
  }
528
553
 
554
+ /**
555
+ * Get Model Manager (SLM Upgrade)
556
+ * Returns null if SLM is not initialized
557
+ */
558
+ export function getModelManager(): ModelManager | null {
559
+ return services?.modelManager ?? null
560
+ }
561
+
562
+ /**
563
+ * Get Inference Router (SLM Upgrade)
564
+ * Returns null if SLM is not initialized
565
+ */
566
+ export function getInferenceRouter(): InferenceRouter | null {
567
+ return services?.inferenceRouter ?? null
568
+ }
569
+
529
570
  /**
530
571
  * Check if services are initialized
531
572
  */
@@ -617,6 +658,12 @@ export async function shutdownServices(): Promise<void> {
617
658
  }
618
659
  }
619
660
 
661
+ // Unload SLM models
662
+ if (services.modelManager) {
663
+ services.modelManager.unloadAll()
664
+ serviceLogger.info('SLM models unloaded')
665
+ }
666
+
620
667
  // Cleanup Phase 12
621
668
  services.phase12.cleanup()
622
669
 
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Training Data Store — Phase 1A (SLM Upgrade)
3
+ * Logs classification decisions to SQLite for model training.
4
+ * Async, non-blocking — never impacts main request path.
5
+ *
6
+ * Table: training_data in ~/.claude-brain/data/memory.db
7
+ */
8
+
9
+ import { Database } from 'bun:sqlite'
10
+ import { join } from 'node:path'
11
+ import { existsSync, mkdirSync } from 'node:fs'
12
+ import { getClaudeBrainHome } from '@/config/home'
13
+
14
+ export type TrainingTask = 'intent' | 'entity' | 'query' | 'knowledge' | 'compress' | 'pattern'
15
+
16
+ export interface TrainingEntry {
17
+ task: TrainingTask
18
+ input: string
19
+ output: string // JSON-encoded: label, entities array, summary, etc.
20
+ metadata?: string // JSON-encoded: confidence, scores, timing
21
+ }
22
+
23
+ export interface ModelFeedbackEntry {
24
+ task: string
25
+ input: string
26
+ modelPrediction: string
27
+ modelConfidence: number
28
+ regexPrediction: string
29
+ actualLabel?: string
30
+ }
31
+
32
+ let db: Database | null = null
33
+ let insertStmt: ReturnType<Database['prepare']> | null = null
34
+ let feedbackInsertStmt: ReturnType<Database['prepare']> | null = null
35
+
36
+ function getDb(): Database | null {
37
+ if (db) return db
38
+ try {
39
+ const dataDir = join(getClaudeBrainHome(), 'data')
40
+ if (!existsSync(dataDir)) {
41
+ mkdirSync(dataDir, { recursive: true })
42
+ }
43
+ const dbPath = join(dataDir, 'memory.db')
44
+ db = new Database(dbPath)
45
+ db.run('PRAGMA journal_mode = WAL')
46
+ ensureTable(db)
47
+ insertStmt = db.prepare(
48
+ 'INSERT INTO training_data (task, input, output, metadata) VALUES (?, ?, ?, ?)'
49
+ )
50
+ feedbackInsertStmt = db.prepare(
51
+ 'INSERT INTO model_feedback (task, input, model_prediction, model_confidence, regex_prediction, actual_label) VALUES (?, ?, ?, ?, ?, ?)'
52
+ )
53
+ return db
54
+ } catch {
55
+ return null
56
+ }
57
+ }
58
+
59
+ function ensureTable(database: Database): void {
60
+ database.run(`
61
+ CREATE TABLE IF NOT EXISTS training_data (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ task TEXT NOT NULL,
64
+ input TEXT NOT NULL,
65
+ output TEXT NOT NULL,
66
+ metadata TEXT,
67
+ verified INTEGER DEFAULT 0,
68
+ created_at TEXT DEFAULT (datetime('now'))
69
+ )
70
+ `)
71
+ // Indexes for efficient querying
72
+ database.run('CREATE INDEX IF NOT EXISTS idx_training_task ON training_data(task)')
73
+ database.run('CREATE INDEX IF NOT EXISTS idx_training_verified ON training_data(verified)')
74
+
75
+ // Phase 6A: Model feedback table for continuous learning loop
76
+ database.run(`
77
+ CREATE TABLE IF NOT EXISTS model_feedback (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ task TEXT NOT NULL,
80
+ input TEXT NOT NULL,
81
+ model_prediction TEXT NOT NULL,
82
+ model_confidence REAL NOT NULL,
83
+ regex_prediction TEXT NOT NULL,
84
+ actual_label TEXT,
85
+ created_at TEXT DEFAULT (datetime('now'))
86
+ )
87
+ `)
88
+ database.run('CREATE INDEX IF NOT EXISTS idx_feedback_task ON model_feedback(task)')
89
+ }
90
+
91
+ /**
92
+ * Log a training example. Fire-and-forget — errors are silently swallowed.
93
+ */
94
+ export function logTrainingData(entry: TrainingEntry): void {
95
+ setImmediate(() => {
96
+ try {
97
+ const database = getDb()
98
+ if (!database || !insertStmt) return
99
+ insertStmt.run(entry.task, entry.input, entry.output, entry.metadata || null)
100
+ } catch {
101
+ // Never block or crash the main path
102
+ }
103
+ })
104
+ }
105
+
106
+ /**
107
+ * Export training data as JSONL lines for a specific task.
108
+ */
109
+ export function exportTrainingData(
110
+ task: TrainingTask,
111
+ options?: { verifiedOnly?: boolean; limit?: number }
112
+ ): string[] {
113
+ const database = getDb()
114
+ if (!database) return []
115
+
116
+ let sql = 'SELECT input, output, metadata, verified, created_at FROM training_data WHERE task = ?'
117
+ const params: any[] = [task]
118
+
119
+ if (options?.verifiedOnly) {
120
+ sql += ' AND verified = 1'
121
+ }
122
+
123
+ sql += ' ORDER BY created_at DESC'
124
+
125
+ if (options?.limit) {
126
+ sql += ' LIMIT ?'
127
+ params.push(options.limit)
128
+ }
129
+
130
+ const rows = database.prepare(sql).all(...params) as any[]
131
+ return rows.map(row => JSON.stringify({
132
+ input: row.input,
133
+ output: JSON.parse(row.output),
134
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
135
+ verified: row.verified === 1,
136
+ created_at: row.created_at,
137
+ }))
138
+ }
139
+
140
+ /**
141
+ * Get count of training examples per task.
142
+ */
143
+ export function getTrainingStats(): Record<TrainingTask, { total: number; verified: number }> {
144
+ const database = getDb()
145
+ const tasks: TrainingTask[] = ['intent', 'entity', 'query', 'knowledge', 'compress', 'pattern']
146
+ const stats = {} as Record<TrainingTask, { total: number; verified: number }>
147
+
148
+ for (const task of tasks) {
149
+ if (!database) {
150
+ stats[task] = { total: 0, verified: 0 }
151
+ continue
152
+ }
153
+ const total = (database.prepare('SELECT COUNT(*) as c FROM training_data WHERE task = ?').get(task) as any)?.c || 0
154
+ const verified = (database.prepare('SELECT COUNT(*) as c FROM training_data WHERE task = ? AND verified = 1').get(task) as any)?.c || 0
155
+ stats[task] = { total, verified }
156
+ }
157
+
158
+ return stats
159
+ }
160
+
161
+ // ── Phase 6A: Model Feedback Functions ──────────────────────────────
162
+
163
+ /**
164
+ * Log a model vs regex comparison. Fire-and-forget — errors are silently swallowed.
165
+ */
166
+ export function logModelFeedback(entry: ModelFeedbackEntry): void {
167
+ setImmediate(() => {
168
+ try {
169
+ const database = getDb()
170
+ if (!database || !feedbackInsertStmt) return
171
+ feedbackInsertStmt.run(
172
+ entry.task,
173
+ entry.input,
174
+ entry.modelPrediction,
175
+ entry.modelConfidence,
176
+ entry.regexPrediction,
177
+ entry.actualLabel || null
178
+ )
179
+ } catch {
180
+ // Never block or crash the main path
181
+ }
182
+ })
183
+ }
184
+
185
+ /**
186
+ * Get per-task feedback stats: total, agreements, disagreements, disagreement rate, reviewed count.
187
+ */
188
+ export function getModelFeedbackStats(): Record<string, {
189
+ total: number
190
+ agreements: number
191
+ disagreements: number
192
+ disagreementRate: number
193
+ reviewed: number
194
+ }> {
195
+ const database = getDb()
196
+ const tasks: TrainingTask[] = ['intent', 'entity', 'query', 'knowledge', 'compress', 'pattern']
197
+ const stats = {} as Record<string, {
198
+ total: number
199
+ agreements: number
200
+ disagreements: number
201
+ disagreementRate: number
202
+ reviewed: number
203
+ }>
204
+
205
+ for (const task of tasks) {
206
+ if (!database) {
207
+ stats[task] = { total: 0, agreements: 0, disagreements: 0, disagreementRate: 0, reviewed: 0 }
208
+ continue
209
+ }
210
+ const total = (database.prepare(
211
+ 'SELECT COUNT(*) as c FROM model_feedback WHERE task = ?'
212
+ ).get(task) as any)?.c || 0
213
+
214
+ const agreements = (database.prepare(
215
+ 'SELECT COUNT(*) as c FROM model_feedback WHERE task = ? AND model_prediction = regex_prediction'
216
+ ).get(task) as any)?.c || 0
217
+
218
+ const disagreements = total - agreements
219
+
220
+ const reviewed = (database.prepare(
221
+ 'SELECT COUNT(*) as c FROM model_feedback WHERE task = ? AND actual_label IS NOT NULL'
222
+ ).get(task) as any)?.c || 0
223
+
224
+ stats[task] = {
225
+ total,
226
+ agreements,
227
+ disagreements,
228
+ disagreementRate: total > 0 ? disagreements / total : 0,
229
+ reviewed,
230
+ }
231
+ }
232
+
233
+ return stats
234
+ }
235
+
236
+ /**
237
+ * Export feedback as JSONL lines for a specific task.
238
+ */
239
+ export function exportModelFeedback(
240
+ task: string,
241
+ options?: { limit?: number }
242
+ ): string[] {
243
+ const database = getDb()
244
+ if (!database) return []
245
+
246
+ let sql = 'SELECT input, model_prediction, model_confidence, regex_prediction, actual_label, created_at FROM model_feedback WHERE task = ?'
247
+ const params: any[] = [task]
248
+
249
+ sql += ' ORDER BY created_at DESC'
250
+
251
+ if (options?.limit) {
252
+ sql += ' LIMIT ?'
253
+ params.push(options.limit)
254
+ }
255
+
256
+ const rows = database.prepare(sql).all(...params) as any[]
257
+ return rows.map(row => JSON.stringify({
258
+ input: row.input,
259
+ modelPrediction: row.model_prediction,
260
+ modelConfidence: row.model_confidence,
261
+ regexPrediction: row.regex_prediction,
262
+ actualLabel: row.actual_label,
263
+ createdAt: row.created_at,
264
+ }))
265
+ }
266
+
267
+ /**
268
+ * Get the most recent disagreements for human review.
269
+ */
270
+ export function getDisagreements(
271
+ task: string,
272
+ limit: number = 50
273
+ ): Array<{
274
+ input: string
275
+ modelPrediction: string
276
+ modelConfidence: number
277
+ regexPrediction: string
278
+ createdAt: string
279
+ }> {
280
+ const database = getDb()
281
+ if (!database) return []
282
+
283
+ const rows = database.prepare(`
284
+ SELECT input, model_prediction, model_confidence, regex_prediction, created_at
285
+ FROM model_feedback
286
+ WHERE task = ? AND model_prediction != regex_prediction
287
+ ORDER BY created_at DESC
288
+ LIMIT ?
289
+ `).all(task, limit) as any[]
290
+
291
+ return rows.map(row => ({
292
+ input: row.input,
293
+ modelPrediction: row.model_prediction,
294
+ modelConfidence: row.model_confidence,
295
+ regexPrediction: row.regex_prediction,
296
+ createdAt: row.created_at,
297
+ }))
298
+ }