@devflow-tools/database 0.15.0 → 0.16.2
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 +32 -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 +143 -2
- package/dist/database.js +658 -26
- 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 +900 -57
- 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,121 @@
|
|
|
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 ContextReceiptRecord {
|
|
82
|
+
projectRoot: string;
|
|
83
|
+
sessionId: string;
|
|
84
|
+
executionId: string;
|
|
85
|
+
contextHash: string;
|
|
86
|
+
issuedAt: number;
|
|
87
|
+
expiresAt: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MemoryDistillCheckpointRecord {
|
|
91
|
+
id: string;
|
|
92
|
+
projectRoot: string;
|
|
93
|
+
sessionId?: string;
|
|
94
|
+
trigger: 'pre_compact' | 'session_end';
|
|
95
|
+
pendingEvents: number;
|
|
96
|
+
releasedLeases: number;
|
|
97
|
+
createdAt: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
101
|
+
const stateDir = process.env.DEVFLOW_STATE_DIR ?? join(home, '.devflow', 'global');
|
|
102
|
+
return join(stateDir, 'devflow.db');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function openGlobalDevFlowDatabase(
|
|
106
|
+
home = homedir(),
|
|
107
|
+
options?: { busyTimeoutMs?: number },
|
|
108
|
+
): DevFlowDatabase {
|
|
109
|
+
return new DevFlowDatabase(home, {
|
|
110
|
+
dbPath: getGlobalDevFlowDbPath(home),
|
|
111
|
+
busyTimeoutMs: options?.busyTimeoutMs,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
4
114
|
|
|
5
115
|
export class DevFlowDatabase {
|
|
6
116
|
private db: NodeSqliteDatabase;
|
|
7
117
|
|
|
8
|
-
constructor(projectRoot: string, opts?: { dbPath?: string }) {
|
|
118
|
+
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
|
|
9
119
|
let dbPath: string;
|
|
10
120
|
if (opts?.dbPath) {
|
|
11
121
|
const dir = dirname(opts.dbPath);
|
|
@@ -16,10 +126,10 @@ export class DevFlowDatabase {
|
|
|
16
126
|
if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
|
|
17
127
|
dbPath = join(devflowDir, 'devflow.db');
|
|
18
128
|
}
|
|
19
|
-
this.db = new NodeSqliteDatabase(dbPath);
|
|
129
|
+
this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
|
|
20
130
|
|
|
21
131
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
22
|
-
this.db.exec(
|
|
132
|
+
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
23
133
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
24
134
|
|
|
25
135
|
this.initializeSchema();
|
|
@@ -123,7 +233,9 @@ export class DevFlowDatabase {
|
|
|
123
233
|
subagent_id TEXT,
|
|
124
234
|
error TEXT,
|
|
125
235
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
126
|
-
block_reason TEXT
|
|
236
|
+
block_reason TEXT,
|
|
237
|
+
failure_category TEXT,
|
|
238
|
+
tool_use_id TEXT
|
|
127
239
|
);
|
|
128
240
|
|
|
129
241
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -146,11 +258,99 @@ export class DevFlowDatabase {
|
|
|
146
258
|
mcp_tool_calls INTEGER DEFAULT 0,
|
|
147
259
|
direct_tool_calls INTEGER DEFAULT 0,
|
|
148
260
|
subagent_count INTEGER DEFAULT 0,
|
|
149
|
-
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
|
|
261
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
262
|
+
metadata TEXT
|
|
150
263
|
);
|
|
151
264
|
|
|
152
265
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
153
266
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
267
|
+
|
|
268
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
269
|
+
id TEXT PRIMARY KEY,
|
|
270
|
+
project_root TEXT NOT NULL,
|
|
271
|
+
query TEXT NOT NULL,
|
|
272
|
+
rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
|
|
273
|
+
task_type TEXT,
|
|
274
|
+
context_mode TEXT,
|
|
275
|
+
run_id TEXT,
|
|
276
|
+
token_count INTEGER DEFAULT 0,
|
|
277
|
+
created_at INTEGER NOT NULL
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
|
|
282
|
+
|
|
283
|
+
CREATE TABLE IF NOT EXISTS telemetry_failures (
|
|
284
|
+
id TEXT PRIMARY KEY,
|
|
285
|
+
operation TEXT NOT NULL,
|
|
286
|
+
payload TEXT,
|
|
287
|
+
error TEXT NOT NULL,
|
|
288
|
+
created_at INTEGER NOT NULL,
|
|
289
|
+
resolved_at INTEGER
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
|
|
293
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
|
|
294
|
+
|
|
295
|
+
CREATE TABLE IF NOT EXISTS devflow_rules (
|
|
296
|
+
id TEXT PRIMARY KEY,
|
|
297
|
+
name TEXT NOT NULL,
|
|
298
|
+
level TEXT NOT NULL DEFAULT 'project',
|
|
299
|
+
gate INTEGER NOT NULL DEFAULT 1,
|
|
300
|
+
condition_json TEXT NOT NULL,
|
|
301
|
+
action TEXT NOT NULL DEFAULT 'warn',
|
|
302
|
+
message TEXT NOT NULL,
|
|
303
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
304
|
+
created_at INTEGER NOT NULL,
|
|
305
|
+
updated_at INTEGER NOT NULL
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
CREATE TABLE IF NOT EXISTS devflow_audit_chain (
|
|
309
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
310
|
+
ts INTEGER NOT NULL,
|
|
311
|
+
tool TEXT NOT NULL,
|
|
312
|
+
args_hash TEXT NOT NULL,
|
|
313
|
+
decision TEXT NOT NULL,
|
|
314
|
+
rule_id TEXT,
|
|
315
|
+
session_id TEXT NOT NULL,
|
|
316
|
+
prev_hash TEXT NOT NULL,
|
|
317
|
+
signature TEXT NOT NULL
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
|
|
321
|
+
|
|
322
|
+
CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
|
|
323
|
+
project_root TEXT PRIMARY KEY,
|
|
324
|
+
last_mcp_call INTEGER,
|
|
325
|
+
bypass_count INTEGER NOT NULL DEFAULT 0,
|
|
326
|
+
updated_at INTEGER NOT NULL
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
CREATE TABLE IF NOT EXISTS devflow_context_receipts (
|
|
330
|
+
project_root TEXT NOT NULL,
|
|
331
|
+
session_id TEXT NOT NULL,
|
|
332
|
+
execution_id TEXT NOT NULL,
|
|
333
|
+
context_hash TEXT NOT NULL,
|
|
334
|
+
issued_at INTEGER NOT NULL,
|
|
335
|
+
expires_at INTEGER NOT NULL,
|
|
336
|
+
PRIMARY KEY (project_root, session_id, execution_id)
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
340
|
+
ON devflow_context_receipts(expires_at);
|
|
341
|
+
|
|
342
|
+
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
343
|
+
id TEXT PRIMARY KEY,
|
|
344
|
+
project_root TEXT NOT NULL,
|
|
345
|
+
session_id TEXT,
|
|
346
|
+
trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
|
|
347
|
+
pending_events INTEGER NOT NULL DEFAULT 0,
|
|
348
|
+
released_leases INTEGER NOT NULL DEFAULT 0,
|
|
349
|
+
created_at INTEGER NOT NULL
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
|
|
353
|
+
ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
|
|
154
354
|
`);
|
|
155
355
|
|
|
156
356
|
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
@@ -163,6 +363,18 @@ export class DevFlowDatabase {
|
|
|
163
363
|
try { this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'"); } catch {}
|
|
164
364
|
// Migration: workflow run persistence
|
|
165
365
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT'); } catch {}
|
|
366
|
+
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT'); } catch {}
|
|
367
|
+
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT'); } catch {}
|
|
368
|
+
try { this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT'); } catch {}
|
|
369
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT'); } catch {}
|
|
370
|
+
try {
|
|
371
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
372
|
+
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
373
|
+
} catch {}
|
|
374
|
+
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
|
|
375
|
+
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER'); } catch {}
|
|
376
|
+
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
377
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
166
378
|
|
|
167
379
|
// Migration: tool_metrics table for per-tool metrics collection
|
|
168
380
|
try {
|
|
@@ -211,6 +423,28 @@ export class DevFlowDatabase {
|
|
|
211
423
|
`);
|
|
212
424
|
} catch {}
|
|
213
425
|
|
|
426
|
+
// Canonical benchmark reports live in the global DevFlow database.
|
|
427
|
+
try {
|
|
428
|
+
this.db.exec(`
|
|
429
|
+
CREATE TABLE IF NOT EXISTS benchmark_reports (
|
|
430
|
+
run_id TEXT PRIMARY KEY,
|
|
431
|
+
suite_id TEXT NOT NULL,
|
|
432
|
+
suite_version TEXT NOT NULL,
|
|
433
|
+
project_root TEXT NOT NULL,
|
|
434
|
+
commit_sha TEXT,
|
|
435
|
+
task_count INTEGER NOT NULL,
|
|
436
|
+
status TEXT NOT NULL CHECK(status IN ('completed')),
|
|
437
|
+
report_json TEXT NOT NULL,
|
|
438
|
+
created_at INTEGER NOT NULL
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
|
|
442
|
+
ON benchmark_reports(created_at DESC);
|
|
443
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
|
|
444
|
+
ON benchmark_reports(suite_id, suite_version, created_at DESC);
|
|
445
|
+
`);
|
|
446
|
+
} catch {}
|
|
447
|
+
|
|
214
448
|
// Migration: logging system views for agent queries
|
|
215
449
|
try {
|
|
216
450
|
this.db.exec(`
|
|
@@ -259,7 +493,7 @@ export class DevFlowDatabase {
|
|
|
259
493
|
|
|
260
494
|
insertRun(run: any): void {
|
|
261
495
|
this.db.prepare(`
|
|
262
|
-
INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
496
|
+
INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
263
497
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
264
498
|
`).run(
|
|
265
499
|
run.id,
|
|
@@ -428,6 +662,27 @@ export class DevFlowDatabase {
|
|
|
428
662
|
`).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
|
|
429
663
|
}
|
|
430
664
|
|
|
665
|
+
ensureSession(params: {
|
|
666
|
+
id: string;
|
|
667
|
+
projectRoot: string;
|
|
668
|
+
label?: string;
|
|
669
|
+
startedAt?: number;
|
|
670
|
+
}): void {
|
|
671
|
+
if (!params.id.trim()) throw new Error('Canonical session ID is required');
|
|
672
|
+
const startedAt = params.startedAt ?? Date.now();
|
|
673
|
+
this.db.prepare(`
|
|
674
|
+
INSERT INTO sessions (id, project_root, label, started_at, status)
|
|
675
|
+
VALUES (?, ?, ?, ?, 'running')
|
|
676
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
677
|
+
project_root = CASE
|
|
678
|
+
WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
|
|
679
|
+
ELSE sessions.project_root
|
|
680
|
+
END,
|
|
681
|
+
label = COALESCE(sessions.label, excluded.label),
|
|
682
|
+
started_at = MIN(sessions.started_at, excluded.started_at)
|
|
683
|
+
`).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
|
|
684
|
+
}
|
|
685
|
+
|
|
431
686
|
getSession(id: string): any | null {
|
|
432
687
|
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as any;
|
|
433
688
|
if (!row) return null;
|
|
@@ -443,6 +698,7 @@ export class DevFlowDatabase {
|
|
|
443
698
|
directToolCalls: row.direct_tool_calls,
|
|
444
699
|
subagentCount: row.subagent_count,
|
|
445
700
|
status: row.status,
|
|
701
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
446
702
|
};
|
|
447
703
|
}
|
|
448
704
|
|
|
@@ -469,6 +725,7 @@ export class DevFlowDatabase {
|
|
|
469
725
|
directToolCalls: row.direct_tool_calls,
|
|
470
726
|
subagentCount: row.subagent_count,
|
|
471
727
|
status: row.status,
|
|
728
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
472
729
|
}));
|
|
473
730
|
}
|
|
474
731
|
|
|
@@ -535,6 +792,8 @@ export class DevFlowDatabase {
|
|
|
535
792
|
error: row.error,
|
|
536
793
|
blocked: row.blocked === 1,
|
|
537
794
|
blockReason: row.block_reason,
|
|
795
|
+
failureCategory: row.failure_category,
|
|
796
|
+
toolUseId: row.tool_use_id,
|
|
538
797
|
tokensUsed: row.tokens_used ?? 0,
|
|
539
798
|
}));
|
|
540
799
|
}
|
|
@@ -570,6 +829,8 @@ export class DevFlowDatabase {
|
|
|
570
829
|
error: row.error,
|
|
571
830
|
blocked: row.blocked === 1,
|
|
572
831
|
blockReason: row.block_reason,
|
|
832
|
+
failureCategory: row.failure_category,
|
|
833
|
+
toolUseId: row.tool_use_id,
|
|
573
834
|
tokensUsed: row.tokens_used ?? 0,
|
|
574
835
|
});
|
|
575
836
|
}
|
|
@@ -588,7 +849,7 @@ export class DevFlowDatabase {
|
|
|
588
849
|
availableMcpTools?: string[];
|
|
589
850
|
}): void {
|
|
590
851
|
this.db.prepare(`
|
|
591
|
-
INSERT INTO skill_executions
|
|
852
|
+
INSERT OR IGNORE INTO skill_executions
|
|
592
853
|
(execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
|
|
593
854
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
594
855
|
`).run(
|
|
@@ -622,6 +883,7 @@ export class DevFlowDatabase {
|
|
|
622
883
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
623
884
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
624
885
|
createdAt: row.created_at,
|
|
886
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
625
887
|
};
|
|
626
888
|
}
|
|
627
889
|
|
|
@@ -635,6 +897,7 @@ export class DevFlowDatabase {
|
|
|
635
897
|
mcpToolCalls?: number;
|
|
636
898
|
directToolCalls?: number;
|
|
637
899
|
subagentCount?: number;
|
|
900
|
+
metadata?: Record<string, unknown>;
|
|
638
901
|
}): void {
|
|
639
902
|
const sets: string[] = [];
|
|
640
903
|
const values: any[] = [];
|
|
@@ -648,6 +911,7 @@ export class DevFlowDatabase {
|
|
|
648
911
|
if (updates.mcpToolCalls !== undefined) { sets.push('mcp_tool_calls = ?'); values.push(updates.mcpToolCalls); }
|
|
649
912
|
if (updates.directToolCalls !== undefined) { sets.push('direct_tool_calls = ?'); values.push(updates.directToolCalls); }
|
|
650
913
|
if (updates.subagentCount !== undefined) { sets.push('subagent_count = ?'); values.push(updates.subagentCount); }
|
|
914
|
+
if (updates.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(updates.metadata)); }
|
|
651
915
|
|
|
652
916
|
if (sets.length > 0) {
|
|
653
917
|
values.push(executionId);
|
|
@@ -681,6 +945,7 @@ export class DevFlowDatabase {
|
|
|
681
945
|
subagentCount: row.subagent_count,
|
|
682
946
|
totalDuration: row.total_duration,
|
|
683
947
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
948
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
684
949
|
}));
|
|
685
950
|
}
|
|
686
951
|
|
|
@@ -707,50 +972,84 @@ export class DevFlowDatabase {
|
|
|
707
972
|
blocked: boolean;
|
|
708
973
|
blockReason?: string;
|
|
709
974
|
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
|
-
|
|
975
|
+
failureCategory?: string;
|
|
976
|
+
tokensUsed?: number;
|
|
977
|
+
toolUseId?: string;
|
|
978
|
+
}): boolean {
|
|
979
|
+
return this.db.transaction(() => {
|
|
980
|
+
const result = this.db.prepare(`
|
|
981
|
+
INSERT OR IGNORE INTO tool_call_events
|
|
982
|
+
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
983
|
+
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
|
|
984
|
+
parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
|
|
985
|
+
failure_category, tool_use_id)
|
|
986
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
987
|
+
`).run(
|
|
988
|
+
params.eventId,
|
|
989
|
+
params.executionId || '',
|
|
990
|
+
params.sessionId ?? null,
|
|
991
|
+
params.timestamp,
|
|
992
|
+
params.toolName,
|
|
993
|
+
params.toolType,
|
|
994
|
+
params.isMcpTool ? 1 : 0,
|
|
995
|
+
params.mcpToolName || null,
|
|
996
|
+
params.mcpEnforced ? 1 : 0,
|
|
997
|
+
params.mcpFallback ? 1 : 0,
|
|
998
|
+
params.kind ?? 'tool_use',
|
|
999
|
+
JSON.stringify(params.input) ?? null,
|
|
1000
|
+
params.output !== undefined ? JSON.stringify(params.output) : null,
|
|
1001
|
+
params.tokensUsed ?? 0,
|
|
1002
|
+
params.duration,
|
|
1003
|
+
params.parentToolCallId || null,
|
|
1004
|
+
params.subagentId || null,
|
|
1005
|
+
params.error || null,
|
|
1006
|
+
params.blocked ? 1 : 0,
|
|
1007
|
+
params.blockReason || null,
|
|
1008
|
+
params.workflowRunId || null,
|
|
1009
|
+
params.failureCategory || null,
|
|
1010
|
+
params.toolUseId || null,
|
|
1011
|
+
);
|
|
739
1012
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1013
|
+
if (result.changes === 0) return false;
|
|
1014
|
+
|
|
1015
|
+
this.db.prepare(`
|
|
1016
|
+
UPDATE skill_executions
|
|
1017
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
1018
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
1019
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
1020
|
+
subagent_count = subagent_count + ?,
|
|
1021
|
+
total_duration = MAX(total_duration, ? - started_at),
|
|
1022
|
+
mcp_compliance_rate = ROUND(
|
|
1023
|
+
100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
|
|
1024
|
+
)
|
|
1025
|
+
WHERE execution_id = ?
|
|
1026
|
+
`).run(
|
|
1027
|
+
params.isMcpTool ? 1 : 0,
|
|
1028
|
+
params.isMcpTool ? 0 : 1,
|
|
1029
|
+
params.toolType === 'subagent' ? 1 : 0,
|
|
1030
|
+
params.timestamp,
|
|
1031
|
+
params.isMcpTool ? 1 : 0,
|
|
1032
|
+
params.executionId,
|
|
1033
|
+
);
|
|
1034
|
+
if (params.sessionId) {
|
|
1035
|
+
this.db.prepare(`
|
|
1036
|
+
UPDATE sessions
|
|
1037
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
1038
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
1039
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
1040
|
+
subagent_count = subagent_count + ?,
|
|
1041
|
+
duration_ms = MAX(duration_ms, ? - started_at)
|
|
1042
|
+
WHERE id = ?
|
|
1043
|
+
`).run(
|
|
1044
|
+
params.isMcpTool ? 1 : 0,
|
|
1045
|
+
params.isMcpTool ? 0 : 1,
|
|
1046
|
+
params.toolType === 'subagent' ? 1 : 0,
|
|
1047
|
+
params.timestamp,
|
|
1048
|
+
params.sessionId,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return true;
|
|
1052
|
+
});
|
|
754
1053
|
}
|
|
755
1054
|
|
|
756
1055
|
listToolCallEvents(executionId: string): any[] {
|
|
@@ -778,10 +1077,12 @@ export class DevFlowDatabase {
|
|
|
778
1077
|
error: row.error,
|
|
779
1078
|
blocked: row.blocked === 1,
|
|
780
1079
|
blockReason: row.block_reason,
|
|
1080
|
+
failureCategory: row.failure_category,
|
|
1081
|
+
toolUseId: row.tool_use_id,
|
|
781
1082
|
}));
|
|
782
1083
|
}
|
|
783
1084
|
|
|
784
|
-
updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }):
|
|
1085
|
+
updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }): boolean {
|
|
785
1086
|
const sets: string[] = [];
|
|
786
1087
|
const values: any[] = [];
|
|
787
1088
|
|
|
@@ -800,10 +1101,50 @@ export class DevFlowDatabase {
|
|
|
800
1101
|
|
|
801
1102
|
if (sets.length > 0) {
|
|
802
1103
|
values.push(eventId);
|
|
803
|
-
this.db.prepare(
|
|
1104
|
+
const result = this.db.prepare(
|
|
804
1105
|
`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`
|
|
805
1106
|
).run(...values);
|
|
1107
|
+
return result.changes > 0;
|
|
806
1108
|
}
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
markToolCallBlocked(eventId: string, reason: string): boolean {
|
|
1113
|
+
return this.db.prepare(`
|
|
1114
|
+
UPDATE tool_call_events
|
|
1115
|
+
SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
|
|
1116
|
+
WHERE event_id = ?
|
|
1117
|
+
`).run(reason, reason, eventId).changes > 0;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
reconcileSkillExecution(
|
|
1121
|
+
executionId: string,
|
|
1122
|
+
status: 'completed' | 'failed',
|
|
1123
|
+
finishedAt = Date.now(),
|
|
1124
|
+
metadata?: Record<string, unknown>,
|
|
1125
|
+
): void {
|
|
1126
|
+
const row = this.db.prepare(`
|
|
1127
|
+
SELECT COUNT(*) AS total,
|
|
1128
|
+
SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
|
|
1129
|
+
SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
|
|
1130
|
+
SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
|
|
1131
|
+
MIN(timestamp) AS first_at,
|
|
1132
|
+
MAX(timestamp) AS last_at
|
|
1133
|
+
FROM tool_call_events WHERE execution_id = ?
|
|
1134
|
+
`).get(executionId) as any;
|
|
1135
|
+
const total = Number(row?.total ?? 0);
|
|
1136
|
+
const mcp = Number(row?.mcp ?? 0);
|
|
1137
|
+
this.updateSkillExecution(executionId, {
|
|
1138
|
+
status,
|
|
1139
|
+
finishedAt,
|
|
1140
|
+
totalToolCalls: total,
|
|
1141
|
+
mcpToolCalls: mcp,
|
|
1142
|
+
directToolCalls: Number(row?.direct ?? 0),
|
|
1143
|
+
subagentCount: Number(row?.subagents ?? 0),
|
|
1144
|
+
totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
1145
|
+
mcpComplianceRate: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
|
|
1146
|
+
metadata,
|
|
1147
|
+
});
|
|
807
1148
|
}
|
|
808
1149
|
|
|
809
1150
|
// ---- MCP Compliance ----
|
|
@@ -860,11 +1201,13 @@ export class DevFlowDatabase {
|
|
|
860
1201
|
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
861
1202
|
try {
|
|
862
1203
|
this.db.prepare(
|
|
863
|
-
`UPDATE skill_executions SET status = 'failed', finished_at =
|
|
864
|
-
|
|
1204
|
+
`UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1205
|
+
WHERE status = 'running' AND started_at < ?`
|
|
1206
|
+
).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
865
1207
|
this.db.prepare(
|
|
866
|
-
`UPDATE sessions SET status = 'failed', finished_at =
|
|
867
|
-
|
|
1208
|
+
`UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1209
|
+
WHERE status = 'running' AND started_at < ?`
|
|
1210
|
+
).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
868
1211
|
} catch { /* best-effort */ }
|
|
869
1212
|
}
|
|
870
1213
|
|
|
@@ -884,7 +1227,7 @@ export class DevFlowDatabase {
|
|
|
884
1227
|
createdAt: number;
|
|
885
1228
|
}): void {
|
|
886
1229
|
this.db.prepare(`
|
|
887
|
-
INSERT OR
|
|
1230
|
+
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
1231
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
889
1232
|
`).run(
|
|
890
1233
|
metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName,
|
|
@@ -928,6 +1271,153 @@ export class DevFlowDatabase {
|
|
|
928
1271
|
}));
|
|
929
1272
|
}
|
|
930
1273
|
|
|
1274
|
+
aggregatePendingToolMetrics(limit = 1000): number {
|
|
1275
|
+
const rows = this.db.prepare(`
|
|
1276
|
+
SELECT e.*
|
|
1277
|
+
FROM tool_call_events e
|
|
1278
|
+
LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
|
|
1279
|
+
WHERE m.id IS NULL AND e.output IS NOT NULL
|
|
1280
|
+
ORDER BY e.timestamp ASC
|
|
1281
|
+
LIMIT ?
|
|
1282
|
+
`).all(limit) as any[];
|
|
1283
|
+
|
|
1284
|
+
let inserted = 0;
|
|
1285
|
+
this.db.transaction(() => {
|
|
1286
|
+
for (const row of rows) {
|
|
1287
|
+
const input = parseJson(row.input);
|
|
1288
|
+
const output = parseJson(row.output);
|
|
1289
|
+
const toolName = row.mcp_tool_name || row.tool_name;
|
|
1290
|
+
const resultCount = countTelemetryResults(output);
|
|
1291
|
+
const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
|
|
1292
|
+
const status = row.error || row.blocked
|
|
1293
|
+
? 'error'
|
|
1294
|
+
: resultCount === 0 ? 'empty' : 'success';
|
|
1295
|
+
const result = this.db.prepare(`
|
|
1296
|
+
INSERT OR IGNORE INTO tool_metrics
|
|
1297
|
+
(id, tool_call_event_id, session_id, tool_name, query, status, result_count,
|
|
1298
|
+
result_size_bytes, latency_ms, engine, metadata, created_at)
|
|
1299
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1300
|
+
`).run(
|
|
1301
|
+
`metric:${row.event_id}`,
|
|
1302
|
+
row.event_id,
|
|
1303
|
+
row.session_id || '',
|
|
1304
|
+
toolName,
|
|
1305
|
+
deriveQuery(input),
|
|
1306
|
+
status,
|
|
1307
|
+
resultCount,
|
|
1308
|
+
resultSizeBytes,
|
|
1309
|
+
row.duration ?? 0,
|
|
1310
|
+
deriveEngine(toolName),
|
|
1311
|
+
JSON.stringify({ executionId: row.execution_id, kind: row.kind }),
|
|
1312
|
+
row.timestamp,
|
|
1313
|
+
);
|
|
1314
|
+
inserted += result.changes;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
return inserted;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ---- Context Feedback ----
|
|
1321
|
+
|
|
1322
|
+
insertFeedback(feedback: FeedbackRecord): void {
|
|
1323
|
+
this.db.prepare(`
|
|
1324
|
+
INSERT OR IGNORE INTO feedback
|
|
1325
|
+
(id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
|
|
1326
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1327
|
+
`).run(
|
|
1328
|
+
feedback.id,
|
|
1329
|
+
feedback.projectRoot,
|
|
1330
|
+
feedback.query,
|
|
1331
|
+
feedback.rating,
|
|
1332
|
+
feedback.taskType ?? null,
|
|
1333
|
+
feedback.contextMode ?? null,
|
|
1334
|
+
feedback.runId ?? null,
|
|
1335
|
+
feedback.tokenCount ?? 0,
|
|
1336
|
+
feedback.createdAt,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
listFeedback(projectRoot?: string, limit = 100): FeedbackRecord[] {
|
|
1341
|
+
const rows = projectRoot
|
|
1342
|
+
? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
|
|
1343
|
+
: this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
1344
|
+
return (rows as any[]).map(row => ({
|
|
1345
|
+
id: row.id,
|
|
1346
|
+
projectRoot: row.project_root,
|
|
1347
|
+
query: row.query,
|
|
1348
|
+
rating: row.rating,
|
|
1349
|
+
taskType: row.task_type ?? undefined,
|
|
1350
|
+
contextMode: row.context_mode ?? undefined,
|
|
1351
|
+
runId: row.run_id ?? undefined,
|
|
1352
|
+
tokenCount: row.token_count ?? 0,
|
|
1353
|
+
createdAt: row.created_at,
|
|
1354
|
+
}));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
getFeedbackSummary(projectRoot?: string): { total: number; hits: number; partials: number; misses: number; hitRate: number } {
|
|
1358
|
+
const row = (projectRoot
|
|
1359
|
+
? 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)
|
|
1360
|
+
: this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get()) as any;
|
|
1361
|
+
const total = Number(row?.total ?? 0);
|
|
1362
|
+
const hits = Number(row?.hits ?? 0);
|
|
1363
|
+
const partials = Number(row?.partials ?? 0);
|
|
1364
|
+
const misses = Number(row?.misses ?? 0);
|
|
1365
|
+
return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ---- Telemetry Failures ----
|
|
1369
|
+
|
|
1370
|
+
insertTelemetryFailure(failure: TelemetryFailureRecord): void {
|
|
1371
|
+
this.db.prepare(`
|
|
1372
|
+
INSERT OR IGNORE INTO telemetry_failures
|
|
1373
|
+
(id, operation, payload, error, created_at, resolved_at)
|
|
1374
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1375
|
+
`).run(
|
|
1376
|
+
failure.id,
|
|
1377
|
+
failure.operation,
|
|
1378
|
+
JSON.stringify(failure.payload) ?? null,
|
|
1379
|
+
failure.error,
|
|
1380
|
+
failure.createdAt,
|
|
1381
|
+
failure.resolvedAt ?? null,
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
resolveTelemetryFailure(id: string, resolvedAt = Date.now()): void {
|
|
1386
|
+
this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
listTelemetryFailures(options?: { unresolvedOnly?: boolean; limit?: number }): TelemetryFailureRecord[] {
|
|
1390
|
+
const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
|
|
1391
|
+
const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
1392
|
+
.all(options?.limit ?? 100) as any[];
|
|
1393
|
+
return rows.map(row => ({
|
|
1394
|
+
id: row.id,
|
|
1395
|
+
operation: row.operation,
|
|
1396
|
+
payload: parseJson(row.payload),
|
|
1397
|
+
error: row.error,
|
|
1398
|
+
createdAt: row.created_at,
|
|
1399
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
1400
|
+
}));
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
countTelemetryFailures(unresolvedOnly = false): number {
|
|
1404
|
+
const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get() as any;
|
|
1405
|
+
return Number(row?.count ?? 0);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
trimTelemetryFailures(maxRows = 500): number {
|
|
1409
|
+
const limit = Math.max(1, Math.floor(maxRows));
|
|
1410
|
+
const result = this.db.prepare(`
|
|
1411
|
+
DELETE FROM telemetry_failures
|
|
1412
|
+
WHERE id IN (
|
|
1413
|
+
SELECT id FROM telemetry_failures
|
|
1414
|
+
ORDER BY created_at DESC, id DESC
|
|
1415
|
+
LIMIT -1 OFFSET ?
|
|
1416
|
+
)
|
|
1417
|
+
`).run(limit);
|
|
1418
|
+
return Number(result.changes);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
931
1421
|
// ---- Accuracy Queries ----
|
|
932
1422
|
|
|
933
1423
|
insertAccuracyQuery(q: {
|
|
@@ -992,6 +1482,320 @@ export class DevFlowDatabase {
|
|
|
992
1482
|
return statsByEngine;
|
|
993
1483
|
}
|
|
994
1484
|
|
|
1485
|
+
// ---- Benchmark Reports ----
|
|
1486
|
+
|
|
1487
|
+
insertBenchmarkReport<T extends BenchmarkReportRecord>(report: T): void {
|
|
1488
|
+
this.db.prepare(`
|
|
1489
|
+
INSERT INTO benchmark_reports
|
|
1490
|
+
(run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
|
|
1491
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1492
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1493
|
+
suite_id = excluded.suite_id,
|
|
1494
|
+
suite_version = excluded.suite_version,
|
|
1495
|
+
project_root = excluded.project_root,
|
|
1496
|
+
commit_sha = excluded.commit_sha,
|
|
1497
|
+
task_count = excluded.task_count,
|
|
1498
|
+
status = excluded.status,
|
|
1499
|
+
report_json = excluded.report_json,
|
|
1500
|
+
created_at = excluded.created_at
|
|
1501
|
+
`).run(
|
|
1502
|
+
report.runId,
|
|
1503
|
+
report.suiteId,
|
|
1504
|
+
report.suiteVersion,
|
|
1505
|
+
report.projectRoot,
|
|
1506
|
+
report.commit,
|
|
1507
|
+
report.tasks.length,
|
|
1508
|
+
report.status,
|
|
1509
|
+
JSON.stringify(report),
|
|
1510
|
+
report.createdAt,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
getBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(runId: string): T | null {
|
|
1515
|
+
const row = this.db.prepare(
|
|
1516
|
+
'SELECT report_json FROM benchmark_reports WHERE run_id = ?',
|
|
1517
|
+
).get(runId) as { report_json?: string } | undefined;
|
|
1518
|
+
return row?.report_json
|
|
1519
|
+
? JSON.parse(row.report_json) as T
|
|
1520
|
+
: null;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
getLatestBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(suiteId?: string): T | null {
|
|
1524
|
+
const row = (suiteId
|
|
1525
|
+
? this.db.prepare(
|
|
1526
|
+
'SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
1527
|
+
).get(suiteId)
|
|
1528
|
+
: this.db.prepare(
|
|
1529
|
+
'SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1',
|
|
1530
|
+
).get()) as { report_json?: string } | undefined;
|
|
1531
|
+
return row?.report_json
|
|
1532
|
+
? JSON.parse(row.report_json) as T
|
|
1533
|
+
: null;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
listBenchmarkReports(limit = 50): BenchmarkReportMetaRecord[] {
|
|
1537
|
+
const rows = this.db.prepare(`
|
|
1538
|
+
SELECT run_id, suite_id, suite_version, project_root, commit_sha,
|
|
1539
|
+
task_count, status, created_at
|
|
1540
|
+
FROM benchmark_reports
|
|
1541
|
+
ORDER BY created_at DESC
|
|
1542
|
+
LIMIT ?
|
|
1543
|
+
`).all(Math.max(1, Math.min(limit, 500))) as Array<{
|
|
1544
|
+
run_id: string;
|
|
1545
|
+
suite_id: string;
|
|
1546
|
+
suite_version: string;
|
|
1547
|
+
project_root: string;
|
|
1548
|
+
commit_sha: string | null;
|
|
1549
|
+
task_count: number;
|
|
1550
|
+
status: 'completed';
|
|
1551
|
+
created_at: number;
|
|
1552
|
+
}>;
|
|
1553
|
+
return rows.map((row) => ({
|
|
1554
|
+
runId: row.run_id,
|
|
1555
|
+
suiteId: row.suite_id,
|
|
1556
|
+
suiteVersion: row.suite_version,
|
|
1557
|
+
projectRoot: row.project_root,
|
|
1558
|
+
commit: row.commit_sha,
|
|
1559
|
+
taskCount: row.task_count,
|
|
1560
|
+
createdAt: row.created_at,
|
|
1561
|
+
status: row.status,
|
|
1562
|
+
}));
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ---- Enforcer Governance ----
|
|
1566
|
+
|
|
1567
|
+
listGovernanceRules(includeDisabled = true): GovernanceRuleRecord[] {
|
|
1568
|
+
const where = includeDisabled ? '' : 'WHERE enabled = 1';
|
|
1569
|
+
const rows = this.db.prepare(`
|
|
1570
|
+
SELECT id, name, gate, level, condition_json, action, message,
|
|
1571
|
+
enabled, created_at, updated_at
|
|
1572
|
+
FROM devflow_rules ${where}
|
|
1573
|
+
ORDER BY gate, id
|
|
1574
|
+
`).all() as any[];
|
|
1575
|
+
return rows.map(row => ({
|
|
1576
|
+
id: row.id,
|
|
1577
|
+
name: row.name,
|
|
1578
|
+
gate: row.gate,
|
|
1579
|
+
level: row.level,
|
|
1580
|
+
condition: parseJson(row.condition_json) as Record<string, unknown>,
|
|
1581
|
+
action: row.action,
|
|
1582
|
+
message: row.message,
|
|
1583
|
+
enabled: row.enabled === 1,
|
|
1584
|
+
createdAt: row.created_at,
|
|
1585
|
+
updatedAt: row.updated_at,
|
|
1586
|
+
}));
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
getGovernanceRule(id: string): GovernanceRuleRecord | null {
|
|
1590
|
+
return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
upsertGovernanceRule(rule: GovernanceRuleRecord): GovernanceRuleRecord {
|
|
1594
|
+
const now = Date.now();
|
|
1595
|
+
this.db.prepare(`
|
|
1596
|
+
INSERT INTO devflow_rules
|
|
1597
|
+
(id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
|
|
1598
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1599
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1600
|
+
name = excluded.name,
|
|
1601
|
+
gate = excluded.gate,
|
|
1602
|
+
level = excluded.level,
|
|
1603
|
+
condition_json = excluded.condition_json,
|
|
1604
|
+
action = excluded.action,
|
|
1605
|
+
message = excluded.message,
|
|
1606
|
+
enabled = excluded.enabled,
|
|
1607
|
+
updated_at = excluded.updated_at
|
|
1608
|
+
`).run(
|
|
1609
|
+
rule.id,
|
|
1610
|
+
rule.name,
|
|
1611
|
+
rule.gate,
|
|
1612
|
+
rule.level,
|
|
1613
|
+
JSON.stringify(rule.condition),
|
|
1614
|
+
rule.action,
|
|
1615
|
+
rule.message,
|
|
1616
|
+
rule.enabled ? 1 : 0,
|
|
1617
|
+
rule.createdAt ?? now,
|
|
1618
|
+
now,
|
|
1619
|
+
);
|
|
1620
|
+
return this.getGovernanceRule(rule.id)!;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
deleteGovernanceRule(id: string): boolean {
|
|
1624
|
+
return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
listGovernanceAudit(limit = 100, offset = 0): { items: GovernanceAuditRecord[]; total: number } {
|
|
1628
|
+
const boundedLimit = Math.max(1, Math.min(limit, 500));
|
|
1629
|
+
const boundedOffset = Math.max(0, offset);
|
|
1630
|
+
const rows = this.db.prepare(`
|
|
1631
|
+
SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
|
|
1632
|
+
FROM devflow_audit_chain
|
|
1633
|
+
ORDER BY seq DESC
|
|
1634
|
+
LIMIT ? OFFSET ?
|
|
1635
|
+
`).all(boundedLimit, boundedOffset) as any[];
|
|
1636
|
+
const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get() as { total?: number } | undefined;
|
|
1637
|
+
return {
|
|
1638
|
+
items: rows.map(row => ({
|
|
1639
|
+
seq: row.seq,
|
|
1640
|
+
ts: row.ts,
|
|
1641
|
+
tool: row.tool,
|
|
1642
|
+
argsHash: row.args_hash,
|
|
1643
|
+
decision: row.decision,
|
|
1644
|
+
ruleId: row.rule_id,
|
|
1645
|
+
sessionId: row.session_id,
|
|
1646
|
+
prevHash: row.prev_hash,
|
|
1647
|
+
signature: row.signature,
|
|
1648
|
+
})),
|
|
1649
|
+
total: Number(count?.total ?? 0),
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// ---- Hook Lifecycle ----
|
|
1654
|
+
|
|
1655
|
+
getHookReceipt(projectRoot: string): HookReceiptRecord | null {
|
|
1656
|
+
const row = this.db.prepare(`
|
|
1657
|
+
SELECT project_root, last_mcp_call, bypass_count, updated_at
|
|
1658
|
+
FROM devflow_hook_receipts WHERE project_root = ?
|
|
1659
|
+
`).get(projectRoot) as any;
|
|
1660
|
+
if (!row) return null;
|
|
1661
|
+
return {
|
|
1662
|
+
projectRoot: row.project_root,
|
|
1663
|
+
lastMcpCall: row.last_mcp_call ?? undefined,
|
|
1664
|
+
bypassCount: Number(row.bypass_count ?? 0),
|
|
1665
|
+
updatedAt: Number(row.updated_at),
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
updateHookReceipt(
|
|
1670
|
+
projectRoot: string,
|
|
1671
|
+
updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>,
|
|
1672
|
+
): HookReceiptRecord {
|
|
1673
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1674
|
+
try {
|
|
1675
|
+
const current = this.getHookReceipt(projectRoot);
|
|
1676
|
+
const next = updater(current);
|
|
1677
|
+
const updatedAt = Date.now();
|
|
1678
|
+
this.db.prepare(`
|
|
1679
|
+
INSERT INTO devflow_hook_receipts
|
|
1680
|
+
(project_root, last_mcp_call, bypass_count, updated_at)
|
|
1681
|
+
VALUES (?, ?, ?, ?)
|
|
1682
|
+
ON CONFLICT(project_root) DO UPDATE SET
|
|
1683
|
+
last_mcp_call = excluded.last_mcp_call,
|
|
1684
|
+
bypass_count = excluded.bypass_count,
|
|
1685
|
+
updated_at = excluded.updated_at
|
|
1686
|
+
`).run(
|
|
1687
|
+
projectRoot,
|
|
1688
|
+
next.lastMcpCall ?? null,
|
|
1689
|
+
next.bypassCount ?? 0,
|
|
1690
|
+
updatedAt,
|
|
1691
|
+
);
|
|
1692
|
+
this.db.exec('COMMIT');
|
|
1693
|
+
return {
|
|
1694
|
+
projectRoot,
|
|
1695
|
+
lastMcpCall: next.lastMcpCall,
|
|
1696
|
+
bypassCount: next.bypassCount ?? 0,
|
|
1697
|
+
updatedAt,
|
|
1698
|
+
};
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
try { this.db.exec('ROLLBACK'); } catch {}
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
deleteHookReceipt(projectRoot: string): boolean {
|
|
1706
|
+
return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
|
|
1707
|
+
.run(projectRoot).changes > 0;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
upsertContextReceipt(receipt: ContextReceiptRecord): void {
|
|
1711
|
+
this.db.prepare(`
|
|
1712
|
+
INSERT INTO devflow_context_receipts
|
|
1713
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at)
|
|
1714
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1715
|
+
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
1716
|
+
context_hash = excluded.context_hash,
|
|
1717
|
+
issued_at = excluded.issued_at,
|
|
1718
|
+
expires_at = excluded.expires_at
|
|
1719
|
+
`).run(
|
|
1720
|
+
receipt.projectRoot,
|
|
1721
|
+
receipt.sessionId,
|
|
1722
|
+
receipt.executionId,
|
|
1723
|
+
receipt.contextHash,
|
|
1724
|
+
receipt.issuedAt,
|
|
1725
|
+
receipt.expiresAt,
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
getContextReceipt(
|
|
1730
|
+
projectRoot: string,
|
|
1731
|
+
sessionId: string,
|
|
1732
|
+
executionId: string,
|
|
1733
|
+
): ContextReceiptRecord | null {
|
|
1734
|
+
const row = this.db.prepare(`
|
|
1735
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
1736
|
+
FROM devflow_context_receipts
|
|
1737
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
1738
|
+
`).get(projectRoot, sessionId, executionId) as any;
|
|
1739
|
+
return row ? {
|
|
1740
|
+
projectRoot: row.project_root,
|
|
1741
|
+
sessionId: row.session_id,
|
|
1742
|
+
executionId: row.execution_id,
|
|
1743
|
+
contextHash: row.context_hash,
|
|
1744
|
+
issuedAt: row.issued_at,
|
|
1745
|
+
expiresAt: row.expires_at,
|
|
1746
|
+
} : null;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number {
|
|
1750
|
+
return executionId
|
|
1751
|
+
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1752
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
|
|
1753
|
+
.run(projectRoot, sessionId, executionId).changes
|
|
1754
|
+
: this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1755
|
+
WHERE project_root = ? AND session_id = ?`)
|
|
1756
|
+
.run(projectRoot, sessionId).changes;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
purgeExpiredContextReceipts(now = Date.now()): number {
|
|
1760
|
+
return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
|
|
1761
|
+
.run(now).changes;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
|
|
1765
|
+
this.db.prepare(`
|
|
1766
|
+
INSERT INTO devflow_memory_distill_checkpoints
|
|
1767
|
+
(id, project_root, session_id, trigger, pending_events, released_leases, created_at)
|
|
1768
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1769
|
+
`).run(
|
|
1770
|
+
checkpoint.id,
|
|
1771
|
+
checkpoint.projectRoot,
|
|
1772
|
+
checkpoint.sessionId ?? null,
|
|
1773
|
+
checkpoint.trigger,
|
|
1774
|
+
checkpoint.pendingEvents,
|
|
1775
|
+
checkpoint.releasedLeases,
|
|
1776
|
+
checkpoint.createdAt,
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
listMemoryDistillCheckpoints(projectRoot: string, limit = 50): MemoryDistillCheckpointRecord[] {
|
|
1781
|
+
const rows = this.db.prepare(`
|
|
1782
|
+
SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
|
|
1783
|
+
FROM devflow_memory_distill_checkpoints
|
|
1784
|
+
WHERE project_root = ?
|
|
1785
|
+
ORDER BY created_at DESC
|
|
1786
|
+
LIMIT ?
|
|
1787
|
+
`).all(projectRoot, Math.max(1, Math.min(limit, 500))) as any[];
|
|
1788
|
+
return rows.map(row => ({
|
|
1789
|
+
id: row.id,
|
|
1790
|
+
projectRoot: row.project_root,
|
|
1791
|
+
sessionId: row.session_id ?? undefined,
|
|
1792
|
+
trigger: row.trigger,
|
|
1793
|
+
pendingEvents: Number(row.pending_events),
|
|
1794
|
+
releasedLeases: Number(row.released_leases),
|
|
1795
|
+
createdAt: Number(row.created_at),
|
|
1796
|
+
}));
|
|
1797
|
+
}
|
|
1798
|
+
|
|
995
1799
|
// Convenience methods for raw SQL queries (used by AutoChecker)
|
|
996
1800
|
all(sql: string, ...params: unknown[]): unknown[] {
|
|
997
1801
|
return this.db.prepare(sql).all(...params);
|
|
@@ -1005,3 +1809,42 @@ export class DevFlowDatabase {
|
|
|
1005
1809
|
this.db.close();
|
|
1006
1810
|
}
|
|
1007
1811
|
}
|
|
1812
|
+
|
|
1813
|
+
function parseJson(value: unknown): unknown {
|
|
1814
|
+
if (typeof value !== 'string') return value;
|
|
1815
|
+
try { return JSON.parse(value); } catch { return value; }
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function deriveQuery(input: unknown): string | null {
|
|
1819
|
+
if (!input || typeof input !== 'object') return null;
|
|
1820
|
+
const record = input as Record<string, unknown>;
|
|
1821
|
+
for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
|
|
1822
|
+
if (typeof record[key] === 'string' && record[key]) return record[key] as string;
|
|
1823
|
+
}
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function deriveEngine(toolName: string): string | null {
|
|
1828
|
+
if (/project_context|symbol|dependency_graph|codegraph/.test(toolName)) return 'codegraph';
|
|
1829
|
+
if (/knowledge|docs/.test(toolName)) return 'knowledge';
|
|
1830
|
+
if (/memory/.test(toolName)) return 'memory';
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function countTelemetryResults(value: unknown, depth = 0): number {
|
|
1835
|
+
if (value == null || depth > 2) return 0;
|
|
1836
|
+
if (Array.isArray(value)) return value.length;
|
|
1837
|
+
if (typeof value !== 'object') return value === '' ? 0 : 1;
|
|
1838
|
+
|
|
1839
|
+
const record = value as Record<string, unknown>;
|
|
1840
|
+
const unwrapped = record.data ?? record.structuredContent;
|
|
1841
|
+
if (unwrapped !== undefined && unwrapped !== value) return countTelemetryResults(unwrapped, depth + 1);
|
|
1842
|
+
|
|
1843
|
+
let count = 0;
|
|
1844
|
+
for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
|
|
1845
|
+
if (Array.isArray(record[key])) count += record[key].length;
|
|
1846
|
+
}
|
|
1847
|
+
if (count > 0) return count;
|
|
1848
|
+
if (record.error) return 0;
|
|
1849
|
+
return Object.keys(record).length > 0 ? 1 : 0;
|
|
1850
|
+
}
|