@devflow-tools/database 0.14.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/CHANGELOG.md +29 -0
- package/__tests__/database.failure-category.test.ts +44 -0
- package/__tests__/database.test.ts +30 -0
- package/__tests__/node-sqlite.test.ts +8 -0
- package/dist/database.d.ts +121 -2
- package/dist/database.js +509 -23
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -5
- package/package.json +2 -2
- package/src/database.ts +715 -52
- package/src/index.ts +15 -1
- package/src/node-sqlite.ts +9 -5
- package/tsconfig.tsbuildinfo +1 -1
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(
|
|
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.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
|
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; } });
|
package/dist/node-sqlite.d.ts
CHANGED
|
@@ -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;
|