@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 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('should compute MCP compliance rate', () => {
146
- // Execution 1: 2 MCP, 1 direct = 66.7%
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: true,
174
+ isMcpTool: false, mcpEnforced: false, mcpFallback: false,
169
175
  input: {}, tokensUsed: 50, duration: 100, blocked: false,
170
176
  });
171
177
 
172
- // Execution 2: 1 MCP, 0 direct = 100%
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).toBeCloseTo(75, 0); // 3 MCP / 4 total = 75%
188
- expect(compliance.bySkill['devflow:react']).toBeCloseTo(66.7, 0);
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
  });
@@ -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
- // Overall compliance (include all executions, not just completed)
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
- for (const row of bySkillRows) {
837
- bySkill[row.skill_name] = row.total_calls > 0
838
- ? Math.round((row.mcp_calls / row.total_calls) * 10000) / 100
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
- // Missed tools: tools that were required but fell back to direct
842
- const missedRows = this.db.prepare(`
843
- SELECT DISTINCT mcp_tool_name
844
- FROM tool_call_events
845
- WHERE mcp_fallback = 1 AND mcp_tool_name IS NOT NULL
846
- ORDER BY timestamp DESC
847
- LIMIT 10
848
- `).all();
849
- const missedTools = missedRows.map((row) => row.mcp_tool_name);
850
- return { overall, bySkill, missedTools };
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 = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
856
- this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
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.0",
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": "2f52776bda72517354d44453756cccb9e8de9afa"
16
+ "gitHead": "5f49cf58702ee94ad9e4ab585e58f824e8633721"
17
17
  }