@devflow-tools/database 0.18.4 → 0.18.5

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.
@@ -198,6 +198,15 @@ export declare class DevFlowDatabase {
198
198
  getRunSteps(runId: string): any[];
199
199
  insertEvent(event: any): void;
200
200
  getEvents(runId: string): any[];
201
+ appendDashboardRunEvent(event: {
202
+ runId: string;
203
+ projectRoot: string;
204
+ sequence: number;
205
+ kind: string;
206
+ payload?: unknown;
207
+ timestamp?: number;
208
+ }): void;
209
+ listDashboardRunEvents(runId: string, projectRoot: string, after?: number): any[];
201
210
  getProjectStats(projectRoot: string): any | null;
202
211
  upsertProjectStats(stats: any): void;
203
212
  getRuns(): any[];
@@ -256,7 +265,7 @@ export declare class DevFlowDatabase {
256
265
  metadata?: Record<string, unknown>;
257
266
  }): void;
258
267
  mergeSkillExecutionMetadata(executionId: string, incoming: Record<string, unknown>): Record<string, unknown> | null;
259
- listSkillExecutions(limit: number, skillName?: string): any[];
268
+ listSkillExecutions(limit: number, skillName?: string, projectRoot?: string): any[];
260
269
  insertToolCallEvent(params: {
261
270
  eventId: string;
262
271
  executionId: string;
@@ -299,7 +308,7 @@ export declare class DevFlowDatabase {
299
308
  missedTools: string[];
300
309
  mcpCallShare: number;
301
310
  };
302
- getMcpCompliance(): {
311
+ getMcpCompliance(projectRoot?: string): {
303
312
  overall: number;
304
313
  bySkill: Record<string, number>;
305
314
  missedTools: string[];
@@ -323,8 +332,8 @@ export declare class DevFlowDatabase {
323
332
  metadata?: string;
324
333
  createdAt: number;
325
334
  }): void;
326
- getToolMetrics(sessionId?: string, toolName?: string, limit?: number, offset?: number): any[];
327
- getToolMetricsSummary(days?: number): any[];
335
+ getToolMetrics(sessionId?: string, toolName?: string, limit?: number, offset?: number, projectRoot?: string): any[];
336
+ getToolMetricsSummary(days?: number, projectRoot?: string): any[];
328
337
  aggregatePendingToolMetrics(limit?: number): number;
329
338
  insertFeedback(feedback: FeedbackRecord): void;
330
339
  listFeedback(projectRoot?: string, limit?: number): FeedbackRecord[];
@@ -357,12 +366,13 @@ export declare class DevFlowDatabase {
357
366
  status?: string;
358
367
  limit?: number;
359
368
  offset?: number;
369
+ projectRoot?: string;
360
370
  }): any[];
361
371
  updateAccuracyFeedback(id: string, feedback: {
362
372
  relevance: 'hit' | 'partial' | 'miss';
363
373
  note?: string;
364
374
  }): void;
365
- getAccuracyStats(engine?: string, since?: number): any;
375
+ getAccuracyStats(engine?: string, since?: number, projectRoot?: string): any;
366
376
  insertBenchmarkReport<T extends BenchmarkReportRecord>(report: T): void;
367
377
  getBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(runId: string): T | null;
368
378
  getLatestBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(suiteId?: string): T | null;
package/dist/database.js CHANGED
@@ -159,6 +159,19 @@ class DevFlowDatabase {
159
159
  FOREIGN KEY (run_id) REFERENCES runs(id)
160
160
  );
161
161
 
162
+ CREATE TABLE IF NOT EXISTS dashboard_run_events (
163
+ run_id TEXT NOT NULL,
164
+ project_root TEXT NOT NULL,
165
+ sequence INTEGER NOT NULL,
166
+ kind TEXT NOT NULL,
167
+ payload TEXT,
168
+ timestamp INTEGER NOT NULL,
169
+ PRIMARY KEY (project_root, run_id, sequence)
170
+ );
171
+
172
+ CREATE INDEX IF NOT EXISTS idx_dashboard_run_events_project
173
+ ON dashboard_run_events(project_root, run_id, sequence);
174
+
162
175
  CREATE TABLE IF NOT EXISTS project_stats (
163
176
  project_root TEXT PRIMARY KEY,
164
177
  total_runs INTEGER DEFAULT 0,
@@ -1077,6 +1090,34 @@ class DevFlowDatabase {
1077
1090
  CREATE INDEX IF NOT EXISTS idx_merge_queue_project
1078
1091
  ON devflow_merge_queue(project_root, state, created_at, merge_id);
1079
1092
  `);
1093
+ // Migration: dashboard run event cursors originally keyed only by run/sequence.
1094
+ // Rebuild the small append-only table so project isolation is part of the key.
1095
+ try {
1096
+ const columns = this.db.prepare('PRAGMA table_info(dashboard_run_events)').all();
1097
+ if (columns.length > 0 && columns.some((column) => column.name === 'project_root')) {
1098
+ const primaryKey = this.db.prepare('PRAGMA table_info(dashboard_run_events)').all();
1099
+ if (primaryKey.filter((column) => column.pk > 0).map((column) => column.name).join(',') === 'run_id,sequence') {
1100
+ this.db.exec('ALTER TABLE dashboard_run_events RENAME TO dashboard_run_events_legacy');
1101
+ this.db.exec(`
1102
+ CREATE TABLE dashboard_run_events (
1103
+ run_id TEXT NOT NULL,
1104
+ project_root TEXT NOT NULL,
1105
+ sequence INTEGER NOT NULL,
1106
+ kind TEXT NOT NULL,
1107
+ payload TEXT,
1108
+ timestamp INTEGER NOT NULL,
1109
+ PRIMARY KEY (project_root, run_id, sequence)
1110
+ );
1111
+ INSERT OR IGNORE INTO dashboard_run_events (run_id, project_root, sequence, kind, payload, timestamp)
1112
+ SELECT run_id, project_root, sequence, kind, payload, timestamp FROM dashboard_run_events_legacy;
1113
+ DROP TABLE dashboard_run_events_legacy;
1114
+ CREATE INDEX IF NOT EXISTS idx_dashboard_run_events_project
1115
+ ON dashboard_run_events(project_root, run_id, sequence);
1116
+ `);
1117
+ }
1118
+ }
1119
+ }
1120
+ catch { }
1080
1121
  // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
1081
1122
  try {
1082
1123
  this.db.exec('ALTER TABLE skill_executions ADD COLUMN session_id TEXT REFERENCES sessions(id)');
@@ -1414,6 +1455,28 @@ class DevFlowDatabase {
1414
1455
  metadata: row.metadata ? JSON.parse(row.metadata) : null
1415
1456
  }));
1416
1457
  }
1458
+ appendDashboardRunEvent(event) {
1459
+ this.db.prepare(`
1460
+ INSERT OR IGNORE INTO dashboard_run_events (run_id, project_root, sequence, kind, payload, timestamp)
1461
+ VALUES (?, ?, ?, ?, ?, ?)
1462
+ `).run(event.runId, event.projectRoot, event.sequence, event.kind, event.payload === undefined ? null : JSON.stringify(event.payload), event.timestamp ?? Date.now());
1463
+ }
1464
+ listDashboardRunEvents(runId, projectRoot, after = 0) {
1465
+ const rows = this.db.prepare(`
1466
+ SELECT run_id, project_root, sequence, kind, payload, timestamp
1467
+ FROM dashboard_run_events
1468
+ WHERE run_id = ? AND project_root = ? AND sequence > ?
1469
+ ORDER BY sequence ASC
1470
+ `).all(runId, projectRoot, after);
1471
+ return rows.map((row) => ({
1472
+ runId: row.run_id,
1473
+ projectRoot: row.project_root,
1474
+ sequence: row.sequence,
1475
+ kind: row.kind,
1476
+ payload: row.payload ? JSON.parse(row.payload) : null,
1477
+ timestamp: row.timestamp,
1478
+ }));
1479
+ }
1417
1480
  getProjectStats(projectRoot) {
1418
1481
  const row = this.db.prepare('SELECT * FROM project_stats WHERE project_root = ?').get(projectRoot);
1419
1482
  if (!row)
@@ -1759,14 +1822,21 @@ class DevFlowDatabase {
1759
1822
  this.updateSkillExecution(executionId, { metadata: merged });
1760
1823
  return merged;
1761
1824
  }
1762
- listSkillExecutions(limit, skillName) {
1763
- let query = 'SELECT * FROM skill_executions';
1825
+ listSkillExecutions(limit, skillName, projectRoot) {
1826
+ let query = 'SELECT e.* FROM skill_executions e LEFT JOIN sessions s ON s.id = e.session_id';
1764
1827
  const params = [];
1828
+ const conditions = [];
1765
1829
  if (skillName) {
1766
- query += ' WHERE skill_name = ?';
1830
+ conditions.push('e.skill_name = ?');
1767
1831
  params.push(skillName);
1768
1832
  }
1769
- query += ' ORDER BY started_at DESC LIMIT ?';
1833
+ if (projectRoot) {
1834
+ conditions.push('s.project_root = ?');
1835
+ params.push(projectRoot);
1836
+ }
1837
+ if (conditions.length)
1838
+ query += ` WHERE ${conditions.join(' AND ')}`;
1839
+ query += ' ORDER BY e.started_at DESC LIMIT ?';
1770
1840
  params.push(Number(limit));
1771
1841
  const rows = this.db.prepare(query).all(...params);
1772
1842
  return rows.map(row => ({
@@ -2038,8 +2108,10 @@ class DevFlowDatabase {
2038
2108
  };
2039
2109
  }
2040
2110
  // ---- MCP Compliance ----
2041
- getMcpCompliance() {
2042
- const executions = this.db.prepare('SELECT execution_id, skill_name FROM skill_executions').all();
2111
+ getMcpCompliance(projectRoot) {
2112
+ const executions = this.db.prepare(projectRoot
2113
+ ? 'SELECT e.execution_id, e.skill_name FROM skill_executions e JOIN sessions s ON s.id = e.session_id WHERE s.project_root = ?'
2114
+ : 'SELECT execution_id, skill_name FROM skill_executions').all(...(projectRoot ? [projectRoot] : []));
2043
2115
  const bySkill = {};
2044
2116
  const bySkillCounts = new Map();
2045
2117
  let applicableObligations = 0;
@@ -2093,34 +2165,41 @@ class DevFlowDatabase {
2093
2165
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2094
2166
  `).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);
2095
2167
  }
2096
- getToolMetrics(sessionId, toolName, limit = 100, offset = 0) {
2097
- let sql = 'SELECT * FROM tool_metrics WHERE 1=1';
2168
+ getToolMetrics(sessionId, toolName, limit = 100, offset = 0, projectRoot) {
2169
+ let sql = 'SELECT tm.* FROM tool_metrics tm LEFT JOIN sessions s ON s.id = tm.session_id WHERE 1=1';
2098
2170
  const params = [];
2099
2171
  if (sessionId) {
2100
- sql += ' AND session_id = ?';
2172
+ sql += ' AND tm.session_id = ?';
2101
2173
  params.push(sessionId);
2102
2174
  }
2103
2175
  if (toolName) {
2104
- sql += ' AND tool_name = ?';
2176
+ sql += ' AND tm.tool_name = ?';
2105
2177
  params.push(toolName);
2106
2178
  }
2107
- sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
2179
+ if (projectRoot) {
2180
+ sql += ' AND s.project_root = ?';
2181
+ params.push(projectRoot);
2182
+ }
2183
+ sql += ' ORDER BY tm.created_at DESC LIMIT ? OFFSET ?';
2108
2184
  params.push(limit, offset);
2109
2185
  return this.db.prepare(sql).all(...params);
2110
2186
  }
2111
- getToolMetricsSummary(days = 7) {
2187
+ getToolMetricsSummary(days = 7, projectRoot) {
2112
2188
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
2189
+ const projectClause = projectRoot ? ' AND s.project_root = ?' : '';
2190
+ const params = projectRoot ? [cutoff, projectRoot] : [cutoff];
2113
2191
  const rows = this.db.prepare(`
2114
- SELECT tool_name, COUNT(*) as call_count,
2115
- SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count,
2116
- SUM(CASE WHEN result_count = 0 THEN 1 ELSE 0 END) as empty_count,
2117
- AVG(result_count) as avg_results,
2118
- AVG(latency_ms) as avg_latency
2119
- FROM tool_metrics
2120
- WHERE created_at >= ?
2121
- GROUP BY tool_name
2192
+ SELECT tm.tool_name, COUNT(*) as call_count,
2193
+ SUM(CASE WHEN tm.status = 'success' THEN 1 ELSE 0 END) as success_count,
2194
+ SUM(CASE WHEN tm.result_count = 0 THEN 1 ELSE 0 END) as empty_count,
2195
+ AVG(tm.result_count) as avg_results,
2196
+ AVG(tm.latency_ms) as avg_latency
2197
+ FROM tool_metrics tm
2198
+ LEFT JOIN sessions s ON s.id = tm.session_id
2199
+ WHERE tm.created_at >= ?${projectClause}
2200
+ GROUP BY tm.tool_name
2122
2201
  ORDER BY call_count DESC
2123
- `).all(cutoff);
2202
+ `).all(...params);
2124
2203
  return rows.map((r) => ({
2125
2204
  toolName: r.tool_name,
2126
2205
  callCount: r.call_count,
@@ -2247,17 +2326,21 @@ class DevFlowDatabase {
2247
2326
  `).run(q.id, q.sessionId, q.engine, q.query, q.topKResults, q.selectedIds ?? null, q.createdAt);
2248
2327
  }
2249
2328
  getAccuracyQueries(options) {
2250
- let sql = 'SELECT * FROM accuracy_queries WHERE 1=1';
2329
+ let sql = 'SELECT aq.* FROM accuracy_queries aq LEFT JOIN sessions s ON s.id = aq.session_id WHERE 1=1';
2251
2330
  const params = [];
2252
2331
  if (options.engine) {
2253
- sql += ' AND engine = ?';
2332
+ sql += ' AND aq.engine = ?';
2254
2333
  params.push(options.engine);
2255
2334
  }
2256
2335
  if (options.status === 'annotated')
2257
- sql += ' AND relevance_feedback IS NOT NULL';
2336
+ sql += ' AND aq.relevance_feedback IS NOT NULL';
2258
2337
  if (options.status === 'unannotated')
2259
- sql += ' AND relevance_feedback IS NULL';
2260
- sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
2338
+ sql += ' AND aq.relevance_feedback IS NULL';
2339
+ if (options.projectRoot) {
2340
+ sql += ' AND s.project_root = ?';
2341
+ params.push(options.projectRoot);
2342
+ }
2343
+ sql += ' ORDER BY aq.created_at DESC LIMIT ? OFFSET ?';
2261
2344
  params.push(options.limit ?? 50, options.offset ?? 0);
2262
2345
  const rows = this.db.prepare(sql).all(...params);
2263
2346
  return rows.map((r) => ({
@@ -2273,24 +2356,29 @@ class DevFlowDatabase {
2273
2356
  updateAccuracyFeedback(id, feedback) {
2274
2357
  this.db.prepare('UPDATE accuracy_queries SET relevance_feedback = ?, annotator_note = ?, annotated_at = ? WHERE id = ?').run(feedback.relevance, feedback.note ?? null, Date.now(), id);
2275
2358
  }
2276
- getAccuracyStats(engine, since) {
2359
+ getAccuracyStats(engine, since, projectRoot) {
2277
2360
  let where = 'WHERE 1=1';
2278
2361
  const params = [];
2279
2362
  if (engine) {
2280
- where += ' AND engine = ?';
2363
+ where += ' AND aq.engine = ?';
2281
2364
  params.push(engine);
2282
2365
  }
2283
2366
  if (since) {
2284
- where += ' AND created_at >= ?';
2367
+ where += ' AND aq.created_at >= ?';
2285
2368
  params.push(since);
2286
2369
  }
2370
+ if (projectRoot) {
2371
+ where += ' AND s.project_root = ?';
2372
+ params.push(projectRoot);
2373
+ }
2287
2374
  const rows = this.db.prepare(`
2288
2375
  SELECT
2289
2376
  engine,
2290
2377
  COUNT(*) as totalQueries,
2291
2378
  SUM(CASE WHEN relevance_feedback IS NOT NULL THEN 1 ELSE 0 END) as annotatedQueries,
2292
2379
  SUM(CASE WHEN relevance_feedback = 'hit' THEN 1 ELSE 0 END) as hitCount
2293
- FROM accuracy_queries ${where}
2380
+ FROM accuracy_queries aq
2381
+ LEFT JOIN sessions s ON s.id = aq.session_id ${where}
2294
2382
  GROUP BY engine
2295
2383
  `).all(...params);
2296
2384
  const statsByEngine = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-tools/database",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "SQLite persistence primitives shared by DevFlow runtime packages.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "test": "vitest run"
13
13
  },
14
14
  "dependencies": {
15
- "@devflow-tools/sdk": "0.18.4"
15
+ "@devflow-tools/sdk": "0.18.5"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^22.15.0",