@devflow-tools/database 0.15.0 → 0.16.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
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DevFlowDatabase = void 0;
4
+ exports.getGlobalDevFlowDbPath = getGlobalDevFlowDbPath;
5
+ exports.openGlobalDevFlowDatabase = openGlobalDevFlowDatabase;
4
6
  const node_sqlite_1 = require("./node-sqlite");
5
7
  const path_1 = require("path");
6
8
  const fs_1 = require("fs");
9
+ const os_1 = require("os");
10
+ function getGlobalDevFlowDbPath(home = (0, os_1.homedir)()) {
11
+ const stateDir = process.env.DEVFLOW_STATE_DIR ?? (0, path_1.join)(home, '.devflow', 'global');
12
+ return (0, path_1.join)(stateDir, 'devflow.db');
13
+ }
14
+ function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
15
+ return new DevFlowDatabase(home, {
16
+ dbPath: getGlobalDevFlowDbPath(home),
17
+ busyTimeoutMs: options?.busyTimeoutMs,
18
+ });
19
+ }
7
20
  class DevFlowDatabase {
8
21
  constructor(projectRoot, opts) {
9
22
  let dbPath;
@@ -19,9 +32,9 @@ class DevFlowDatabase {
19
32
  (0, fs_1.mkdirSync)(devflowDir, { recursive: true });
20
33
  dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
21
34
  }
22
- this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
35
+ this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
23
36
  this.db.exec('PRAGMA journal_mode = WAL');
24
- this.db.exec('PRAGMA busy_timeout = 5000');
37
+ this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
25
38
  this.db.exec('PRAGMA foreign_keys = OFF');
26
39
  this.initializeSchema();
27
40
  }
@@ -123,7 +136,8 @@ class DevFlowDatabase {
123
136
  subagent_id TEXT,
124
137
  error TEXT,
125
138
  blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
126
- block_reason TEXT
139
+ block_reason TEXT,
140
+ failure_category TEXT
127
141
  );
128
142
 
129
143
  CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
@@ -151,6 +165,80 @@ class DevFlowDatabase {
151
165
 
152
166
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
153
167
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
168
+
169
+ CREATE TABLE IF NOT EXISTS feedback (
170
+ id TEXT PRIMARY KEY,
171
+ project_root TEXT NOT NULL,
172
+ query TEXT NOT NULL,
173
+ rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
174
+ task_type TEXT,
175
+ context_mode TEXT,
176
+ run_id TEXT,
177
+ token_count INTEGER DEFAULT 0,
178
+ created_at INTEGER NOT NULL
179
+ );
180
+
181
+ CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
182
+ CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
183
+
184
+ CREATE TABLE IF NOT EXISTS telemetry_failures (
185
+ id TEXT PRIMARY KEY,
186
+ operation TEXT NOT NULL,
187
+ payload TEXT,
188
+ error TEXT NOT NULL,
189
+ created_at INTEGER NOT NULL,
190
+ resolved_at INTEGER
191
+ );
192
+
193
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
194
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
195
+
196
+ CREATE TABLE IF NOT EXISTS devflow_rules (
197
+ id TEXT PRIMARY KEY,
198
+ name TEXT NOT NULL,
199
+ level TEXT NOT NULL DEFAULT 'project',
200
+ gate INTEGER NOT NULL DEFAULT 1,
201
+ condition_json TEXT NOT NULL,
202
+ action TEXT NOT NULL DEFAULT 'warn',
203
+ message TEXT NOT NULL,
204
+ enabled INTEGER NOT NULL DEFAULT 1,
205
+ created_at INTEGER NOT NULL,
206
+ updated_at INTEGER NOT NULL
207
+ );
208
+
209
+ CREATE TABLE IF NOT EXISTS devflow_audit_chain (
210
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
211
+ ts INTEGER NOT NULL,
212
+ tool TEXT NOT NULL,
213
+ args_hash TEXT NOT NULL,
214
+ decision TEXT NOT NULL,
215
+ rule_id TEXT,
216
+ session_id TEXT NOT NULL,
217
+ prev_hash TEXT NOT NULL,
218
+ signature TEXT NOT NULL
219
+ );
220
+
221
+ CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
222
+
223
+ CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
224
+ project_root TEXT PRIMARY KEY,
225
+ last_mcp_call INTEGER,
226
+ bypass_count INTEGER NOT NULL DEFAULT 0,
227
+ updated_at INTEGER NOT NULL
228
+ );
229
+
230
+ CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
231
+ id TEXT PRIMARY KEY,
232
+ project_root TEXT NOT NULL,
233
+ session_id TEXT,
234
+ trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
235
+ pending_events INTEGER NOT NULL DEFAULT 0,
236
+ released_leases INTEGER NOT NULL DEFAULT 0,
237
+ created_at INTEGER NOT NULL
238
+ );
239
+
240
+ CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
241
+ ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
154
242
  `);
155
243
  // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
156
244
  try {
@@ -177,6 +265,20 @@ class DevFlowDatabase {
177
265
  this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT');
178
266
  }
179
267
  catch { }
268
+ try {
269
+ this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT');
270
+ }
271
+ catch { }
272
+ try {
273
+ this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1');
274
+ }
275
+ catch { }
276
+ try {
277
+ this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
278
+ }
279
+ catch { }
280
+ this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
281
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
180
282
  // Migration: tool_metrics table for per-tool metrics collection
181
283
  try {
182
284
  this.db.exec(`
@@ -221,6 +323,28 @@ class DevFlowDatabase {
221
323
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
222
324
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
223
325
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
326
+ `);
327
+ }
328
+ catch { }
329
+ // Canonical benchmark reports live in the global DevFlow database.
330
+ try {
331
+ this.db.exec(`
332
+ CREATE TABLE IF NOT EXISTS benchmark_reports (
333
+ run_id TEXT PRIMARY KEY,
334
+ suite_id TEXT NOT NULL,
335
+ suite_version TEXT NOT NULL,
336
+ project_root TEXT NOT NULL,
337
+ commit_sha TEXT,
338
+ task_count INTEGER NOT NULL,
339
+ status TEXT NOT NULL CHECK(status IN ('completed')),
340
+ report_json TEXT NOT NULL,
341
+ created_at INTEGER NOT NULL
342
+ );
343
+
344
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
345
+ ON benchmark_reports(created_at DESC);
346
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
347
+ ON benchmark_reports(suite_id, suite_version, created_at DESC);
224
348
  `);
225
349
  }
226
350
  catch { }
@@ -272,7 +396,7 @@ class DevFlowDatabase {
272
396
  }
273
397
  insertRun(run) {
274
398
  this.db.prepare(`
275
- INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
399
+ INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
276
400
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
277
401
  `).run(run.id, run.source, run.tool, JSON.stringify(run.input) ?? null, run.status, run.startedAt, run.finishedAt ?? null, run.tokenUsed ?? 0, JSON.stringify(run.metadata) ?? null, Date.now());
278
402
  }
@@ -476,6 +600,7 @@ class DevFlowDatabase {
476
600
  error: row.error,
477
601
  blocked: row.blocked === 1,
478
602
  blockReason: row.block_reason,
603
+ failureCategory: row.failure_category,
479
604
  tokensUsed: row.tokens_used ?? 0,
480
605
  }));
481
606
  }
@@ -509,6 +634,7 @@ class DevFlowDatabase {
509
634
  error: row.error,
510
635
  blocked: row.blocked === 1,
511
636
  blockReason: row.block_reason,
637
+ failureCategory: row.failure_category,
512
638
  tokensUsed: row.tokens_used ?? 0,
513
639
  });
514
640
  }
@@ -517,7 +643,7 @@ class DevFlowDatabase {
517
643
  // ---- Skill Executions ----
518
644
  insertSkillExecution(params) {
519
645
  this.db.prepare(`
520
- INSERT INTO skill_executions
646
+ INSERT OR IGNORE INTO skill_executions
521
647
  (execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
522
648
  VALUES (?, ?, ?, ?, ?, ?, ?)
523
649
  `).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);
@@ -616,22 +742,27 @@ class DevFlowDatabase {
616
742
  }
617
743
  // ---- Tool Call Events ----
618
744
  insertToolCallEvent(params) {
619
- this.db.prepare(`
620
- INSERT INTO tool_call_events
621
- (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
622
- mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
623
- parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
624
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
625
- `).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);
626
- // Update execution summary counters
627
- this.db.prepare(`
628
- UPDATE skill_executions
629
- SET total_tool_calls = total_tool_calls + 1,
630
- mcp_tool_calls = mcp_tool_calls + ?,
631
- direct_tool_calls = direct_tool_calls + ?,
632
- subagent_count = subagent_count + ?
633
- WHERE execution_id = ?
634
- `).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.executionId);
745
+ return this.db.transaction(() => {
746
+ const result = this.db.prepare(`
747
+ INSERT OR IGNORE INTO tool_call_events
748
+ (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
749
+ mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
750
+ parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
751
+ failure_category)
752
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
753
+ `).run(params.eventId, params.executionId || '', 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 !== undefined ? JSON.stringify(params.output) : null, params.tokensUsed ?? 0, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null, params.failureCategory || null);
754
+ if (result.changes === 0)
755
+ return false;
756
+ this.db.prepare(`
757
+ UPDATE skill_executions
758
+ SET total_tool_calls = total_tool_calls + 1,
759
+ mcp_tool_calls = mcp_tool_calls + ?,
760
+ direct_tool_calls = direct_tool_calls + ?,
761
+ subagent_count = subagent_count + ?
762
+ WHERE execution_id = ?
763
+ `).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.executionId);
764
+ return true;
765
+ });
635
766
  }
636
767
  listToolCallEvents(executionId) {
637
768
  const rows = this.db.prepare('SELECT * FROM tool_call_events WHERE execution_id = ? ORDER BY timestamp ASC').all(executionId);
@@ -655,6 +786,7 @@ class DevFlowDatabase {
655
786
  error: row.error,
656
787
  blocked: row.blocked === 1,
657
788
  blockReason: row.block_reason,
789
+ failureCategory: row.failure_category,
658
790
  }));
659
791
  }
660
792
  updateToolCallEvent(eventId, updates) {
@@ -674,8 +806,10 @@ class DevFlowDatabase {
674
806
  }
675
807
  if (sets.length > 0) {
676
808
  values.push(eventId);
677
- this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
809
+ const result = this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
810
+ return result.changes > 0;
678
811
  }
812
+ return false;
679
813
  }
680
814
  // ---- MCP Compliance ----
681
815
  getMcpCompliance() {
@@ -725,7 +859,7 @@ class DevFlowDatabase {
725
859
  }
726
860
  insertToolMetric(metric) {
727
861
  this.db.prepare(`
728
- 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)
862
+ INSERT OR IGNORE 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)
729
863
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
730
864
  `).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);
731
865
  }
@@ -766,6 +900,111 @@ class DevFlowDatabase {
766
900
  avgLatency: Math.round(r.avg_latency),
767
901
  }));
768
902
  }
903
+ aggregatePendingToolMetrics(limit = 1000) {
904
+ const rows = this.db.prepare(`
905
+ SELECT e.*
906
+ FROM tool_call_events e
907
+ LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
908
+ WHERE m.id IS NULL AND e.output IS NOT NULL
909
+ ORDER BY e.timestamp ASC
910
+ LIMIT ?
911
+ `).all(limit);
912
+ let inserted = 0;
913
+ this.db.transaction(() => {
914
+ for (const row of rows) {
915
+ const input = parseJson(row.input);
916
+ const output = parseJson(row.output);
917
+ const toolName = row.mcp_tool_name || row.tool_name;
918
+ const resultCount = countTelemetryResults(output);
919
+ const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
920
+ const status = row.error || row.blocked
921
+ ? 'error'
922
+ : resultCount === 0 ? 'empty' : 'success';
923
+ const result = this.db.prepare(`
924
+ INSERT OR IGNORE INTO tool_metrics
925
+ (id, tool_call_event_id, session_id, tool_name, query, status, result_count,
926
+ result_size_bytes, latency_ms, engine, metadata, created_at)
927
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
928
+ `).run(`metric:${row.event_id}`, row.event_id, row.session_id || '', toolName, deriveQuery(input), status, resultCount, resultSizeBytes, row.duration ?? 0, deriveEngine(toolName), JSON.stringify({ executionId: row.execution_id, kind: row.kind }), row.timestamp);
929
+ inserted += result.changes;
930
+ }
931
+ });
932
+ return inserted;
933
+ }
934
+ // ---- Context Feedback ----
935
+ insertFeedback(feedback) {
936
+ this.db.prepare(`
937
+ INSERT OR IGNORE INTO feedback
938
+ (id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
939
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
940
+ `).run(feedback.id, feedback.projectRoot, feedback.query, feedback.rating, feedback.taskType ?? null, feedback.contextMode ?? null, feedback.runId ?? null, feedback.tokenCount ?? 0, feedback.createdAt);
941
+ }
942
+ listFeedback(projectRoot, limit = 100) {
943
+ const rows = projectRoot
944
+ ? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
945
+ : this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
946
+ return rows.map(row => ({
947
+ id: row.id,
948
+ projectRoot: row.project_root,
949
+ query: row.query,
950
+ rating: row.rating,
951
+ taskType: row.task_type ?? undefined,
952
+ contextMode: row.context_mode ?? undefined,
953
+ runId: row.run_id ?? undefined,
954
+ tokenCount: row.token_count ?? 0,
955
+ createdAt: row.created_at,
956
+ }));
957
+ }
958
+ getFeedbackSummary(projectRoot) {
959
+ const row = (projectRoot
960
+ ? this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback WHERE project_root = ?`).get(projectRoot)
961
+ : this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get());
962
+ const total = Number(row?.total ?? 0);
963
+ const hits = Number(row?.hits ?? 0);
964
+ const partials = Number(row?.partials ?? 0);
965
+ const misses = Number(row?.misses ?? 0);
966
+ return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
967
+ }
968
+ // ---- Telemetry Failures ----
969
+ insertTelemetryFailure(failure) {
970
+ this.db.prepare(`
971
+ INSERT OR IGNORE INTO telemetry_failures
972
+ (id, operation, payload, error, created_at, resolved_at)
973
+ VALUES (?, ?, ?, ?, ?, ?)
974
+ `).run(failure.id, failure.operation, JSON.stringify(failure.payload) ?? null, failure.error, failure.createdAt, failure.resolvedAt ?? null);
975
+ }
976
+ resolveTelemetryFailure(id, resolvedAt = Date.now()) {
977
+ this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
978
+ }
979
+ listTelemetryFailures(options) {
980
+ const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
981
+ const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
982
+ .all(options?.limit ?? 100);
983
+ return rows.map(row => ({
984
+ id: row.id,
985
+ operation: row.operation,
986
+ payload: parseJson(row.payload),
987
+ error: row.error,
988
+ createdAt: row.created_at,
989
+ resolvedAt: row.resolved_at ?? undefined,
990
+ }));
991
+ }
992
+ countTelemetryFailures(unresolvedOnly = false) {
993
+ const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get();
994
+ return Number(row?.count ?? 0);
995
+ }
996
+ trimTelemetryFailures(maxRows = 500) {
997
+ const limit = Math.max(1, Math.floor(maxRows));
998
+ const result = this.db.prepare(`
999
+ DELETE FROM telemetry_failures
1000
+ WHERE id IN (
1001
+ SELECT id FROM telemetry_failures
1002
+ ORDER BY created_at DESC, id DESC
1003
+ LIMIT -1 OFFSET ?
1004
+ )
1005
+ `).run(limit);
1006
+ return Number(result.changes);
1007
+ }
769
1008
  // ---- Accuracy Queries ----
770
1009
  insertAccuracyQuery(q) {
771
1010
  this.db.prepare(`
@@ -830,6 +1069,202 @@ class DevFlowDatabase {
830
1069
  }
831
1070
  return statsByEngine;
832
1071
  }
1072
+ // ---- Benchmark Reports ----
1073
+ insertBenchmarkReport(report) {
1074
+ this.db.prepare(`
1075
+ INSERT INTO benchmark_reports
1076
+ (run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
1077
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1078
+ ON CONFLICT(run_id) DO UPDATE SET
1079
+ suite_id = excluded.suite_id,
1080
+ suite_version = excluded.suite_version,
1081
+ project_root = excluded.project_root,
1082
+ commit_sha = excluded.commit_sha,
1083
+ task_count = excluded.task_count,
1084
+ status = excluded.status,
1085
+ report_json = excluded.report_json,
1086
+ created_at = excluded.created_at
1087
+ `).run(report.runId, report.suiteId, report.suiteVersion, report.projectRoot, report.commit, report.tasks.length, report.status, JSON.stringify(report), report.createdAt);
1088
+ }
1089
+ getBenchmarkReport(runId) {
1090
+ const row = this.db.prepare('SELECT report_json FROM benchmark_reports WHERE run_id = ?').get(runId);
1091
+ return row?.report_json
1092
+ ? JSON.parse(row.report_json)
1093
+ : null;
1094
+ }
1095
+ getLatestBenchmarkReport(suiteId) {
1096
+ const row = (suiteId
1097
+ ? this.db.prepare('SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1').get(suiteId)
1098
+ : this.db.prepare('SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1').get());
1099
+ return row?.report_json
1100
+ ? JSON.parse(row.report_json)
1101
+ : null;
1102
+ }
1103
+ listBenchmarkReports(limit = 50) {
1104
+ const rows = this.db.prepare(`
1105
+ SELECT run_id, suite_id, suite_version, project_root, commit_sha,
1106
+ task_count, status, created_at
1107
+ FROM benchmark_reports
1108
+ ORDER BY created_at DESC
1109
+ LIMIT ?
1110
+ `).all(Math.max(1, Math.min(limit, 500)));
1111
+ return rows.map((row) => ({
1112
+ runId: row.run_id,
1113
+ suiteId: row.suite_id,
1114
+ suiteVersion: row.suite_version,
1115
+ projectRoot: row.project_root,
1116
+ commit: row.commit_sha,
1117
+ taskCount: row.task_count,
1118
+ createdAt: row.created_at,
1119
+ status: row.status,
1120
+ }));
1121
+ }
1122
+ // ---- Enforcer Governance ----
1123
+ listGovernanceRules(includeDisabled = true) {
1124
+ const where = includeDisabled ? '' : 'WHERE enabled = 1';
1125
+ const rows = this.db.prepare(`
1126
+ SELECT id, name, gate, level, condition_json, action, message,
1127
+ enabled, created_at, updated_at
1128
+ FROM devflow_rules ${where}
1129
+ ORDER BY gate, id
1130
+ `).all();
1131
+ return rows.map(row => ({
1132
+ id: row.id,
1133
+ name: row.name,
1134
+ gate: row.gate,
1135
+ level: row.level,
1136
+ condition: parseJson(row.condition_json),
1137
+ action: row.action,
1138
+ message: row.message,
1139
+ enabled: row.enabled === 1,
1140
+ createdAt: row.created_at,
1141
+ updatedAt: row.updated_at,
1142
+ }));
1143
+ }
1144
+ getGovernanceRule(id) {
1145
+ return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
1146
+ }
1147
+ upsertGovernanceRule(rule) {
1148
+ const now = Date.now();
1149
+ this.db.prepare(`
1150
+ INSERT INTO devflow_rules
1151
+ (id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
1152
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1153
+ ON CONFLICT(id) DO UPDATE SET
1154
+ name = excluded.name,
1155
+ gate = excluded.gate,
1156
+ level = excluded.level,
1157
+ condition_json = excluded.condition_json,
1158
+ action = excluded.action,
1159
+ message = excluded.message,
1160
+ enabled = excluded.enabled,
1161
+ updated_at = excluded.updated_at
1162
+ `).run(rule.id, rule.name, rule.gate, rule.level, JSON.stringify(rule.condition), rule.action, rule.message, rule.enabled ? 1 : 0, rule.createdAt ?? now, now);
1163
+ return this.getGovernanceRule(rule.id);
1164
+ }
1165
+ deleteGovernanceRule(id) {
1166
+ return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
1167
+ }
1168
+ listGovernanceAudit(limit = 100, offset = 0) {
1169
+ const boundedLimit = Math.max(1, Math.min(limit, 500));
1170
+ const boundedOffset = Math.max(0, offset);
1171
+ const rows = this.db.prepare(`
1172
+ SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
1173
+ FROM devflow_audit_chain
1174
+ ORDER BY seq DESC
1175
+ LIMIT ? OFFSET ?
1176
+ `).all(boundedLimit, boundedOffset);
1177
+ const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get();
1178
+ return {
1179
+ items: rows.map(row => ({
1180
+ seq: row.seq,
1181
+ ts: row.ts,
1182
+ tool: row.tool,
1183
+ argsHash: row.args_hash,
1184
+ decision: row.decision,
1185
+ ruleId: row.rule_id,
1186
+ sessionId: row.session_id,
1187
+ prevHash: row.prev_hash,
1188
+ signature: row.signature,
1189
+ })),
1190
+ total: Number(count?.total ?? 0),
1191
+ };
1192
+ }
1193
+ // ---- Hook Lifecycle ----
1194
+ getHookReceipt(projectRoot) {
1195
+ const row = this.db.prepare(`
1196
+ SELECT project_root, last_mcp_call, bypass_count, updated_at
1197
+ FROM devflow_hook_receipts WHERE project_root = ?
1198
+ `).get(projectRoot);
1199
+ if (!row)
1200
+ return null;
1201
+ return {
1202
+ projectRoot: row.project_root,
1203
+ lastMcpCall: row.last_mcp_call ?? undefined,
1204
+ bypassCount: Number(row.bypass_count ?? 0),
1205
+ updatedAt: Number(row.updated_at),
1206
+ };
1207
+ }
1208
+ updateHookReceipt(projectRoot, updater) {
1209
+ this.db.exec('BEGIN IMMEDIATE');
1210
+ try {
1211
+ const current = this.getHookReceipt(projectRoot);
1212
+ const next = updater(current);
1213
+ const updatedAt = Date.now();
1214
+ this.db.prepare(`
1215
+ INSERT INTO devflow_hook_receipts
1216
+ (project_root, last_mcp_call, bypass_count, updated_at)
1217
+ VALUES (?, ?, ?, ?)
1218
+ ON CONFLICT(project_root) DO UPDATE SET
1219
+ last_mcp_call = excluded.last_mcp_call,
1220
+ bypass_count = excluded.bypass_count,
1221
+ updated_at = excluded.updated_at
1222
+ `).run(projectRoot, next.lastMcpCall ?? null, next.bypassCount ?? 0, updatedAt);
1223
+ this.db.exec('COMMIT');
1224
+ return {
1225
+ projectRoot,
1226
+ lastMcpCall: next.lastMcpCall,
1227
+ bypassCount: next.bypassCount ?? 0,
1228
+ updatedAt,
1229
+ };
1230
+ }
1231
+ catch (error) {
1232
+ try {
1233
+ this.db.exec('ROLLBACK');
1234
+ }
1235
+ catch { }
1236
+ throw error;
1237
+ }
1238
+ }
1239
+ deleteHookReceipt(projectRoot) {
1240
+ return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
1241
+ .run(projectRoot).changes > 0;
1242
+ }
1243
+ recordMemoryDistillCheckpoint(checkpoint) {
1244
+ this.db.prepare(`
1245
+ INSERT INTO devflow_memory_distill_checkpoints
1246
+ (id, project_root, session_id, trigger, pending_events, released_leases, created_at)
1247
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1248
+ `).run(checkpoint.id, checkpoint.projectRoot, checkpoint.sessionId ?? null, checkpoint.trigger, checkpoint.pendingEvents, checkpoint.releasedLeases, checkpoint.createdAt);
1249
+ }
1250
+ listMemoryDistillCheckpoints(projectRoot, limit = 50) {
1251
+ const rows = this.db.prepare(`
1252
+ SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
1253
+ FROM devflow_memory_distill_checkpoints
1254
+ WHERE project_root = ?
1255
+ ORDER BY created_at DESC
1256
+ LIMIT ?
1257
+ `).all(projectRoot, Math.max(1, Math.min(limit, 500)));
1258
+ return rows.map(row => ({
1259
+ id: row.id,
1260
+ projectRoot: row.project_root,
1261
+ sessionId: row.session_id ?? undefined,
1262
+ trigger: row.trigger,
1263
+ pendingEvents: Number(row.pending_events),
1264
+ releasedLeases: Number(row.released_leases),
1265
+ createdAt: Number(row.created_at),
1266
+ }));
1267
+ }
833
1268
  // Convenience methods for raw SQL queries (used by AutoChecker)
834
1269
  all(sql, ...params) {
835
1270
  return this.db.prepare(sql).all(...params);
@@ -842,3 +1277,54 @@ class DevFlowDatabase {
842
1277
  }
843
1278
  }
844
1279
  exports.DevFlowDatabase = DevFlowDatabase;
1280
+ function parseJson(value) {
1281
+ if (typeof value !== 'string')
1282
+ return value;
1283
+ try {
1284
+ return JSON.parse(value);
1285
+ }
1286
+ catch {
1287
+ return value;
1288
+ }
1289
+ }
1290
+ function deriveQuery(input) {
1291
+ if (!input || typeof input !== 'object')
1292
+ return null;
1293
+ const record = input;
1294
+ for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
1295
+ if (typeof record[key] === 'string' && record[key])
1296
+ return record[key];
1297
+ }
1298
+ return null;
1299
+ }
1300
+ function deriveEngine(toolName) {
1301
+ if (/project_context|symbol|dependency_graph|codegraph/.test(toolName))
1302
+ return 'codegraph';
1303
+ if (/knowledge|docs/.test(toolName))
1304
+ return 'knowledge';
1305
+ if (/memory/.test(toolName))
1306
+ return 'memory';
1307
+ return null;
1308
+ }
1309
+ function countTelemetryResults(value, depth = 0) {
1310
+ if (value == null || depth > 2)
1311
+ return 0;
1312
+ if (Array.isArray(value))
1313
+ return value.length;
1314
+ if (typeof value !== 'object')
1315
+ return value === '' ? 0 : 1;
1316
+ const record = value;
1317
+ const unwrapped = record.data ?? record.structuredContent;
1318
+ if (unwrapped !== undefined && unwrapped !== value)
1319
+ return countTelemetryResults(unwrapped, depth + 1);
1320
+ let count = 0;
1321
+ for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
1322
+ if (Array.isArray(record[key]))
1323
+ count += record[key].length;
1324
+ }
1325
+ if (count > 0)
1326
+ return count;
1327
+ if (record.error)
1328
+ return 0;
1329
+ return Object.keys(record).length > 0 ? 1 : 0;
1330
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { DevFlowDatabase } from './database';
1
+ export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
2
+ export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookReceiptRecord, MemoryDistillCheckpointRecord, TelemetryFailureRecord, } from './database';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DevFlowDatabase = void 0;
3
+ exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
4
4
  var database_1 = require("./database");
5
5
  Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
6
+ Object.defineProperty(exports, "getGlobalDevFlowDbPath", { enumerable: true, get: function () { return database_1.getGlobalDevFlowDbPath; } });
7
+ Object.defineProperty(exports, "openGlobalDevFlowDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowDatabase; } });
@@ -1,7 +1,7 @@
1
1
  import type { Database, Statement } from './types';
2
2
  export declare class NodeSqliteDatabase implements Database {
3
3
  private db;
4
- constructor(path: string);
4
+ constructor(path: string, busyTimeoutMs?: number);
5
5
  exec(sql: string): void;
6
6
  prepare(sql: string): Statement;
7
7
  transaction<T>(fn: () => T): T;