@devflow-tools/database 0.8.10 → 0.9.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/dist/database.js CHANGED
@@ -12,9 +12,9 @@ class DevFlowDatabase {
12
12
  }
13
13
  const dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
14
14
  this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
15
- // 启用 WAL 模式
16
15
  this.db.exec('PRAGMA journal_mode = WAL');
17
16
  this.db.exec('PRAGMA busy_timeout = 5000');
17
+ this.db.exec('PRAGMA foreign_keys = OFF');
18
18
  this.initializeSchema();
19
19
  }
20
20
  initializeSchema() {
@@ -115,8 +115,7 @@ class DevFlowDatabase {
115
115
  subagent_id TEXT,
116
116
  error TEXT,
117
117
  blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
118
- block_reason TEXT,
119
- FOREIGN KEY (execution_id) REFERENCES skill_executions(execution_id) ON DELETE CASCADE
118
+ block_reason TEXT
120
119
  );
121
120
 
122
121
  CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
@@ -126,7 +125,93 @@ class DevFlowDatabase {
126
125
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON tool_call_events(timestamp);
127
126
  CREATE INDEX IF NOT EXISTS idx_events_tool_type ON tool_call_events(tool_type);
128
127
  CREATE INDEX IF NOT EXISTS idx_events_mcp ON tool_call_events(is_mcp_tool);
128
+ CREATE INDEX IF NOT EXISTS idx_events_session ON tool_call_events(session_id);
129
+
130
+ -- Sessions table: top-level container per Claude Code session
131
+ CREATE TABLE IF NOT EXISTS sessions (
132
+ id TEXT PRIMARY KEY,
133
+ project_root TEXT NOT NULL,
134
+ label TEXT,
135
+ started_at INTEGER NOT NULL,
136
+ finished_at INTEGER,
137
+ duration_ms INTEGER DEFAULT 0,
138
+ total_tool_calls INTEGER DEFAULT 0,
139
+ mcp_tool_calls INTEGER DEFAULT 0,
140
+ direct_tool_calls INTEGER DEFAULT 0,
141
+ subagent_count INTEGER DEFAULT 0,
142
+ status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
143
+ );
144
+
145
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
146
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
129
147
  `);
148
+ // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
149
+ try {
150
+ this.db.exec('ALTER TABLE skill_executions ADD COLUMN session_id TEXT REFERENCES sessions(id)');
151
+ }
152
+ catch { }
153
+ // Migration: add session_id to tool_call_events
154
+ try {
155
+ this.db.exec('ALTER TABLE tool_call_events ADD COLUMN session_id TEXT REFERENCES sessions(id)');
156
+ }
157
+ catch { }
158
+ // Migration: kind column for event type classification
159
+ try {
160
+ this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'");
161
+ }
162
+ catch { }
163
+ // Migration: workflow run persistence
164
+ try {
165
+ this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT');
166
+ }
167
+ catch { }
168
+ // Migration: tool_metrics table for per-tool metrics collection
169
+ try {
170
+ this.db.exec(`
171
+ CREATE TABLE IF NOT EXISTS tool_metrics (
172
+ id TEXT PRIMARY KEY,
173
+ tool_call_event_id TEXT NOT NULL,
174
+ session_id TEXT NOT NULL,
175
+ tool_name TEXT NOT NULL,
176
+ query TEXT,
177
+ status TEXT NOT NULL DEFAULT 'success',
178
+ result_count INTEGER DEFAULT 0,
179
+ result_size_bytes INTEGER DEFAULT 0,
180
+ latency_ms INTEGER DEFAULT 0,
181
+ engine TEXT,
182
+ accuracy_query_id TEXT,
183
+ metadata TEXT,
184
+ created_at INTEGER NOT NULL
185
+ );
186
+
187
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_session ON tool_metrics(session_id);
188
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_tool ON tool_metrics(tool_name);
189
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_created ON tool_metrics(created_at);
190
+ `);
191
+ }
192
+ catch { }
193
+ // Migration: accuracy queries table for search quality tracking
194
+ try {
195
+ this.db.exec(`
196
+ CREATE TABLE IF NOT EXISTS accuracy_queries (
197
+ id TEXT PRIMARY KEY,
198
+ session_id TEXT NOT NULL,
199
+ engine TEXT NOT NULL CHECK(engine IN ('codegraph','knowledge','memory')),
200
+ query TEXT NOT NULL,
201
+ top_k_results TEXT NOT NULL,
202
+ selected_ids TEXT,
203
+ relevance_feedback TEXT CHECK(relevance_feedback IN ('hit','partial','miss')),
204
+ annotator_note TEXT,
205
+ annotated_at INTEGER,
206
+ created_at INTEGER NOT NULL
207
+ );
208
+
209
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
210
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
211
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
212
+ `);
213
+ }
214
+ catch { }
130
215
  }
131
216
  insertRun(run) {
132
217
  this.db.prepare(`
@@ -181,7 +266,7 @@ class DevFlowDatabase {
181
266
  this.db.prepare(`
182
267
  INSERT INTO telemetry_events (run_id, kind, timestamp, duration, success, tool_name, plugin_name, metadata)
183
268
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
184
- `).run(event.runId, event.kind, event.timestamp, event.duration, event.success, event.toolName, event.pluginName, JSON.stringify(event.metadata) ?? null);
269
+ `).run(event.runId ?? null, event.kind ?? null, event.timestamp ?? null, event.duration ?? null, event.success ?? null, event.toolName ?? null, event.pluginName ?? null, event.metadata ? JSON.stringify(event.metadata) : null);
185
270
  }
186
271
  getEvents(runId) {
187
272
  const rows = this.db.prepare('SELECT * FROM telemetry_events WHERE run_id = ?').all(runId);
@@ -234,13 +319,146 @@ class DevFlowDatabase {
234
319
  deleteSetting(key) {
235
320
  this.db.prepare('DELETE FROM settings WHERE key = ?').run(key);
236
321
  }
322
+ // ---- Sessions ----
323
+ insertSession(params) {
324
+ this.db.prepare(`
325
+ INSERT OR IGNORE INTO sessions (id, project_root, label, started_at, status)
326
+ VALUES (?, ?, ?, ?, 'running')
327
+ `).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
328
+ }
329
+ getSession(id) {
330
+ const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
331
+ if (!row)
332
+ return null;
333
+ return {
334
+ id: row.id,
335
+ projectRoot: row.project_root,
336
+ label: row.label,
337
+ startedAt: row.started_at,
338
+ finishedAt: row.finished_at,
339
+ durationMs: row.duration_ms,
340
+ totalToolCalls: row.total_tool_calls,
341
+ mcpToolCalls: row.mcp_tool_calls,
342
+ directToolCalls: row.direct_tool_calls,
343
+ subagentCount: row.subagent_count,
344
+ status: row.status,
345
+ };
346
+ }
347
+ listSessions(limit, offset, projectRoot) {
348
+ let query = 'SELECT * FROM sessions';
349
+ const params = [];
350
+ if (projectRoot) {
351
+ query += ' WHERE project_root = ?';
352
+ params.push(projectRoot);
353
+ }
354
+ query += ' ORDER BY started_at DESC LIMIT ? OFFSET ?';
355
+ params.push(Number(limit), Number(offset));
356
+ const rows = this.db.prepare(query).all(...params);
357
+ return rows.map((row) => ({
358
+ id: row.id,
359
+ projectRoot: row.project_root,
360
+ label: row.label,
361
+ startedAt: row.started_at,
362
+ finishedAt: row.finished_at,
363
+ durationMs: row.duration_ms,
364
+ totalToolCalls: row.total_tool_calls,
365
+ mcpToolCalls: row.mcp_tool_calls,
366
+ directToolCalls: row.direct_tool_calls,
367
+ subagentCount: row.subagent_count,
368
+ status: row.status,
369
+ }));
370
+ }
371
+ closeSession(id, finishedAt) {
372
+ const events = this.db.prepare('SELECT * FROM tool_call_events WHERE session_id = ? OR execution_id = ?').all(id, id);
373
+ // deduplicate by event_id
374
+ const seen = new Set();
375
+ const unique = events.filter((e) => {
376
+ if (seen.has(e.event_id))
377
+ return false;
378
+ seen.add(e.event_id);
379
+ return true;
380
+ });
381
+ const totalToolCalls = unique.length;
382
+ const mcpToolCalls = unique.filter((e) => e.is_mcp_tool === 1).length;
383
+ const directToolCalls = totalToolCalls - mcpToolCalls;
384
+ const subagentCount = unique.filter((e) => e.tool_type === 'subagent').length;
385
+ const timestamps = unique.map((e) => e.timestamp).filter(Boolean);
386
+ const durationMs = timestamps.length >= 2
387
+ ? Math.max(...timestamps) - Math.min(...timestamps)
388
+ : 0;
389
+ this.db.prepare(`
390
+ UPDATE sessions
391
+ SET finished_at = ?, duration_ms = ?, total_tool_calls = ?,
392
+ mcp_tool_calls = ?, direct_tool_calls = ?, subagent_count = ?,
393
+ status = 'completed'
394
+ WHERE id = ?
395
+ `).run(finishedAt ?? Date.now(), durationMs, totalToolCalls, mcpToolCalls, directToolCalls, subagentCount, id);
396
+ }
397
+ listToolCallEventsBySession(sessionId) {
398
+ const rows = this.db.prepare('SELECT * FROM tool_call_events WHERE session_id = ? ORDER BY timestamp ASC').all(sessionId);
399
+ return rows.map(row => ({
400
+ eventId: row.event_id,
401
+ executionId: row.execution_id,
402
+ sessionId: row.session_id,
403
+ timestamp: row.timestamp,
404
+ toolName: row.tool_name,
405
+ toolType: row.tool_type,
406
+ isMcpTool: row.is_mcp_tool === 1,
407
+ mcpToolName: row.mcp_tool_name,
408
+ mcpEnforced: row.mcp_enforced === 1,
409
+ mcpFallback: row.mcp_fallback === 1,
410
+ kind: row.kind,
411
+ input: row.input ? JSON.parse(row.input) : null,
412
+ output: row.output ? JSON.parse(row.output) : null,
413
+ duration: row.duration,
414
+ parentToolCallId: row.parent_tool_call_id,
415
+ subagentId: row.subagent_id,
416
+ error: row.error,
417
+ blocked: row.blocked === 1,
418
+ blockReason: row.block_reason,
419
+ }));
420
+ }
421
+ listToolCallEventsBySessions(sessionIds) {
422
+ if (sessionIds.length === 0)
423
+ return {};
424
+ const placeholders = sessionIds.map(() => '?').join(',');
425
+ const rows = this.db.prepare(`SELECT * FROM tool_call_events WHERE session_id IN (${placeholders}) ORDER BY timestamp ASC`).all(...sessionIds);
426
+ const grouped = {};
427
+ for (const row of rows) {
428
+ const sid = row.session_id;
429
+ if (!grouped[sid])
430
+ grouped[sid] = [];
431
+ grouped[sid].push({
432
+ eventId: row.event_id,
433
+ executionId: row.execution_id,
434
+ sessionId: row.session_id,
435
+ timestamp: row.timestamp,
436
+ toolName: row.tool_name,
437
+ toolType: row.tool_type,
438
+ isMcpTool: row.is_mcp_tool === 1,
439
+ mcpToolName: row.mcp_tool_name,
440
+ mcpEnforced: row.mcp_enforced === 1,
441
+ mcpFallback: row.mcp_fallback === 1,
442
+ kind: row.kind,
443
+ input: row.input ? JSON.parse(row.input) : null,
444
+ output: row.output ? JSON.parse(row.output) : null,
445
+ duration: row.duration,
446
+ parentToolCallId: row.parent_tool_call_id,
447
+ subagentId: row.subagent_id,
448
+ error: row.error,
449
+ blocked: row.blocked === 1,
450
+ blockReason: row.block_reason,
451
+ });
452
+ }
453
+ return grouped;
454
+ }
237
455
  // ---- Skill Executions ----
238
456
  insertSkillExecution(params) {
239
457
  this.db.prepare(`
240
458
  INSERT INTO skill_executions
241
- (execution_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
242
- VALUES (?, ?, ?, ?, ?, ?)
243
- `).run(params.executionId, params.skillName, params.startedAt, params.status, params.requiredMcpTools ? JSON.stringify(params.requiredMcpTools) : null, params.availableMcpTools ? JSON.stringify(params.availableMcpTools) : null);
459
+ (execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
460
+ VALUES (?, ?, ?, ?, ?, ?, ?)
461
+ `).run(params.executionId, params.sessionId ?? null, params.skillName, params.startedAt, params.status, params.requiredMcpTools ? JSON.stringify(params.requiredMcpTools) : null, params.availableMcpTools ? JSON.stringify(params.availableMcpTools) : null);
244
462
  }
245
463
  getSkillExecution(executionId) {
246
464
  const row = this.db.prepare('SELECT * FROM skill_executions WHERE execution_id = ?').get(executionId);
@@ -248,6 +466,7 @@ class DevFlowDatabase {
248
466
  return null;
249
467
  return {
250
468
  executionId: row.execution_id,
469
+ sessionId: row.session_id,
251
470
  skillName: row.skill_name,
252
471
  startedAt: row.started_at,
253
472
  finishedAt: row.finished_at,
@@ -258,7 +477,6 @@ class DevFlowDatabase {
258
477
  mcpToolCalls: row.mcp_tool_calls,
259
478
  directToolCalls: row.direct_tool_calls,
260
479
  subagentCount: row.subagent_count,
261
- totalTokens: row.total_tokens,
262
480
  totalDuration: row.total_duration,
263
481
  mcpComplianceRate: row.mcp_compliance_rate,
264
482
  missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
@@ -280,10 +498,6 @@ class DevFlowDatabase {
280
498
  sets.push('total_duration = ?');
281
499
  values.push(updates.totalDuration);
282
500
  }
283
- if (updates.totalTokens !== undefined) {
284
- sets.push('total_tokens = ?');
285
- values.push(updates.totalTokens);
286
- }
287
501
  if (updates.mcpComplianceRate !== undefined) {
288
502
  sets.push('mcp_compliance_rate = ?');
289
503
  values.push(updates.mcpComplianceRate);
@@ -314,13 +528,6 @@ class DevFlowDatabase {
314
528
  }
315
529
  }
316
530
  listSkillExecutions(limit, skillName) {
317
- // Auto-timeout stale running executions (> 30 minutes)
318
- const timeoutThreshold = Date.now() - 30 * 60 * 1000;
319
- this.db.prepare(`
320
- UPDATE skill_executions
321
- SET status = 'failed', finished_at = ?
322
- WHERE status = 'running' AND started_at < ?
323
- `).run(Date.now(), timeoutThreshold);
324
531
  let query = 'SELECT * FROM skill_executions';
325
532
  const params = [];
326
533
  if (skillName) {
@@ -332,6 +539,7 @@ class DevFlowDatabase {
332
539
  const rows = this.db.prepare(query).all(...params);
333
540
  return rows.map(row => ({
334
541
  executionId: row.execution_id,
542
+ sessionId: row.session_id,
335
543
  skillName: row.skill_name,
336
544
  startedAt: row.started_at,
337
545
  finishedAt: row.finished_at,
@@ -340,7 +548,6 @@ class DevFlowDatabase {
340
548
  mcpToolCalls: row.mcp_tool_calls,
341
549
  directToolCalls: row.direct_tool_calls,
342
550
  subagentCount: row.subagent_count,
343
- totalTokens: row.total_tokens,
344
551
  totalDuration: row.total_duration,
345
552
  mcpComplianceRate: row.mcp_compliance_rate,
346
553
  }));
@@ -349,11 +556,11 @@ class DevFlowDatabase {
349
556
  insertToolCallEvent(params) {
350
557
  this.db.prepare(`
351
558
  INSERT INTO tool_call_events
352
- (event_id, execution_id, timestamp, tool_name, tool_type, is_mcp_tool,
353
- mcp_tool_name, mcp_enforced, mcp_fallback, input, output, tokens_used, duration,
354
- parent_tool_call_id, subagent_id, error, blocked, block_reason)
355
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
356
- `).run(params.eventId, params.executionId, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, JSON.stringify(params.input) ?? null, params.output ? JSON.stringify(params.output) : null, params.tokensUsed, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null);
559
+ (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
560
+ mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
561
+ parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
562
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
563
+ `).run(params.eventId, params.executionId || null, params.sessionId ?? null, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, params.kind ?? 'tool_use', JSON.stringify(params.input) ?? null, params.output ? JSON.stringify(params.output) : null, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null);
357
564
  // Update execution summary counters
358
565
  this.db.prepare(`
359
566
  UPDATE skill_executions
@@ -369,6 +576,7 @@ class DevFlowDatabase {
369
576
  return rows.map(row => ({
370
577
  eventId: row.event_id,
371
578
  executionId: row.execution_id,
579
+ sessionId: row.session_id,
372
580
  timestamp: row.timestamp,
373
581
  toolName: row.tool_name,
374
582
  toolType: row.tool_type,
@@ -376,9 +584,9 @@ class DevFlowDatabase {
376
584
  mcpToolName: row.mcp_tool_name,
377
585
  mcpEnforced: row.mcp_enforced === 1,
378
586
  mcpFallback: row.mcp_fallback === 1,
587
+ kind: row.kind,
379
588
  input: row.input ? JSON.parse(row.input) : null,
380
589
  output: row.output ? JSON.parse(row.output) : null,
381
- tokensUsed: row.tokens_used,
382
590
  duration: row.duration,
383
591
  parentToolCallId: row.parent_tool_call_id,
384
592
  subagentId: row.subagent_id,
@@ -398,6 +606,10 @@ class DevFlowDatabase {
398
606
  sets.push('error = ?');
399
607
  values.push(updates.error);
400
608
  }
609
+ if (updates.duration !== undefined) {
610
+ sets.push('duration = ?');
611
+ values.push(updates.duration);
612
+ }
401
613
  if (sets.length > 0) {
402
614
  values.push(eventId);
403
615
  this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
@@ -441,6 +653,117 @@ class DevFlowDatabase {
441
653
  const missedTools = missedRows.map((row) => row.mcp_tool_name);
442
654
  return { overall, bySkill, missedTools };
443
655
  }
656
+ cleanupStaleRecords() {
657
+ const cutoff = Date.now() - 30 * 60 * 1000;
658
+ try {
659
+ this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
660
+ this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
661
+ }
662
+ catch { /* best-effort */ }
663
+ }
664
+ insertToolMetric(metric) {
665
+ this.db.prepare(`
666
+ INSERT OR REPLACE INTO tool_metrics (id, tool_call_event_id, session_id, tool_name, query, status, result_count, result_size_bytes, latency_ms, engine, accuracy_query_id, metadata, created_at)
667
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
668
+ `).run(metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName, metric.query ?? null, metric.status ?? 'success', metric.resultCount ?? 0, metric.resultSizeBytes ?? 0, metric.latencyMs ?? 0, metric.engine ?? null, metric.accuracyQueryId ?? null, metric.metadata ?? null, metric.createdAt);
669
+ }
670
+ getToolMetrics(sessionId, toolName, limit = 100, offset = 0) {
671
+ let sql = 'SELECT * FROM tool_metrics WHERE 1=1';
672
+ const params = [];
673
+ if (sessionId) {
674
+ sql += ' AND session_id = ?';
675
+ params.push(sessionId);
676
+ }
677
+ if (toolName) {
678
+ sql += ' AND tool_name = ?';
679
+ params.push(toolName);
680
+ }
681
+ sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
682
+ params.push(limit, offset);
683
+ return this.db.prepare(sql).all(...params);
684
+ }
685
+ getToolMetricsSummary(days = 7) {
686
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
687
+ const rows = this.db.prepare(`
688
+ SELECT tool_name, COUNT(*) as call_count,
689
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count,
690
+ SUM(CASE WHEN result_count = 0 THEN 1 ELSE 0 END) as empty_count,
691
+ AVG(result_count) as avg_results,
692
+ AVG(latency_ms) as avg_latency
693
+ FROM tool_metrics
694
+ WHERE created_at >= ?
695
+ GROUP BY tool_name
696
+ ORDER BY call_count DESC
697
+ `).all(cutoff);
698
+ return rows.map((r) => ({
699
+ toolName: r.tool_name,
700
+ callCount: r.call_count,
701
+ successRate: r.call_count > 0 ? Math.round((r.success_count / r.call_count) * 10000) / 100 : 0,
702
+ emptyRate: r.call_count > 0 ? Math.round((r.empty_count / r.call_count) * 10000) / 100 : 0,
703
+ avgResults: Math.round(r.avg_results * 10) / 10,
704
+ avgLatency: Math.round(r.avg_latency),
705
+ }));
706
+ }
707
+ // ---- Accuracy Queries ----
708
+ insertAccuracyQuery(q) {
709
+ this.db.prepare(`
710
+ INSERT INTO accuracy_queries (id, session_id, engine, query, top_k_results, selected_ids, created_at)
711
+ VALUES (?, ?, ?, ?, ?, ?, ?)
712
+ `).run(q.id, q.sessionId, q.engine, q.query, q.topKResults, q.selectedIds ?? null, q.createdAt);
713
+ }
714
+ getAccuracyQueries(options) {
715
+ let sql = 'SELECT * FROM accuracy_queries WHERE 1=1';
716
+ const params = [];
717
+ if (options.engine) {
718
+ sql += ' AND engine = ?';
719
+ params.push(options.engine);
720
+ }
721
+ if (options.status === 'annotated')
722
+ sql += ' AND relevance_feedback IS NOT NULL';
723
+ if (options.status === 'unannotated')
724
+ sql += ' AND relevance_feedback IS NULL';
725
+ sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
726
+ params.push(options.limit ?? 50, options.offset ?? 0);
727
+ const rows = this.db.prepare(sql).all(...params);
728
+ return rows.map((r) => ({
729
+ id: r.id, sessionId: r.session_id, engine: r.engine, query: r.query,
730
+ topKResults: r.top_k_results ? JSON.parse(r.top_k_results) : [],
731
+ selectedIds: r.selected_ids ? JSON.parse(r.selected_ids) : null,
732
+ relevanceFeedback: r.relevance_feedback,
733
+ annotatorNote: r.annotator_note,
734
+ annotatedAt: r.annotated_at,
735
+ createdAt: r.created_at,
736
+ }));
737
+ }
738
+ updateAccuracyFeedback(id, feedback) {
739
+ this.db.prepare('UPDATE accuracy_queries SET relevance_feedback = ?, annotator_note = ?, annotated_at = ? WHERE id = ?').run(feedback.relevance, feedback.note ?? null, Date.now(), id);
740
+ }
741
+ getAccuracyStats(engine, since) {
742
+ let where = 'WHERE 1=1';
743
+ const params = [];
744
+ if (engine) {
745
+ where += ' AND engine = ?';
746
+ params.push(engine);
747
+ }
748
+ if (since) {
749
+ where += ' AND created_at >= ?';
750
+ params.push(since);
751
+ }
752
+ const totalQueries = this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where}`).get(...params)?.c ?? 0;
753
+ const annotatedQueries = this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback IS NOT NULL`).get(...params)?.c ?? 0;
754
+ const hitCount = annotatedQueries > 0
755
+ ? (this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback = 'hit'`).get(...params)?.c ?? 0)
756
+ : 0;
757
+ const hitRate = annotatedQueries > 0 ? hitCount / annotatedQueries : null;
758
+ return { totalQueries, annotatedQueries, hitRate };
759
+ }
760
+ // Convenience methods for raw SQL queries (used by AutoChecker)
761
+ all(sql, ...params) {
762
+ return this.db.prepare(sql).all(...params);
763
+ }
764
+ get(sql, ...params) {
765
+ return this.db.prepare(sql).get(...params);
766
+ }
444
767
  close() {
445
768
  this.db.close();
446
769
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-tools/database",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
4
4
  "description": "DevFlow SQLite database package",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,5 +13,5 @@
13
13
  "typescript": "^5.5.0",
14
14
  "vitest": "^2.0.0"
15
15
  },
16
- "gitHead": "ac4f93d6a52fa40b4126b514e899eee581f8f46e"
16
+ "gitHead": "7872bccedbacd09cc92b983f30b3702db6878125"
17
17
  }