@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/CHANGELOG.md +21 -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/src/database.ts
CHANGED
|
@@ -1,11 +1,112 @@
|
|
|
1
1
|
import { NodeSqliteDatabase } from './node-sqlite';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface BenchmarkReportRecord {
|
|
7
|
+
runId: string;
|
|
8
|
+
suiteId: string;
|
|
9
|
+
suiteVersion: string;
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
commit: string | null;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
status: 'completed';
|
|
14
|
+
tasks: unknown[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BenchmarkReportMetaRecord {
|
|
18
|
+
runId: string;
|
|
19
|
+
suiteId: string;
|
|
20
|
+
suiteVersion: string;
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
commit: string | null;
|
|
23
|
+
taskCount: number;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
status: 'completed';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FeedbackRecord {
|
|
29
|
+
id: string;
|
|
30
|
+
projectRoot: string;
|
|
31
|
+
query: string;
|
|
32
|
+
rating: 'hit' | 'partial' | 'miss';
|
|
33
|
+
taskType?: string;
|
|
34
|
+
contextMode?: 'general' | 'task';
|
|
35
|
+
runId?: string;
|
|
36
|
+
tokenCount?: number;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TelemetryFailureRecord {
|
|
41
|
+
id: string;
|
|
42
|
+
operation: string;
|
|
43
|
+
payload: unknown;
|
|
44
|
+
error: string;
|
|
45
|
+
createdAt: number;
|
|
46
|
+
resolvedAt?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GovernanceRuleRecord {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
gate: 1 | 2 | 3 | 4;
|
|
53
|
+
level: 'constitutional' | 'project' | 'session';
|
|
54
|
+
condition: Record<string, unknown>;
|
|
55
|
+
action: 'deny' | 'warn' | 'allow';
|
|
56
|
+
message: string;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
createdAt?: number;
|
|
59
|
+
updatedAt?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GovernanceAuditRecord {
|
|
63
|
+
seq: number;
|
|
64
|
+
ts: number;
|
|
65
|
+
tool: string;
|
|
66
|
+
argsHash: string;
|
|
67
|
+
decision: 'allow' | 'deny' | 'warn';
|
|
68
|
+
ruleId: string | null;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
prevHash: string;
|
|
71
|
+
signature: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface HookReceiptRecord {
|
|
75
|
+
projectRoot: string;
|
|
76
|
+
lastMcpCall?: number;
|
|
77
|
+
bypassCount?: number;
|
|
78
|
+
updatedAt: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface MemoryDistillCheckpointRecord {
|
|
82
|
+
id: string;
|
|
83
|
+
projectRoot: string;
|
|
84
|
+
sessionId?: string;
|
|
85
|
+
trigger: 'pre_compact' | 'session_end';
|
|
86
|
+
pendingEvents: number;
|
|
87
|
+
releasedLeases: number;
|
|
88
|
+
createdAt: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
92
|
+
const stateDir = process.env.DEVFLOW_STATE_DIR ?? join(home, '.devflow', 'global');
|
|
93
|
+
return join(stateDir, 'devflow.db');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function openGlobalDevFlowDatabase(
|
|
97
|
+
home = homedir(),
|
|
98
|
+
options?: { busyTimeoutMs?: number },
|
|
99
|
+
): DevFlowDatabase {
|
|
100
|
+
return new DevFlowDatabase(home, {
|
|
101
|
+
dbPath: getGlobalDevFlowDbPath(home),
|
|
102
|
+
busyTimeoutMs: options?.busyTimeoutMs,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
4
105
|
|
|
5
106
|
export class DevFlowDatabase {
|
|
6
107
|
private db: NodeSqliteDatabase;
|
|
7
108
|
|
|
8
|
-
constructor(projectRoot: string, opts?: { dbPath?: string }) {
|
|
109
|
+
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
|
|
9
110
|
let dbPath: string;
|
|
10
111
|
if (opts?.dbPath) {
|
|
11
112
|
const dir = dirname(opts.dbPath);
|
|
@@ -16,10 +117,10 @@ export class DevFlowDatabase {
|
|
|
16
117
|
if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
|
|
17
118
|
dbPath = join(devflowDir, 'devflow.db');
|
|
18
119
|
}
|
|
19
|
-
this.db = new NodeSqliteDatabase(dbPath);
|
|
120
|
+
this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
|
|
20
121
|
|
|
21
122
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
22
|
-
this.db.exec(
|
|
123
|
+
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
23
124
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
24
125
|
|
|
25
126
|
this.initializeSchema();
|
|
@@ -123,7 +224,8 @@ export class DevFlowDatabase {
|
|
|
123
224
|
subagent_id TEXT,
|
|
124
225
|
error TEXT,
|
|
125
226
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
126
|
-
block_reason TEXT
|
|
227
|
+
block_reason TEXT,
|
|
228
|
+
failure_category TEXT
|
|
127
229
|
);
|
|
128
230
|
|
|
129
231
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -151,6 +253,80 @@ export class DevFlowDatabase {
|
|
|
151
253
|
|
|
152
254
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
153
255
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
256
|
+
|
|
257
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
258
|
+
id TEXT PRIMARY KEY,
|
|
259
|
+
project_root TEXT NOT NULL,
|
|
260
|
+
query TEXT NOT NULL,
|
|
261
|
+
rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
|
|
262
|
+
task_type TEXT,
|
|
263
|
+
context_mode TEXT,
|
|
264
|
+
run_id TEXT,
|
|
265
|
+
token_count INTEGER DEFAULT 0,
|
|
266
|
+
created_at INTEGER NOT NULL
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
|
|
271
|
+
|
|
272
|
+
CREATE TABLE IF NOT EXISTS telemetry_failures (
|
|
273
|
+
id TEXT PRIMARY KEY,
|
|
274
|
+
operation TEXT NOT NULL,
|
|
275
|
+
payload TEXT,
|
|
276
|
+
error TEXT NOT NULL,
|
|
277
|
+
created_at INTEGER NOT NULL,
|
|
278
|
+
resolved_at INTEGER
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
|
|
283
|
+
|
|
284
|
+
CREATE TABLE IF NOT EXISTS devflow_rules (
|
|
285
|
+
id TEXT PRIMARY KEY,
|
|
286
|
+
name TEXT NOT NULL,
|
|
287
|
+
level TEXT NOT NULL DEFAULT 'project',
|
|
288
|
+
gate INTEGER NOT NULL DEFAULT 1,
|
|
289
|
+
condition_json TEXT NOT NULL,
|
|
290
|
+
action TEXT NOT NULL DEFAULT 'warn',
|
|
291
|
+
message TEXT NOT NULL,
|
|
292
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
293
|
+
created_at INTEGER NOT NULL,
|
|
294
|
+
updated_at INTEGER NOT NULL
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
CREATE TABLE IF NOT EXISTS devflow_audit_chain (
|
|
298
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
299
|
+
ts INTEGER NOT NULL,
|
|
300
|
+
tool TEXT NOT NULL,
|
|
301
|
+
args_hash TEXT NOT NULL,
|
|
302
|
+
decision TEXT NOT NULL,
|
|
303
|
+
rule_id TEXT,
|
|
304
|
+
session_id TEXT NOT NULL,
|
|
305
|
+
prev_hash TEXT NOT NULL,
|
|
306
|
+
signature TEXT NOT NULL
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
|
|
310
|
+
|
|
311
|
+
CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
|
|
312
|
+
project_root TEXT PRIMARY KEY,
|
|
313
|
+
last_mcp_call INTEGER,
|
|
314
|
+
bypass_count INTEGER NOT NULL DEFAULT 0,
|
|
315
|
+
updated_at INTEGER NOT NULL
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
319
|
+
id TEXT PRIMARY KEY,
|
|
320
|
+
project_root TEXT NOT NULL,
|
|
321
|
+
session_id TEXT,
|
|
322
|
+
trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
|
|
323
|
+
pending_events INTEGER NOT NULL DEFAULT 0,
|
|
324
|
+
released_leases INTEGER NOT NULL DEFAULT 0,
|
|
325
|
+
created_at INTEGER NOT NULL
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
|
|
329
|
+
ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
|
|
154
330
|
`);
|
|
155
331
|
|
|
156
332
|
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
@@ -163,6 +339,11 @@ export class DevFlowDatabase {
|
|
|
163
339
|
try { this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'"); } catch {}
|
|
164
340
|
// Migration: workflow run persistence
|
|
165
341
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT'); } catch {}
|
|
342
|
+
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT'); } catch {}
|
|
343
|
+
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
|
|
344
|
+
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER'); } catch {}
|
|
345
|
+
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
346
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
166
347
|
|
|
167
348
|
// Migration: tool_metrics table for per-tool metrics collection
|
|
168
349
|
try {
|
|
@@ -211,6 +392,28 @@ export class DevFlowDatabase {
|
|
|
211
392
|
`);
|
|
212
393
|
} catch {}
|
|
213
394
|
|
|
395
|
+
// Canonical benchmark reports live in the global DevFlow database.
|
|
396
|
+
try {
|
|
397
|
+
this.db.exec(`
|
|
398
|
+
CREATE TABLE IF NOT EXISTS benchmark_reports (
|
|
399
|
+
run_id TEXT PRIMARY KEY,
|
|
400
|
+
suite_id TEXT NOT NULL,
|
|
401
|
+
suite_version TEXT NOT NULL,
|
|
402
|
+
project_root TEXT NOT NULL,
|
|
403
|
+
commit_sha TEXT,
|
|
404
|
+
task_count INTEGER NOT NULL,
|
|
405
|
+
status TEXT NOT NULL CHECK(status IN ('completed')),
|
|
406
|
+
report_json TEXT NOT NULL,
|
|
407
|
+
created_at INTEGER NOT NULL
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
|
|
411
|
+
ON benchmark_reports(created_at DESC);
|
|
412
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
|
|
413
|
+
ON benchmark_reports(suite_id, suite_version, created_at DESC);
|
|
414
|
+
`);
|
|
415
|
+
} catch {}
|
|
416
|
+
|
|
214
417
|
// Migration: logging system views for agent queries
|
|
215
418
|
try {
|
|
216
419
|
this.db.exec(`
|
|
@@ -259,7 +462,7 @@ export class DevFlowDatabase {
|
|
|
259
462
|
|
|
260
463
|
insertRun(run: any): void {
|
|
261
464
|
this.db.prepare(`
|
|
262
|
-
INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
465
|
+
INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
263
466
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
264
467
|
`).run(
|
|
265
468
|
run.id,
|
|
@@ -535,6 +738,7 @@ export class DevFlowDatabase {
|
|
|
535
738
|
error: row.error,
|
|
536
739
|
blocked: row.blocked === 1,
|
|
537
740
|
blockReason: row.block_reason,
|
|
741
|
+
failureCategory: row.failure_category,
|
|
538
742
|
tokensUsed: row.tokens_used ?? 0,
|
|
539
743
|
}));
|
|
540
744
|
}
|
|
@@ -570,6 +774,7 @@ export class DevFlowDatabase {
|
|
|
570
774
|
error: row.error,
|
|
571
775
|
blocked: row.blocked === 1,
|
|
572
776
|
blockReason: row.block_reason,
|
|
777
|
+
failureCategory: row.failure_category,
|
|
573
778
|
tokensUsed: row.tokens_used ?? 0,
|
|
574
779
|
});
|
|
575
780
|
}
|
|
@@ -588,7 +793,7 @@ export class DevFlowDatabase {
|
|
|
588
793
|
availableMcpTools?: string[];
|
|
589
794
|
}): void {
|
|
590
795
|
this.db.prepare(`
|
|
591
|
-
INSERT INTO skill_executions
|
|
796
|
+
INSERT OR IGNORE INTO skill_executions
|
|
592
797
|
(execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
|
|
593
798
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
594
799
|
`).run(
|
|
@@ -707,50 +912,59 @@ export class DevFlowDatabase {
|
|
|
707
912
|
blocked: boolean;
|
|
708
913
|
blockReason?: string;
|
|
709
914
|
workflowRunId?: string;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
915
|
+
failureCategory?: string;
|
|
916
|
+
tokensUsed?: number;
|
|
917
|
+
}): boolean {
|
|
918
|
+
return this.db.transaction(() => {
|
|
919
|
+
const result = this.db.prepare(`
|
|
920
|
+
INSERT OR IGNORE INTO tool_call_events
|
|
921
|
+
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
922
|
+
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
|
|
923
|
+
parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
|
|
924
|
+
failure_category)
|
|
925
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
926
|
+
`).run(
|
|
927
|
+
params.eventId,
|
|
928
|
+
params.executionId || '',
|
|
929
|
+
params.sessionId ?? null,
|
|
930
|
+
params.timestamp,
|
|
931
|
+
params.toolName,
|
|
932
|
+
params.toolType,
|
|
933
|
+
params.isMcpTool ? 1 : 0,
|
|
934
|
+
params.mcpToolName || null,
|
|
935
|
+
params.mcpEnforced ? 1 : 0,
|
|
936
|
+
params.mcpFallback ? 1 : 0,
|
|
937
|
+
params.kind ?? 'tool_use',
|
|
938
|
+
JSON.stringify(params.input) ?? null,
|
|
939
|
+
params.output !== undefined ? JSON.stringify(params.output) : null,
|
|
940
|
+
params.tokensUsed ?? 0,
|
|
941
|
+
params.duration,
|
|
942
|
+
params.parentToolCallId || null,
|
|
943
|
+
params.subagentId || null,
|
|
944
|
+
params.error || null,
|
|
945
|
+
params.blocked ? 1 : 0,
|
|
946
|
+
params.blockReason || null,
|
|
947
|
+
params.workflowRunId || null,
|
|
948
|
+
params.failureCategory || null,
|
|
949
|
+
);
|
|
739
950
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
951
|
+
if (result.changes === 0) return false;
|
|
952
|
+
|
|
953
|
+
this.db.prepare(`
|
|
954
|
+
UPDATE skill_executions
|
|
955
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
956
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
957
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
958
|
+
subagent_count = subagent_count + ?
|
|
959
|
+
WHERE execution_id = ?
|
|
960
|
+
`).run(
|
|
961
|
+
params.isMcpTool ? 1 : 0,
|
|
962
|
+
params.isMcpTool ? 0 : 1,
|
|
963
|
+
params.toolType === 'subagent' ? 1 : 0,
|
|
964
|
+
params.executionId,
|
|
965
|
+
);
|
|
966
|
+
return true;
|
|
967
|
+
});
|
|
754
968
|
}
|
|
755
969
|
|
|
756
970
|
listToolCallEvents(executionId: string): any[] {
|
|
@@ -778,10 +992,11 @@ export class DevFlowDatabase {
|
|
|
778
992
|
error: row.error,
|
|
779
993
|
blocked: row.blocked === 1,
|
|
780
994
|
blockReason: row.block_reason,
|
|
995
|
+
failureCategory: row.failure_category,
|
|
781
996
|
}));
|
|
782
997
|
}
|
|
783
998
|
|
|
784
|
-
updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }):
|
|
999
|
+
updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }): boolean {
|
|
785
1000
|
const sets: string[] = [];
|
|
786
1001
|
const values: any[] = [];
|
|
787
1002
|
|
|
@@ -800,10 +1015,12 @@ export class DevFlowDatabase {
|
|
|
800
1015
|
|
|
801
1016
|
if (sets.length > 0) {
|
|
802
1017
|
values.push(eventId);
|
|
803
|
-
this.db.prepare(
|
|
1018
|
+
const result = this.db.prepare(
|
|
804
1019
|
`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`
|
|
805
1020
|
).run(...values);
|
|
1021
|
+
return result.changes > 0;
|
|
806
1022
|
}
|
|
1023
|
+
return false;
|
|
807
1024
|
}
|
|
808
1025
|
|
|
809
1026
|
// ---- MCP Compliance ----
|
|
@@ -884,7 +1101,7 @@ export class DevFlowDatabase {
|
|
|
884
1101
|
createdAt: number;
|
|
885
1102
|
}): void {
|
|
886
1103
|
this.db.prepare(`
|
|
887
|
-
INSERT OR
|
|
1104
|
+
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)
|
|
888
1105
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
889
1106
|
`).run(
|
|
890
1107
|
metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName,
|
|
@@ -928,6 +1145,153 @@ export class DevFlowDatabase {
|
|
|
928
1145
|
}));
|
|
929
1146
|
}
|
|
930
1147
|
|
|
1148
|
+
aggregatePendingToolMetrics(limit = 1000): number {
|
|
1149
|
+
const rows = this.db.prepare(`
|
|
1150
|
+
SELECT e.*
|
|
1151
|
+
FROM tool_call_events e
|
|
1152
|
+
LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
|
|
1153
|
+
WHERE m.id IS NULL AND e.output IS NOT NULL
|
|
1154
|
+
ORDER BY e.timestamp ASC
|
|
1155
|
+
LIMIT ?
|
|
1156
|
+
`).all(limit) as any[];
|
|
1157
|
+
|
|
1158
|
+
let inserted = 0;
|
|
1159
|
+
this.db.transaction(() => {
|
|
1160
|
+
for (const row of rows) {
|
|
1161
|
+
const input = parseJson(row.input);
|
|
1162
|
+
const output = parseJson(row.output);
|
|
1163
|
+
const toolName = row.mcp_tool_name || row.tool_name;
|
|
1164
|
+
const resultCount = countTelemetryResults(output);
|
|
1165
|
+
const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
|
|
1166
|
+
const status = row.error || row.blocked
|
|
1167
|
+
? 'error'
|
|
1168
|
+
: resultCount === 0 ? 'empty' : 'success';
|
|
1169
|
+
const result = this.db.prepare(`
|
|
1170
|
+
INSERT OR IGNORE INTO tool_metrics
|
|
1171
|
+
(id, tool_call_event_id, session_id, tool_name, query, status, result_count,
|
|
1172
|
+
result_size_bytes, latency_ms, engine, metadata, created_at)
|
|
1173
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1174
|
+
`).run(
|
|
1175
|
+
`metric:${row.event_id}`,
|
|
1176
|
+
row.event_id,
|
|
1177
|
+
row.session_id || '',
|
|
1178
|
+
toolName,
|
|
1179
|
+
deriveQuery(input),
|
|
1180
|
+
status,
|
|
1181
|
+
resultCount,
|
|
1182
|
+
resultSizeBytes,
|
|
1183
|
+
row.duration ?? 0,
|
|
1184
|
+
deriveEngine(toolName),
|
|
1185
|
+
JSON.stringify({ executionId: row.execution_id, kind: row.kind }),
|
|
1186
|
+
row.timestamp,
|
|
1187
|
+
);
|
|
1188
|
+
inserted += result.changes;
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
return inserted;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ---- Context Feedback ----
|
|
1195
|
+
|
|
1196
|
+
insertFeedback(feedback: FeedbackRecord): void {
|
|
1197
|
+
this.db.prepare(`
|
|
1198
|
+
INSERT OR IGNORE INTO feedback
|
|
1199
|
+
(id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
|
|
1200
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1201
|
+
`).run(
|
|
1202
|
+
feedback.id,
|
|
1203
|
+
feedback.projectRoot,
|
|
1204
|
+
feedback.query,
|
|
1205
|
+
feedback.rating,
|
|
1206
|
+
feedback.taskType ?? null,
|
|
1207
|
+
feedback.contextMode ?? null,
|
|
1208
|
+
feedback.runId ?? null,
|
|
1209
|
+
feedback.tokenCount ?? 0,
|
|
1210
|
+
feedback.createdAt,
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
listFeedback(projectRoot?: string, limit = 100): FeedbackRecord[] {
|
|
1215
|
+
const rows = projectRoot
|
|
1216
|
+
? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
|
|
1217
|
+
: this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
1218
|
+
return (rows as any[]).map(row => ({
|
|
1219
|
+
id: row.id,
|
|
1220
|
+
projectRoot: row.project_root,
|
|
1221
|
+
query: row.query,
|
|
1222
|
+
rating: row.rating,
|
|
1223
|
+
taskType: row.task_type ?? undefined,
|
|
1224
|
+
contextMode: row.context_mode ?? undefined,
|
|
1225
|
+
runId: row.run_id ?? undefined,
|
|
1226
|
+
tokenCount: row.token_count ?? 0,
|
|
1227
|
+
createdAt: row.created_at,
|
|
1228
|
+
}));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
getFeedbackSummary(projectRoot?: string): { total: number; hits: number; partials: number; misses: number; hitRate: number } {
|
|
1232
|
+
const row = (projectRoot
|
|
1233
|
+
? 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)
|
|
1234
|
+
: this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get()) as any;
|
|
1235
|
+
const total = Number(row?.total ?? 0);
|
|
1236
|
+
const hits = Number(row?.hits ?? 0);
|
|
1237
|
+
const partials = Number(row?.partials ?? 0);
|
|
1238
|
+
const misses = Number(row?.misses ?? 0);
|
|
1239
|
+
return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// ---- Telemetry Failures ----
|
|
1243
|
+
|
|
1244
|
+
insertTelemetryFailure(failure: TelemetryFailureRecord): void {
|
|
1245
|
+
this.db.prepare(`
|
|
1246
|
+
INSERT OR IGNORE INTO telemetry_failures
|
|
1247
|
+
(id, operation, payload, error, created_at, resolved_at)
|
|
1248
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1249
|
+
`).run(
|
|
1250
|
+
failure.id,
|
|
1251
|
+
failure.operation,
|
|
1252
|
+
JSON.stringify(failure.payload) ?? null,
|
|
1253
|
+
failure.error,
|
|
1254
|
+
failure.createdAt,
|
|
1255
|
+
failure.resolvedAt ?? null,
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
resolveTelemetryFailure(id: string, resolvedAt = Date.now()): void {
|
|
1260
|
+
this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
listTelemetryFailures(options?: { unresolvedOnly?: boolean; limit?: number }): TelemetryFailureRecord[] {
|
|
1264
|
+
const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
|
|
1265
|
+
const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
1266
|
+
.all(options?.limit ?? 100) as any[];
|
|
1267
|
+
return rows.map(row => ({
|
|
1268
|
+
id: row.id,
|
|
1269
|
+
operation: row.operation,
|
|
1270
|
+
payload: parseJson(row.payload),
|
|
1271
|
+
error: row.error,
|
|
1272
|
+
createdAt: row.created_at,
|
|
1273
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
1274
|
+
}));
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
countTelemetryFailures(unresolvedOnly = false): number {
|
|
1278
|
+
const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get() as any;
|
|
1279
|
+
return Number(row?.count ?? 0);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
trimTelemetryFailures(maxRows = 500): number {
|
|
1283
|
+
const limit = Math.max(1, Math.floor(maxRows));
|
|
1284
|
+
const result = this.db.prepare(`
|
|
1285
|
+
DELETE FROM telemetry_failures
|
|
1286
|
+
WHERE id IN (
|
|
1287
|
+
SELECT id FROM telemetry_failures
|
|
1288
|
+
ORDER BY created_at DESC, id DESC
|
|
1289
|
+
LIMIT -1 OFFSET ?
|
|
1290
|
+
)
|
|
1291
|
+
`).run(limit);
|
|
1292
|
+
return Number(result.changes);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
931
1295
|
// ---- Accuracy Queries ----
|
|
932
1296
|
|
|
933
1297
|
insertAccuracyQuery(q: {
|
|
@@ -992,6 +1356,266 @@ export class DevFlowDatabase {
|
|
|
992
1356
|
return statsByEngine;
|
|
993
1357
|
}
|
|
994
1358
|
|
|
1359
|
+
// ---- Benchmark Reports ----
|
|
1360
|
+
|
|
1361
|
+
insertBenchmarkReport<T extends BenchmarkReportRecord>(report: T): void {
|
|
1362
|
+
this.db.prepare(`
|
|
1363
|
+
INSERT INTO benchmark_reports
|
|
1364
|
+
(run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
|
|
1365
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1366
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1367
|
+
suite_id = excluded.suite_id,
|
|
1368
|
+
suite_version = excluded.suite_version,
|
|
1369
|
+
project_root = excluded.project_root,
|
|
1370
|
+
commit_sha = excluded.commit_sha,
|
|
1371
|
+
task_count = excluded.task_count,
|
|
1372
|
+
status = excluded.status,
|
|
1373
|
+
report_json = excluded.report_json,
|
|
1374
|
+
created_at = excluded.created_at
|
|
1375
|
+
`).run(
|
|
1376
|
+
report.runId,
|
|
1377
|
+
report.suiteId,
|
|
1378
|
+
report.suiteVersion,
|
|
1379
|
+
report.projectRoot,
|
|
1380
|
+
report.commit,
|
|
1381
|
+
report.tasks.length,
|
|
1382
|
+
report.status,
|
|
1383
|
+
JSON.stringify(report),
|
|
1384
|
+
report.createdAt,
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
getBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(runId: string): T | null {
|
|
1389
|
+
const row = this.db.prepare(
|
|
1390
|
+
'SELECT report_json FROM benchmark_reports WHERE run_id = ?',
|
|
1391
|
+
).get(runId) as { report_json?: string } | undefined;
|
|
1392
|
+
return row?.report_json
|
|
1393
|
+
? JSON.parse(row.report_json) as T
|
|
1394
|
+
: null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
getLatestBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(suiteId?: string): T | null {
|
|
1398
|
+
const row = (suiteId
|
|
1399
|
+
? this.db.prepare(
|
|
1400
|
+
'SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
1401
|
+
).get(suiteId)
|
|
1402
|
+
: this.db.prepare(
|
|
1403
|
+
'SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1',
|
|
1404
|
+
).get()) as { report_json?: string } | undefined;
|
|
1405
|
+
return row?.report_json
|
|
1406
|
+
? JSON.parse(row.report_json) as T
|
|
1407
|
+
: null;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
listBenchmarkReports(limit = 50): BenchmarkReportMetaRecord[] {
|
|
1411
|
+
const rows = this.db.prepare(`
|
|
1412
|
+
SELECT run_id, suite_id, suite_version, project_root, commit_sha,
|
|
1413
|
+
task_count, status, created_at
|
|
1414
|
+
FROM benchmark_reports
|
|
1415
|
+
ORDER BY created_at DESC
|
|
1416
|
+
LIMIT ?
|
|
1417
|
+
`).all(Math.max(1, Math.min(limit, 500))) as Array<{
|
|
1418
|
+
run_id: string;
|
|
1419
|
+
suite_id: string;
|
|
1420
|
+
suite_version: string;
|
|
1421
|
+
project_root: string;
|
|
1422
|
+
commit_sha: string | null;
|
|
1423
|
+
task_count: number;
|
|
1424
|
+
status: 'completed';
|
|
1425
|
+
created_at: number;
|
|
1426
|
+
}>;
|
|
1427
|
+
return rows.map((row) => ({
|
|
1428
|
+
runId: row.run_id,
|
|
1429
|
+
suiteId: row.suite_id,
|
|
1430
|
+
suiteVersion: row.suite_version,
|
|
1431
|
+
projectRoot: row.project_root,
|
|
1432
|
+
commit: row.commit_sha,
|
|
1433
|
+
taskCount: row.task_count,
|
|
1434
|
+
createdAt: row.created_at,
|
|
1435
|
+
status: row.status,
|
|
1436
|
+
}));
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ---- Enforcer Governance ----
|
|
1440
|
+
|
|
1441
|
+
listGovernanceRules(includeDisabled = true): GovernanceRuleRecord[] {
|
|
1442
|
+
const where = includeDisabled ? '' : 'WHERE enabled = 1';
|
|
1443
|
+
const rows = this.db.prepare(`
|
|
1444
|
+
SELECT id, name, gate, level, condition_json, action, message,
|
|
1445
|
+
enabled, created_at, updated_at
|
|
1446
|
+
FROM devflow_rules ${where}
|
|
1447
|
+
ORDER BY gate, id
|
|
1448
|
+
`).all() as any[];
|
|
1449
|
+
return rows.map(row => ({
|
|
1450
|
+
id: row.id,
|
|
1451
|
+
name: row.name,
|
|
1452
|
+
gate: row.gate,
|
|
1453
|
+
level: row.level,
|
|
1454
|
+
condition: parseJson(row.condition_json) as Record<string, unknown>,
|
|
1455
|
+
action: row.action,
|
|
1456
|
+
message: row.message,
|
|
1457
|
+
enabled: row.enabled === 1,
|
|
1458
|
+
createdAt: row.created_at,
|
|
1459
|
+
updatedAt: row.updated_at,
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
getGovernanceRule(id: string): GovernanceRuleRecord | null {
|
|
1464
|
+
return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
upsertGovernanceRule(rule: GovernanceRuleRecord): GovernanceRuleRecord {
|
|
1468
|
+
const now = Date.now();
|
|
1469
|
+
this.db.prepare(`
|
|
1470
|
+
INSERT INTO devflow_rules
|
|
1471
|
+
(id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
|
|
1472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1473
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1474
|
+
name = excluded.name,
|
|
1475
|
+
gate = excluded.gate,
|
|
1476
|
+
level = excluded.level,
|
|
1477
|
+
condition_json = excluded.condition_json,
|
|
1478
|
+
action = excluded.action,
|
|
1479
|
+
message = excluded.message,
|
|
1480
|
+
enabled = excluded.enabled,
|
|
1481
|
+
updated_at = excluded.updated_at
|
|
1482
|
+
`).run(
|
|
1483
|
+
rule.id,
|
|
1484
|
+
rule.name,
|
|
1485
|
+
rule.gate,
|
|
1486
|
+
rule.level,
|
|
1487
|
+
JSON.stringify(rule.condition),
|
|
1488
|
+
rule.action,
|
|
1489
|
+
rule.message,
|
|
1490
|
+
rule.enabled ? 1 : 0,
|
|
1491
|
+
rule.createdAt ?? now,
|
|
1492
|
+
now,
|
|
1493
|
+
);
|
|
1494
|
+
return this.getGovernanceRule(rule.id)!;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
deleteGovernanceRule(id: string): boolean {
|
|
1498
|
+
return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
listGovernanceAudit(limit = 100, offset = 0): { items: GovernanceAuditRecord[]; total: number } {
|
|
1502
|
+
const boundedLimit = Math.max(1, Math.min(limit, 500));
|
|
1503
|
+
const boundedOffset = Math.max(0, offset);
|
|
1504
|
+
const rows = this.db.prepare(`
|
|
1505
|
+
SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
|
|
1506
|
+
FROM devflow_audit_chain
|
|
1507
|
+
ORDER BY seq DESC
|
|
1508
|
+
LIMIT ? OFFSET ?
|
|
1509
|
+
`).all(boundedLimit, boundedOffset) as any[];
|
|
1510
|
+
const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get() as { total?: number } | undefined;
|
|
1511
|
+
return {
|
|
1512
|
+
items: rows.map(row => ({
|
|
1513
|
+
seq: row.seq,
|
|
1514
|
+
ts: row.ts,
|
|
1515
|
+
tool: row.tool,
|
|
1516
|
+
argsHash: row.args_hash,
|
|
1517
|
+
decision: row.decision,
|
|
1518
|
+
ruleId: row.rule_id,
|
|
1519
|
+
sessionId: row.session_id,
|
|
1520
|
+
prevHash: row.prev_hash,
|
|
1521
|
+
signature: row.signature,
|
|
1522
|
+
})),
|
|
1523
|
+
total: Number(count?.total ?? 0),
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ---- Hook Lifecycle ----
|
|
1528
|
+
|
|
1529
|
+
getHookReceipt(projectRoot: string): HookReceiptRecord | null {
|
|
1530
|
+
const row = this.db.prepare(`
|
|
1531
|
+
SELECT project_root, last_mcp_call, bypass_count, updated_at
|
|
1532
|
+
FROM devflow_hook_receipts WHERE project_root = ?
|
|
1533
|
+
`).get(projectRoot) as any;
|
|
1534
|
+
if (!row) return null;
|
|
1535
|
+
return {
|
|
1536
|
+
projectRoot: row.project_root,
|
|
1537
|
+
lastMcpCall: row.last_mcp_call ?? undefined,
|
|
1538
|
+
bypassCount: Number(row.bypass_count ?? 0),
|
|
1539
|
+
updatedAt: Number(row.updated_at),
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
updateHookReceipt(
|
|
1544
|
+
projectRoot: string,
|
|
1545
|
+
updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>,
|
|
1546
|
+
): HookReceiptRecord {
|
|
1547
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1548
|
+
try {
|
|
1549
|
+
const current = this.getHookReceipt(projectRoot);
|
|
1550
|
+
const next = updater(current);
|
|
1551
|
+
const updatedAt = Date.now();
|
|
1552
|
+
this.db.prepare(`
|
|
1553
|
+
INSERT INTO devflow_hook_receipts
|
|
1554
|
+
(project_root, last_mcp_call, bypass_count, updated_at)
|
|
1555
|
+
VALUES (?, ?, ?, ?)
|
|
1556
|
+
ON CONFLICT(project_root) DO UPDATE SET
|
|
1557
|
+
last_mcp_call = excluded.last_mcp_call,
|
|
1558
|
+
bypass_count = excluded.bypass_count,
|
|
1559
|
+
updated_at = excluded.updated_at
|
|
1560
|
+
`).run(
|
|
1561
|
+
projectRoot,
|
|
1562
|
+
next.lastMcpCall ?? null,
|
|
1563
|
+
next.bypassCount ?? 0,
|
|
1564
|
+
updatedAt,
|
|
1565
|
+
);
|
|
1566
|
+
this.db.exec('COMMIT');
|
|
1567
|
+
return {
|
|
1568
|
+
projectRoot,
|
|
1569
|
+
lastMcpCall: next.lastMcpCall,
|
|
1570
|
+
bypassCount: next.bypassCount ?? 0,
|
|
1571
|
+
updatedAt,
|
|
1572
|
+
};
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
try { this.db.exec('ROLLBACK'); } catch {}
|
|
1575
|
+
throw error;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
deleteHookReceipt(projectRoot: string): boolean {
|
|
1580
|
+
return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
|
|
1581
|
+
.run(projectRoot).changes > 0;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
|
|
1585
|
+
this.db.prepare(`
|
|
1586
|
+
INSERT INTO devflow_memory_distill_checkpoints
|
|
1587
|
+
(id, project_root, session_id, trigger, pending_events, released_leases, created_at)
|
|
1588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1589
|
+
`).run(
|
|
1590
|
+
checkpoint.id,
|
|
1591
|
+
checkpoint.projectRoot,
|
|
1592
|
+
checkpoint.sessionId ?? null,
|
|
1593
|
+
checkpoint.trigger,
|
|
1594
|
+
checkpoint.pendingEvents,
|
|
1595
|
+
checkpoint.releasedLeases,
|
|
1596
|
+
checkpoint.createdAt,
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
listMemoryDistillCheckpoints(projectRoot: string, limit = 50): MemoryDistillCheckpointRecord[] {
|
|
1601
|
+
const rows = this.db.prepare(`
|
|
1602
|
+
SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
|
|
1603
|
+
FROM devflow_memory_distill_checkpoints
|
|
1604
|
+
WHERE project_root = ?
|
|
1605
|
+
ORDER BY created_at DESC
|
|
1606
|
+
LIMIT ?
|
|
1607
|
+
`).all(projectRoot, Math.max(1, Math.min(limit, 500))) as any[];
|
|
1608
|
+
return rows.map(row => ({
|
|
1609
|
+
id: row.id,
|
|
1610
|
+
projectRoot: row.project_root,
|
|
1611
|
+
sessionId: row.session_id ?? undefined,
|
|
1612
|
+
trigger: row.trigger,
|
|
1613
|
+
pendingEvents: Number(row.pending_events),
|
|
1614
|
+
releasedLeases: Number(row.released_leases),
|
|
1615
|
+
createdAt: Number(row.created_at),
|
|
1616
|
+
}));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
995
1619
|
// Convenience methods for raw SQL queries (used by AutoChecker)
|
|
996
1620
|
all(sql: string, ...params: unknown[]): unknown[] {
|
|
997
1621
|
return this.db.prepare(sql).all(...params);
|
|
@@ -1005,3 +1629,42 @@ export class DevFlowDatabase {
|
|
|
1005
1629
|
this.db.close();
|
|
1006
1630
|
}
|
|
1007
1631
|
}
|
|
1632
|
+
|
|
1633
|
+
function parseJson(value: unknown): unknown {
|
|
1634
|
+
if (typeof value !== 'string') return value;
|
|
1635
|
+
try { return JSON.parse(value); } catch { return value; }
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function deriveQuery(input: unknown): string | null {
|
|
1639
|
+
if (!input || typeof input !== 'object') return null;
|
|
1640
|
+
const record = input as Record<string, unknown>;
|
|
1641
|
+
for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
|
|
1642
|
+
if (typeof record[key] === 'string' && record[key]) return record[key] as string;
|
|
1643
|
+
}
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function deriveEngine(toolName: string): string | null {
|
|
1648
|
+
if (/project_context|symbol|dependency_graph|codegraph/.test(toolName)) return 'codegraph';
|
|
1649
|
+
if (/knowledge|docs/.test(toolName)) return 'knowledge';
|
|
1650
|
+
if (/memory/.test(toolName)) return 'memory';
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function countTelemetryResults(value: unknown, depth = 0): number {
|
|
1655
|
+
if (value == null || depth > 2) return 0;
|
|
1656
|
+
if (Array.isArray(value)) return value.length;
|
|
1657
|
+
if (typeof value !== 'object') return value === '' ? 0 : 1;
|
|
1658
|
+
|
|
1659
|
+
const record = value as Record<string, unknown>;
|
|
1660
|
+
const unwrapped = record.data ?? record.structuredContent;
|
|
1661
|
+
if (unwrapped !== undefined && unwrapped !== value) return countTelemetryResults(unwrapped, depth + 1);
|
|
1662
|
+
|
|
1663
|
+
let count = 0;
|
|
1664
|
+
for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
|
|
1665
|
+
if (Array.isArray(record[key])) count += record[key].length;
|
|
1666
|
+
}
|
|
1667
|
+
if (count > 0) return count;
|
|
1668
|
+
if (record.error) return 0;
|
|
1669
|
+
return Object.keys(record).length > 0 ? 1 : 0;
|
|
1670
|
+
}
|