@devflow-tools/database 0.16.0 → 0.16.3
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 +22 -0
- package/__tests__/database.skill-executions.test.ts +21 -6
- package/dist/database.d.ts +32 -0
- package/dist/database.js +256 -41
- package/package.json +2 -2
- package/src/database.ts +296 -47
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.16.3](https://github.com/shilongfeicool/dev-flow/compare/v0.16.2...v0.16.3) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **telemetry:** measure required MCP obligations ([f7e976c](https://github.com/shilongfeicool/dev-flow/commit/f7e976c28bf1629cf4c05c2ba6a49f569bf882d7))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.16.2](https://github.com/shilongfeicool/dev-flow/compare/v0.16.1...v0.16.2) (2026-07-22)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* harden Claude plugin lifecycle ([992e74f](https://github.com/shilongfeicool/dev-flow/commit/992e74ff0a37cdb5132736eba3926be7639ac806))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# [0.16.0](https://github.com/shilongfeicool/dev-flow/compare/v0.15.0...v0.16.0) (2026-07-22)
|
|
7
29
|
|
|
8
30
|
|
|
@@ -142,14 +142,20 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
142
142
|
expect(reactExecs[0].skillName).toBe('devflow:react');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
it('
|
|
146
|
-
//
|
|
145
|
+
it('computes required MCP obligation compliance independently of direct calls', () => {
|
|
146
|
+
// React requires successful context plus at least one React domain tool.
|
|
147
147
|
db.insertSkillExecution({
|
|
148
148
|
executionId: 'exec_001',
|
|
149
149
|
skillName: 'devflow:react',
|
|
150
150
|
startedAt: Date.now(),
|
|
151
151
|
status: 'completed',
|
|
152
152
|
});
|
|
153
|
+
db.insertToolCallEvent({
|
|
154
|
+
eventId: 'evt_context_1', executionId: 'exec_001', timestamp: Date.now(),
|
|
155
|
+
toolName: 'mcp__devflow__get_project_context', toolType: 'mcp',
|
|
156
|
+
isMcpTool: true, mcpToolName: 'get_project_context', mcpEnforced: true,
|
|
157
|
+
mcpFallback: false, input: {}, duration: 20, blocked: false,
|
|
158
|
+
});
|
|
153
159
|
db.insertToolCallEvent({
|
|
154
160
|
eventId: 'evt_001', executionId: 'exec_001', timestamp: Date.now(),
|
|
155
161
|
toolName: 'mcp__devflow__react_diagnose_bug', toolType: 'mcp',
|
|
@@ -165,17 +171,23 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
165
171
|
db.insertToolCallEvent({
|
|
166
172
|
eventId: 'evt_003', executionId: 'exec_001', timestamp: Date.now(),
|
|
167
173
|
toolName: 'Read', toolType: 'direct',
|
|
168
|
-
isMcpTool: false, mcpEnforced: false, mcpFallback:
|
|
174
|
+
isMcpTool: false, mcpEnforced: false, mcpFallback: false,
|
|
169
175
|
input: {}, tokensUsed: 50, duration: 100, blocked: false,
|
|
170
176
|
});
|
|
171
177
|
|
|
172
|
-
//
|
|
178
|
+
// Vue has the same context + domain obligations.
|
|
173
179
|
db.insertSkillExecution({
|
|
174
180
|
executionId: 'exec_002',
|
|
175
181
|
skillName: 'devflow:vue',
|
|
176
182
|
startedAt: Date.now(),
|
|
177
183
|
status: 'completed',
|
|
178
184
|
});
|
|
185
|
+
db.insertToolCallEvent({
|
|
186
|
+
eventId: 'evt_context_2', executionId: 'exec_002', timestamp: Date.now(),
|
|
187
|
+
toolName: 'mcp__devflow__get_project_context', toolType: 'mcp',
|
|
188
|
+
isMcpTool: true, mcpToolName: 'get_project_context', mcpEnforced: true,
|
|
189
|
+
mcpFallback: false, input: {}, duration: 20, blocked: false,
|
|
190
|
+
});
|
|
179
191
|
db.insertToolCallEvent({
|
|
180
192
|
eventId: 'evt_004', executionId: 'exec_002', timestamp: Date.now(),
|
|
181
193
|
toolName: 'mcp__devflow__vue_diagnose_bug', toolType: 'mcp',
|
|
@@ -184,8 +196,11 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
184
196
|
});
|
|
185
197
|
|
|
186
198
|
const compliance = db.getMcpCompliance();
|
|
187
|
-
expect(compliance.overall).
|
|
188
|
-
expect(compliance.bySkill['devflow:react']).
|
|
199
|
+
expect(compliance.overall).toBe(100);
|
|
200
|
+
expect(compliance.bySkill['devflow:react']).toBe(100);
|
|
189
201
|
expect(compliance.bySkill['devflow:vue']).toBeCloseTo(100, 0);
|
|
202
|
+
expect(compliance.applicableObligations).toBe(4);
|
|
203
|
+
expect(compliance.satisfiedObligations).toBe(4);
|
|
204
|
+
expect(compliance.mcpCallShare).toBeCloseTo(83.33, 1);
|
|
190
205
|
});
|
|
191
206
|
});
|
package/dist/database.d.ts
CHANGED
|
@@ -66,6 +66,14 @@ export interface HookReceiptRecord {
|
|
|
66
66
|
bypassCount?: number;
|
|
67
67
|
updatedAt: number;
|
|
68
68
|
}
|
|
69
|
+
export interface ContextReceiptRecord {
|
|
70
|
+
projectRoot: string;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
executionId: string;
|
|
73
|
+
contextHash: string;
|
|
74
|
+
issuedAt: number;
|
|
75
|
+
expiresAt: number;
|
|
76
|
+
}
|
|
69
77
|
export interface MemoryDistillCheckpointRecord {
|
|
70
78
|
id: string;
|
|
71
79
|
projectRoot: string;
|
|
@@ -105,6 +113,12 @@ export declare class DevFlowDatabase {
|
|
|
105
113
|
label?: string;
|
|
106
114
|
startedAt: number;
|
|
107
115
|
}): void;
|
|
116
|
+
ensureSession(params: {
|
|
117
|
+
id: string;
|
|
118
|
+
projectRoot: string;
|
|
119
|
+
label?: string;
|
|
120
|
+
startedAt?: number;
|
|
121
|
+
}): void;
|
|
108
122
|
getSession(id: string): any | null;
|
|
109
123
|
listSessions(limit: number, offset: number, projectRoot?: string): any[];
|
|
110
124
|
closeSession(id: string, finishedAt?: number): void;
|
|
@@ -130,6 +144,7 @@ export declare class DevFlowDatabase {
|
|
|
130
144
|
mcpToolCalls?: number;
|
|
131
145
|
directToolCalls?: number;
|
|
132
146
|
subagentCount?: number;
|
|
147
|
+
metadata?: Record<string, unknown>;
|
|
133
148
|
}): void;
|
|
134
149
|
listSkillExecutions(limit: number, skillName?: string): any[];
|
|
135
150
|
insertToolCallEvent(params: {
|
|
@@ -155,6 +170,7 @@ export declare class DevFlowDatabase {
|
|
|
155
170
|
workflowRunId?: string;
|
|
156
171
|
failureCategory?: string;
|
|
157
172
|
tokensUsed?: number;
|
|
173
|
+
toolUseId?: string;
|
|
158
174
|
}): boolean;
|
|
159
175
|
listToolCallEvents(executionId: string): any[];
|
|
160
176
|
updateToolCallEvent(eventId: string, updates: {
|
|
@@ -162,10 +178,22 @@ export declare class DevFlowDatabase {
|
|
|
162
178
|
error?: string;
|
|
163
179
|
duration?: number;
|
|
164
180
|
}): boolean;
|
|
181
|
+
markToolCallBlocked(eventId: string, reason: string): boolean;
|
|
182
|
+
reconcileSkillExecution(executionId: string, status: 'completed' | 'failed', finishedAt?: number, metadata?: Record<string, unknown>): void;
|
|
183
|
+
getExecutionObligationCompliance(executionId: string): {
|
|
184
|
+
rate: number;
|
|
185
|
+
applicable: number;
|
|
186
|
+
satisfied: number;
|
|
187
|
+
missedTools: string[];
|
|
188
|
+
mcpCallShare: number;
|
|
189
|
+
};
|
|
165
190
|
getMcpCompliance(): {
|
|
166
191
|
overall: number;
|
|
167
192
|
bySkill: Record<string, number>;
|
|
168
193
|
missedTools: string[];
|
|
194
|
+
applicableObligations: number;
|
|
195
|
+
satisfiedObligations: number;
|
|
196
|
+
mcpCallShare: number;
|
|
169
197
|
};
|
|
170
198
|
cleanupStaleRecords(): void;
|
|
171
199
|
insertToolMetric(metric: {
|
|
@@ -238,6 +266,10 @@ export declare class DevFlowDatabase {
|
|
|
238
266
|
getHookReceipt(projectRoot: string): HookReceiptRecord | null;
|
|
239
267
|
updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
|
|
240
268
|
deleteHookReceipt(projectRoot: string): boolean;
|
|
269
|
+
upsertContextReceipt(receipt: ContextReceiptRecord): void;
|
|
270
|
+
getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
|
|
271
|
+
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number;
|
|
272
|
+
purgeExpiredContextReceipts(now?: number): number;
|
|
241
273
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void;
|
|
242
274
|
listMemoryDistillCheckpoints(projectRoot: string, limit?: number): MemoryDistillCheckpointRecord[];
|
|
243
275
|
all(sql: string, ...params: unknown[]): unknown[];
|
package/dist/database.js
CHANGED
|
@@ -7,6 +7,36 @@ const node_sqlite_1 = require("./node-sqlite");
|
|
|
7
7
|
const path_1 = require("path");
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const os_1 = require("os");
|
|
10
|
+
const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
11
|
+
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
12
|
+
]);
|
|
13
|
+
function toolNameMatches(event, expected) {
|
|
14
|
+
if (event.blocked || event.error)
|
|
15
|
+
return false;
|
|
16
|
+
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
17
|
+
.replace(/^mcp__[^_]+__/, '');
|
|
18
|
+
return name === expected;
|
|
19
|
+
}
|
|
20
|
+
function obligationsForSkill(skillName, configured) {
|
|
21
|
+
const configuredTools = Array.isArray(configured)
|
|
22
|
+
? configured.filter((tool) => typeof tool === 'string' && tool.length > 0)
|
|
23
|
+
: [];
|
|
24
|
+
const family = skillName.split(':').pop() ?? skillName;
|
|
25
|
+
if (CONTEXT_REQUIRED_SKILLS.has(family)) {
|
|
26
|
+
return [`${family}:context`, `${family}:domain`];
|
|
27
|
+
}
|
|
28
|
+
if (family === 'context')
|
|
29
|
+
return ['get_project_context'];
|
|
30
|
+
if (family === 'graph')
|
|
31
|
+
return ['get_dependency_graph'];
|
|
32
|
+
if (family === 'knowledge')
|
|
33
|
+
return ['get_knowledge'];
|
|
34
|
+
if (family === 'memory')
|
|
35
|
+
return ['get_memory'];
|
|
36
|
+
if (family === 'workflow')
|
|
37
|
+
return ['run_workflow'];
|
|
38
|
+
return configuredTools.slice(0, 1);
|
|
39
|
+
}
|
|
10
40
|
function getGlobalDevFlowDbPath(home = (0, os_1.homedir)()) {
|
|
11
41
|
const stateDir = process.env.DEVFLOW_STATE_DIR ?? (0, path_1.join)(home, '.devflow', 'global');
|
|
12
42
|
return (0, path_1.join)(stateDir, 'devflow.db');
|
|
@@ -137,7 +167,8 @@ class DevFlowDatabase {
|
|
|
137
167
|
error TEXT,
|
|
138
168
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
139
169
|
block_reason TEXT,
|
|
140
|
-
failure_category TEXT
|
|
170
|
+
failure_category TEXT,
|
|
171
|
+
tool_use_id TEXT
|
|
141
172
|
);
|
|
142
173
|
|
|
143
174
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -160,7 +191,8 @@ class DevFlowDatabase {
|
|
|
160
191
|
mcp_tool_calls INTEGER DEFAULT 0,
|
|
161
192
|
direct_tool_calls INTEGER DEFAULT 0,
|
|
162
193
|
subagent_count INTEGER DEFAULT 0,
|
|
163
|
-
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
|
|
194
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
195
|
+
metadata TEXT
|
|
164
196
|
);
|
|
165
197
|
|
|
166
198
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
@@ -227,6 +259,19 @@ class DevFlowDatabase {
|
|
|
227
259
|
updated_at INTEGER NOT NULL
|
|
228
260
|
);
|
|
229
261
|
|
|
262
|
+
CREATE TABLE IF NOT EXISTS devflow_context_receipts (
|
|
263
|
+
project_root TEXT NOT NULL,
|
|
264
|
+
session_id TEXT NOT NULL,
|
|
265
|
+
execution_id TEXT NOT NULL,
|
|
266
|
+
context_hash TEXT NOT NULL,
|
|
267
|
+
issued_at INTEGER NOT NULL,
|
|
268
|
+
expires_at INTEGER NOT NULL,
|
|
269
|
+
PRIMARY KEY (project_root, session_id, execution_id)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
273
|
+
ON devflow_context_receipts(expires_at);
|
|
274
|
+
|
|
230
275
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
231
276
|
id TEXT PRIMARY KEY,
|
|
232
277
|
project_root TEXT NOT NULL,
|
|
@@ -269,6 +314,23 @@ class DevFlowDatabase {
|
|
|
269
314
|
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT');
|
|
270
315
|
}
|
|
271
316
|
catch { }
|
|
317
|
+
try {
|
|
318
|
+
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT');
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
try {
|
|
322
|
+
this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT');
|
|
323
|
+
}
|
|
324
|
+
catch { }
|
|
325
|
+
try {
|
|
326
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT');
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
try {
|
|
330
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
331
|
+
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
332
|
+
}
|
|
333
|
+
catch { }
|
|
272
334
|
try {
|
|
273
335
|
this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1');
|
|
274
336
|
}
|
|
@@ -507,6 +569,22 @@ class DevFlowDatabase {
|
|
|
507
569
|
VALUES (?, ?, ?, ?, 'running')
|
|
508
570
|
`).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
|
|
509
571
|
}
|
|
572
|
+
ensureSession(params) {
|
|
573
|
+
if (!params.id.trim())
|
|
574
|
+
throw new Error('Canonical session ID is required');
|
|
575
|
+
const startedAt = params.startedAt ?? Date.now();
|
|
576
|
+
this.db.prepare(`
|
|
577
|
+
INSERT INTO sessions (id, project_root, label, started_at, status)
|
|
578
|
+
VALUES (?, ?, ?, ?, 'running')
|
|
579
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
580
|
+
project_root = CASE
|
|
581
|
+
WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
|
|
582
|
+
ELSE sessions.project_root
|
|
583
|
+
END,
|
|
584
|
+
label = COALESCE(sessions.label, excluded.label),
|
|
585
|
+
started_at = MIN(sessions.started_at, excluded.started_at)
|
|
586
|
+
`).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
|
|
587
|
+
}
|
|
510
588
|
getSession(id) {
|
|
511
589
|
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
512
590
|
if (!row)
|
|
@@ -523,6 +601,7 @@ class DevFlowDatabase {
|
|
|
523
601
|
directToolCalls: row.direct_tool_calls,
|
|
524
602
|
subagentCount: row.subagent_count,
|
|
525
603
|
status: row.status,
|
|
604
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
526
605
|
};
|
|
527
606
|
}
|
|
528
607
|
listSessions(limit, offset, projectRoot) {
|
|
@@ -547,6 +626,7 @@ class DevFlowDatabase {
|
|
|
547
626
|
directToolCalls: row.direct_tool_calls,
|
|
548
627
|
subagentCount: row.subagent_count,
|
|
549
628
|
status: row.status,
|
|
629
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
550
630
|
}));
|
|
551
631
|
}
|
|
552
632
|
closeSession(id, finishedAt) {
|
|
@@ -601,6 +681,7 @@ class DevFlowDatabase {
|
|
|
601
681
|
blocked: row.blocked === 1,
|
|
602
682
|
blockReason: row.block_reason,
|
|
603
683
|
failureCategory: row.failure_category,
|
|
684
|
+
toolUseId: row.tool_use_id,
|
|
604
685
|
tokensUsed: row.tokens_used ?? 0,
|
|
605
686
|
}));
|
|
606
687
|
}
|
|
@@ -635,6 +716,7 @@ class DevFlowDatabase {
|
|
|
635
716
|
blocked: row.blocked === 1,
|
|
636
717
|
blockReason: row.block_reason,
|
|
637
718
|
failureCategory: row.failure_category,
|
|
719
|
+
toolUseId: row.tool_use_id,
|
|
638
720
|
tokensUsed: row.tokens_used ?? 0,
|
|
639
721
|
});
|
|
640
722
|
}
|
|
@@ -669,6 +751,7 @@ class DevFlowDatabase {
|
|
|
669
751
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
670
752
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
671
753
|
createdAt: row.created_at,
|
|
754
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
672
755
|
};
|
|
673
756
|
}
|
|
674
757
|
updateSkillExecution(executionId, updates) {
|
|
@@ -710,6 +793,10 @@ class DevFlowDatabase {
|
|
|
710
793
|
sets.push('subagent_count = ?');
|
|
711
794
|
values.push(updates.subagentCount);
|
|
712
795
|
}
|
|
796
|
+
if (updates.metadata !== undefined) {
|
|
797
|
+
sets.push('metadata = ?');
|
|
798
|
+
values.push(JSON.stringify(updates.metadata));
|
|
799
|
+
}
|
|
713
800
|
if (sets.length > 0) {
|
|
714
801
|
values.push(executionId);
|
|
715
802
|
this.db.prepare(`UPDATE skill_executions SET ${sets.join(', ')} WHERE execution_id = ?`).run(...values);
|
|
@@ -738,6 +825,7 @@ class DevFlowDatabase {
|
|
|
738
825
|
subagentCount: row.subagent_count,
|
|
739
826
|
totalDuration: row.total_duration,
|
|
740
827
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
828
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
741
829
|
}));
|
|
742
830
|
}
|
|
743
831
|
// ---- Tool Call Events ----
|
|
@@ -748,9 +836,9 @@ class DevFlowDatabase {
|
|
|
748
836
|
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
749
837
|
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
|
|
750
838
|
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);
|
|
839
|
+
failure_category, tool_use_id)
|
|
840
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
841
|
+
`).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, params.toolUseId || null);
|
|
754
842
|
if (result.changes === 0)
|
|
755
843
|
return false;
|
|
756
844
|
this.db.prepare(`
|
|
@@ -758,9 +846,24 @@ class DevFlowDatabase {
|
|
|
758
846
|
SET total_tool_calls = total_tool_calls + 1,
|
|
759
847
|
mcp_tool_calls = mcp_tool_calls + ?,
|
|
760
848
|
direct_tool_calls = direct_tool_calls + ?,
|
|
761
|
-
subagent_count = subagent_count +
|
|
849
|
+
subagent_count = subagent_count + ?,
|
|
850
|
+
total_duration = MAX(total_duration, ? - started_at),
|
|
851
|
+
mcp_compliance_rate = ROUND(
|
|
852
|
+
100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
|
|
853
|
+
)
|
|
762
854
|
WHERE execution_id = ?
|
|
763
|
-
`).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.executionId);
|
|
855
|
+
`).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.isMcpTool ? 1 : 0, params.executionId);
|
|
856
|
+
if (params.sessionId) {
|
|
857
|
+
this.db.prepare(`
|
|
858
|
+
UPDATE sessions
|
|
859
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
860
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
861
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
862
|
+
subagent_count = subagent_count + ?,
|
|
863
|
+
duration_ms = MAX(duration_ms, ? - started_at)
|
|
864
|
+
WHERE id = ?
|
|
865
|
+
`).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.sessionId);
|
|
866
|
+
}
|
|
764
867
|
return true;
|
|
765
868
|
});
|
|
766
869
|
}
|
|
@@ -787,6 +890,7 @@ class DevFlowDatabase {
|
|
|
787
890
|
blocked: row.blocked === 1,
|
|
788
891
|
blockReason: row.block_reason,
|
|
789
892
|
failureCategory: row.failure_category,
|
|
893
|
+
toolUseId: row.tool_use_id,
|
|
790
894
|
}));
|
|
791
895
|
}
|
|
792
896
|
updateToolCallEvent(eventId, updates) {
|
|
@@ -811,49 +915,121 @@ class DevFlowDatabase {
|
|
|
811
915
|
}
|
|
812
916
|
return false;
|
|
813
917
|
}
|
|
918
|
+
markToolCallBlocked(eventId, reason) {
|
|
919
|
+
return this.db.prepare(`
|
|
920
|
+
UPDATE tool_call_events
|
|
921
|
+
SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
|
|
922
|
+
WHERE event_id = ?
|
|
923
|
+
`).run(reason, reason, eventId).changes > 0;
|
|
924
|
+
}
|
|
925
|
+
reconcileSkillExecution(executionId, status, finishedAt = Date.now(), metadata) {
|
|
926
|
+
const row = this.db.prepare(`
|
|
927
|
+
SELECT COUNT(*) AS total,
|
|
928
|
+
SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
|
|
929
|
+
SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
|
|
930
|
+
SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
|
|
931
|
+
MIN(timestamp) AS first_at,
|
|
932
|
+
MAX(timestamp) AS last_at
|
|
933
|
+
FROM tool_call_events WHERE execution_id = ?
|
|
934
|
+
`).get(executionId);
|
|
935
|
+
const total = Number(row?.total ?? 0);
|
|
936
|
+
const mcp = Number(row?.mcp ?? 0);
|
|
937
|
+
const obligation = this.getExecutionObligationCompliance(executionId);
|
|
938
|
+
this.updateSkillExecution(executionId, {
|
|
939
|
+
status,
|
|
940
|
+
finishedAt,
|
|
941
|
+
totalToolCalls: total,
|
|
942
|
+
mcpToolCalls: mcp,
|
|
943
|
+
directToolCalls: Number(row?.direct ?? 0),
|
|
944
|
+
subagentCount: Number(row?.subagents ?? 0),
|
|
945
|
+
totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
946
|
+
mcpComplianceRate: obligation.rate,
|
|
947
|
+
metadata,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
getExecutionObligationCompliance(executionId) {
|
|
951
|
+
const execution = this.getSkillExecution(executionId);
|
|
952
|
+
const events = this.listToolCallEvents(executionId);
|
|
953
|
+
if (!execution) {
|
|
954
|
+
return { rate: 0, applicable: 0, satisfied: 0, missedTools: [], mcpCallShare: 0 };
|
|
955
|
+
}
|
|
956
|
+
const obligations = obligationsForSkill(execution.skillName, execution.requiredMcpTools);
|
|
957
|
+
const satisfied = obligations.filter((obligation) => {
|
|
958
|
+
if (obligation.endsWith(':context')) {
|
|
959
|
+
return events.some((event) => toolNameMatches(event, 'get_project_context'));
|
|
960
|
+
}
|
|
961
|
+
if (obligation.endsWith(':domain')) {
|
|
962
|
+
const family = obligation.slice(0, -':domain'.length);
|
|
963
|
+
return events.some((event) => {
|
|
964
|
+
if (event.blocked || event.error || !event.isMcpTool)
|
|
965
|
+
return false;
|
|
966
|
+
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
967
|
+
.replace(/^mcp__[^_]+__/, '');
|
|
968
|
+
return name.startsWith(`${family}_`);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
return events.some((event) => toolNameMatches(event, obligation));
|
|
972
|
+
});
|
|
973
|
+
const missedTools = obligations.filter((obligation) => !satisfied.includes(obligation));
|
|
974
|
+
const total = events.length;
|
|
975
|
+
const mcp = events.filter((event) => event.isMcpTool).length;
|
|
976
|
+
return {
|
|
977
|
+
rate: obligations.length > 0
|
|
978
|
+
? Math.round((satisfied.length / obligations.length) * 10000) / 100
|
|
979
|
+
: 0,
|
|
980
|
+
applicable: obligations.length,
|
|
981
|
+
satisfied: satisfied.length,
|
|
982
|
+
missedTools,
|
|
983
|
+
mcpCallShare: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
814
986
|
// ---- MCP Compliance ----
|
|
815
987
|
getMcpCompliance() {
|
|
816
|
-
|
|
817
|
-
const overallRow = this.db.prepare(`
|
|
818
|
-
SELECT
|
|
819
|
-
SUM(mcp_tool_calls) as mcp_calls,
|
|
820
|
-
SUM(total_tool_calls) as total_calls
|
|
821
|
-
FROM skill_executions
|
|
822
|
-
`).get();
|
|
823
|
-
const overall = overallRow?.total_calls > 0
|
|
824
|
-
? Math.round((overallRow.mcp_calls / overallRow.total_calls) * 10000) / 100
|
|
825
|
-
: 0;
|
|
826
|
-
// By skill
|
|
827
|
-
const bySkillRows = this.db.prepare(`
|
|
828
|
-
SELECT
|
|
829
|
-
skill_name,
|
|
830
|
-
SUM(mcp_tool_calls) as mcp_calls,
|
|
831
|
-
SUM(total_tool_calls) as total_calls
|
|
832
|
-
FROM skill_executions
|
|
833
|
-
GROUP BY skill_name
|
|
834
|
-
`).all();
|
|
988
|
+
const executions = this.db.prepare('SELECT execution_id, skill_name FROM skill_executions').all();
|
|
835
989
|
const bySkill = {};
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
990
|
+
const bySkillCounts = new Map();
|
|
991
|
+
let applicableObligations = 0;
|
|
992
|
+
let satisfiedObligations = 0;
|
|
993
|
+
let totalCalls = 0;
|
|
994
|
+
let mcpCalls = 0;
|
|
995
|
+
const missedTools = new Set();
|
|
996
|
+
for (const execution of executions) {
|
|
997
|
+
const result = this.getExecutionObligationCompliance(execution.execution_id);
|
|
998
|
+
const skill = String(execution.skill_name ?? 'unknown');
|
|
999
|
+
const skillCounts = bySkillCounts.get(skill) ?? { applicable: 0, satisfied: 0 };
|
|
1000
|
+
skillCounts.applicable += result.applicable;
|
|
1001
|
+
skillCounts.satisfied += result.satisfied;
|
|
1002
|
+
bySkillCounts.set(skill, skillCounts);
|
|
1003
|
+
applicableObligations += result.applicable;
|
|
1004
|
+
satisfiedObligations += result.satisfied;
|
|
1005
|
+
result.missedTools.forEach((tool) => missedTools.add(tool));
|
|
1006
|
+
const events = this.listToolCallEvents(execution.execution_id);
|
|
1007
|
+
totalCalls += events.length;
|
|
1008
|
+
mcpCalls += events.filter((event) => event.isMcpTool).length;
|
|
1009
|
+
}
|
|
1010
|
+
for (const [skill, counts] of bySkillCounts) {
|
|
1011
|
+
bySkill[skill] = counts.applicable > 0
|
|
1012
|
+
? Math.round((counts.satisfied / counts.applicable) * 10000) / 100
|
|
839
1013
|
: 0;
|
|
840
1014
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1015
|
+
return {
|
|
1016
|
+
overall: applicableObligations > 0
|
|
1017
|
+
? Math.round((satisfiedObligations / applicableObligations) * 10000) / 100
|
|
1018
|
+
: 0,
|
|
1019
|
+
bySkill,
|
|
1020
|
+
missedTools: [...missedTools].slice(0, 10),
|
|
1021
|
+
applicableObligations,
|
|
1022
|
+
satisfiedObligations,
|
|
1023
|
+
mcpCallShare: totalCalls > 0 ? Math.round((mcpCalls / totalCalls) * 10000) / 100 : 0,
|
|
1024
|
+
};
|
|
851
1025
|
}
|
|
852
1026
|
cleanupStaleRecords() {
|
|
853
1027
|
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
854
1028
|
try {
|
|
855
|
-
this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at =
|
|
856
|
-
|
|
1029
|
+
this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1030
|
+
WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
1031
|
+
this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1032
|
+
WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
857
1033
|
}
|
|
858
1034
|
catch { /* best-effort */ }
|
|
859
1035
|
}
|
|
@@ -1240,6 +1416,45 @@ class DevFlowDatabase {
|
|
|
1240
1416
|
return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
|
|
1241
1417
|
.run(projectRoot).changes > 0;
|
|
1242
1418
|
}
|
|
1419
|
+
upsertContextReceipt(receipt) {
|
|
1420
|
+
this.db.prepare(`
|
|
1421
|
+
INSERT INTO devflow_context_receipts
|
|
1422
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at)
|
|
1423
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1424
|
+
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
1425
|
+
context_hash = excluded.context_hash,
|
|
1426
|
+
issued_at = excluded.issued_at,
|
|
1427
|
+
expires_at = excluded.expires_at
|
|
1428
|
+
`).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt);
|
|
1429
|
+
}
|
|
1430
|
+
getContextReceipt(projectRoot, sessionId, executionId) {
|
|
1431
|
+
const row = this.db.prepare(`
|
|
1432
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
1433
|
+
FROM devflow_context_receipts
|
|
1434
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
1435
|
+
`).get(projectRoot, sessionId, executionId);
|
|
1436
|
+
return row ? {
|
|
1437
|
+
projectRoot: row.project_root,
|
|
1438
|
+
sessionId: row.session_id,
|
|
1439
|
+
executionId: row.execution_id,
|
|
1440
|
+
contextHash: row.context_hash,
|
|
1441
|
+
issuedAt: row.issued_at,
|
|
1442
|
+
expiresAt: row.expires_at,
|
|
1443
|
+
} : null;
|
|
1444
|
+
}
|
|
1445
|
+
deleteContextReceipt(projectRoot, sessionId, executionId) {
|
|
1446
|
+
return executionId
|
|
1447
|
+
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1448
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
|
|
1449
|
+
.run(projectRoot, sessionId, executionId).changes
|
|
1450
|
+
: this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1451
|
+
WHERE project_root = ? AND session_id = ?`)
|
|
1452
|
+
.run(projectRoot, sessionId).changes;
|
|
1453
|
+
}
|
|
1454
|
+
purgeExpiredContextReceipts(now = Date.now()) {
|
|
1455
|
+
return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
|
|
1456
|
+
.run(now).changes;
|
|
1457
|
+
}
|
|
1243
1458
|
recordMemoryDistillCheckpoint(checkpoint) {
|
|
1244
1459
|
this.db.prepare(`
|
|
1245
1460
|
INSERT INTO devflow_memory_distill_checkpoints
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devflow-tools/database",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "DevFlow SQLite database package",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
"typescript": "^5.5.0",
|
|
14
14
|
"vitest": "^2.0.0"
|
|
15
15
|
},
|
|
16
|
-
"gitHead": "
|
|
16
|
+
"gitHead": "5f49cf58702ee94ad9e4ab585e58f824e8633721"
|
|
17
17
|
}
|