@devflow-tools/database 0.17.7 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/data/LocalDataProvider.d.ts +63 -0
  4. package/dist/data/LocalDataProvider.js +332 -0
  5. package/dist/data/default-enforcer-rules.d.ts +3 -0
  6. package/dist/data/default-enforcer-rules.js +170 -0
  7. package/dist/data/devflow-schema.d.ts +2 -0
  8. package/dist/data/devflow-schema.js +80 -0
  9. package/dist/database.d.ts +45 -7
  10. package/dist/database.js +583 -125
  11. package/dist/index.d.ts +8 -4
  12. package/dist/index.js +11 -5
  13. package/dist/retrieval-ledger.d.ts +16 -1
  14. package/dist/retrieval-ledger.js +6 -0
  15. package/dist/retrieval-runtime.d.ts +39 -0
  16. package/dist/retrieval-runtime.js +2 -0
  17. package/dist/task-aggregate.d.ts +49 -0
  18. package/dist/task-aggregate.js +2 -0
  19. package/dist/task-semantic-control.d.ts +8 -1
  20. package/dist/task-semantic-control.js +2 -4
  21. package/package.json +15 -3
  22. package/CHANGELOG.md +0 -836
  23. package/__tests__/database.failure-category.test.ts +0 -44
  24. package/__tests__/database.host-actions.test.ts +0 -405
  25. package/__tests__/database.learning-candidates.test.ts +0 -93
  26. package/__tests__/database.memory-turn-receipts.test.ts +0 -98
  27. package/__tests__/database.retrieval-ledger.test.ts +0 -68
  28. package/__tests__/database.retrieval-sessions.test.ts +0 -79
  29. package/__tests__/database.semantic-resolution.test.ts +0 -87
  30. package/__tests__/database.skill-executions.test.ts +0 -456
  31. package/__tests__/database.task-runtime.test.ts +0 -73
  32. package/__tests__/database.test.ts +0 -177
  33. package/__tests__/database.work-queue.test.ts +0 -274
  34. package/__tests__/database.workflow-workers.test.ts +0 -60
  35. package/__tests__/node-sqlite.test.ts +0 -68
  36. package/src/database.ts +0 -5853
  37. package/src/host-actions.ts +0 -211
  38. package/src/index.ts +0 -153
  39. package/src/learning-candidates.ts +0 -181
  40. package/src/node-sqlite.ts +0 -60
  41. package/src/obligation-ledger.ts +0 -57
  42. package/src/retrieval-ledger.ts +0 -35
  43. package/src/retrieval-sessions.ts +0 -196
  44. package/src/semantic-resolution.ts +0 -181
  45. package/src/task-runtime.ts +0 -169
  46. package/src/task-semantic-control.ts +0 -270
  47. package/src/types.ts +0 -17
  48. package/src/work-queue.ts +0 -123
  49. package/src/workflow-workers.ts +0 -198
  50. package/tsconfig.json +0 -19
  51. package/tsconfig.tsbuildinfo +0 -1
  52. package/vitest.config.ts +0 -41
@@ -1,68 +0,0 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, describe, expect, it } from 'vitest';
5
- import { DevFlowDatabase, type RetrievalLedgerEventRecord } from '../src/index.js';
6
-
7
- describe('retrieval ledger', () => {
8
- const directories: string[] = [];
9
-
10
- afterEach(() => {
11
- for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true });
12
- });
13
-
14
- it('is append-only and idempotent per immutable event identity', () => {
15
- const database = createDatabase();
16
- const selected = event({ id: 'selected-a', stage: 'selected' });
17
- expect(database.appendRetrievalLedgerEvent(selected)).toBe(true);
18
- expect(database.appendRetrievalLedgerEvent({ ...selected, reason: 'changed' })).toBe(false);
19
- expect(database.appendRetrievalLedgerEvent(event({ id: 'exposed-a', stage: 'exposed' }))).toBe(true);
20
- expect(database.listRetrievalLedgerEvents({ projectRoot: '/project' })).toEqual(expect.arrayContaining([
21
- expect.objectContaining({ id: 'selected-a', stage: 'selected', reason: undefined }),
22
- expect.objectContaining({ id: 'exposed-a', stage: 'exposed' }),
23
- ]));
24
- database.close();
25
- });
26
-
27
- it('rejects score loss between returned evidence and persisted selected evidence', () => {
28
- const database = createDatabase();
29
- expect(() => database.appendRetrievalLedgerEvent(event({
30
- id: 'score-loss',
31
- stage: 'selected',
32
- finalScore: 0,
33
- payload: { returnedFinalScore: 0.81 },
34
- }))).toThrow('RETRIEVAL_LEDGER_INVALID_FINAL_SCORE');
35
- database.close();
36
- });
37
-
38
- it('persists v1 provenance and reads legacy adopted rows as consumed', () => {
39
- const database = createDatabase();
40
- database.appendRetrievalLedgerEvent(event({
41
- id: 'legacy-adopted', stage: 'adopted', taskSpecHash: 'spec:1', actor: 'tool:read',
42
- sourceVersion: 'v1', sourceContentHash: 'sha256:1', evidenceIds: ['memory-a'],
43
- reasonCode: 'structural_evidence_ref',
44
- }));
45
- expect(database.listRetrievalLedgerEvents({ projectRoot: '/project' })[0]).toMatchObject({
46
- schemaVersion: 'retrieval-ledger-event.v1', stage: 'consumed', taskSpecHash: 'spec:1',
47
- actor: 'tool:read', sourceVersion: 'v1', sourceContentHash: 'sha256:1',
48
- evidenceIds: ['memory-a'], reasonCode: 'structural_evidence_ref',
49
- });
50
- database.close();
51
- });
52
-
53
- function createDatabase(): DevFlowDatabase {
54
- const directory = mkdtempSync(join(tmpdir(), 'devflow-retrieval-ledger-'));
55
- directories.push(directory);
56
- return new DevFlowDatabase(join(directory, 'devflow.db'));
57
- }
58
-
59
- function event(overrides: Partial<RetrievalLedgerEventRecord>): RetrievalLedgerEventRecord {
60
- return {
61
- id: 'event-a', projectRoot: '/project', sessionId: 'session-a', executionId: 'execution-a',
62
- requestId: 'request-a', contextReceipt: 'context-a', sourceType: 'memory', sourceId: 'memory-a',
63
- stage: 'candidate', rank: 1, rawScore: 0.7, normalizedScore: 0.75, finalScore: 0.8,
64
- applicability: ['formatting'], toolEvidence: [], payload: { returnedFinalScore: 0.8 }, createdAt: 100,
65
- ...overrides,
66
- };
67
- }
68
- });
@@ -1,79 +0,0 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
- import { DevFlowDatabase } from '../src/database';
6
-
7
- describe('DevFlowDatabase retrieval sessions', () => {
8
- let root: string;
9
- let database: DevFlowDatabase;
10
-
11
- beforeEach(() => {
12
- root = mkdtempSync(join(tmpdir(), 'devflow-retrieval-session-'));
13
- database = new DevFlowDatabase(root);
14
- });
15
-
16
- afterEach(() => {
17
- database.close();
18
- rmSync(root, { recursive: true, force: true });
19
- });
20
-
21
- const create = () => database.createRetrievalSession({
22
- id: 'retrieval-1', requestId: 'request-1', projectRoot: '/project-a',
23
- sessionId: 'session-1', executionId: 'execution-1', query: 'review player',
24
- intent: 'performance', tokenBudget: 1000, baselineReceipt: 'baseline-1',
25
- expiresAt: Date.now() + 60_000,
26
- });
27
-
28
- it('persists idempotent cycles and rejects changed evidence', () => {
29
- expect(create()).toMatchObject({ id: 'retrieval-1', cycle: 0, remainingTokenBudget: 1000 });
30
- expect(create()).toMatchObject({ id: 'retrieval-1' });
31
- const input = {
32
- retrievalSessionId: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
33
- baselineReceipt: 'baseline-1', cycle: 1,
34
- gaps: [{ kind: 'caller' as const, reason: 'missing caller', evidence: [], resolver: 'codegraph' as const, priority: 80 }],
35
- selectedIds: ['code:src/player.tsx'], rejectedIds: [], tokenCost: 200,
36
- remainingTokenBudget: 800, quality: { status: 'healthy' }, receipt: 'cycle-1', evidenceHash: 'hash-1',
37
- };
38
- expect(database.appendRetrievalCycle(input)).toMatchObject({ cycle: 1, tokenCost: 200 });
39
- expect(database.appendRetrievalCycle(input)).toMatchObject({ evidenceHash: 'hash-1' });
40
- expect(() => database.appendRetrievalCycle({ ...input, evidenceHash: 'changed' }))
41
- .toThrow('RETRIEVAL_CYCLE_CONFLICT:1');
42
- expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-1'))
43
- .toMatchObject({ cycle: 1, remainingTokenBudget: 800 });
44
- });
45
-
46
- it('enforces ownership, sequence, terminal state, and expiration', () => {
47
- create();
48
- expect(database.getRetrievalSession('/project-b', 'session-1', 'retrieval-1')).toBeNull();
49
- expect(() => database.appendRetrievalCycle({
50
- retrievalSessionId: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
51
- baselineReceipt: 'baseline-1', cycle: 2, gaps: [], selectedIds: [], rejectedIds: [],
52
- tokenCost: 1, remainingTokenBudget: 999, quality: {}, receipt: 'cycle-2', evidenceHash: 'hash-2',
53
- })).toThrow('RETRIEVAL_CYCLE_SEQUENCE:0->2');
54
- database.finalizeRetrievalSession({
55
- id: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
56
- state: 'exhausted', finalReceipt: 'final-1',
57
- });
58
- expect(() => database.finalizeRetrievalSession({
59
- id: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
60
- state: 'satisfied', finalReceipt: 'different',
61
- })).toThrow('RETRIEVAL_SESSION_TERMINAL:exhausted');
62
-
63
- database.createRetrievalSession({
64
- id: 'retrieval-expire', requestId: 'request-expire', projectRoot: '/project-a',
65
- sessionId: 'session-1', query: 'x', intent: 'debug', tokenBudget: 1,
66
- baselineReceipt: 'baseline-expire', expiresAt: Date.now() + 10,
67
- });
68
- expect(database.expireRetrievalSessions(Date.now() + 20)).toBe(1);
69
- expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-expire')?.state).toBe('expired');
70
- });
71
-
72
- it('keeps retrieval rows stable across reopen', () => {
73
- create();
74
- database.close();
75
- database = new DevFlowDatabase(root);
76
- expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-1'))
77
- .toMatchObject({ requestId: 'request-1', baselineReceipt: 'baseline-1' });
78
- });
79
- });
@@ -1,87 +0,0 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
- import { DevFlowDatabase } from '../src/database';
6
- import type { SemanticResolutionRecord } from '../src/semantic-resolution';
7
-
8
- describe('DevFlowDatabase semantic resolution persistence', () => {
9
- let directory: string;
10
- let database: DevFlowDatabase;
11
-
12
- beforeEach(() => {
13
- directory = mkdtempSync(join(tmpdir(), 'devflow-semantic-resolution-'));
14
- database = new DevFlowDatabase(directory);
15
- });
16
-
17
- afterEach(() => {
18
- database.close();
19
- rmSync(directory, { recursive: true, force: true });
20
- });
21
-
22
- it('appends, lists, and returns the latest immutable revision', () => {
23
- const provisional = resolution();
24
- const resolved = resolution({
25
- frameHash: 'b'.repeat(64),
26
- artifactHash: 'artifact:2',
27
- artifactRevision: 2,
28
- supersedesFrameHash: provisional.frameHash,
29
- state: 'resolved',
30
- frame: { frameHash: 'b'.repeat(64), state: 'resolved' },
31
- createdAt: 2,
32
- });
33
- expect(database.appendSemanticResolution(provisional)).toBe(true);
34
- expect(database.appendSemanticResolution(resolved)).toBe(true);
35
- expect(database.listSemanticResolutions('/project', 'session:1', 'turn:1'))
36
- .toEqual([provisional, resolved]);
37
- expect(database.getLatestSemanticResolution('/project', 'session:1', 'turn:1'))
38
- .toEqual(resolved);
39
- });
40
-
41
- it('makes identical replay idempotent and rejects hash conflicts', () => {
42
- const record = resolution();
43
- expect(database.appendSemanticResolution(record)).toBe(true);
44
- expect(database.appendSemanticResolution(record)).toBe(false);
45
- expect(() => database.appendSemanticResolution({
46
- ...record,
47
- durationMs: 9,
48
- })).toThrow(`SEMANTIC_RESOLUTION_HASH_CONFLICT:${record.frameHash}`);
49
- });
50
-
51
- it('rejects another frame at the same artifact revision', () => {
52
- const record = resolution();
53
- database.appendSemanticResolution(record);
54
- expect(() => database.appendSemanticResolution({
55
- ...record,
56
- frameHash: 'c'.repeat(64),
57
- frame: { frameHash: 'c'.repeat(64), state: 'provisional' },
58
- })).toThrow('SEMANTIC_RESOLUTION_REVISION_CONFLICT:turn:1:1');
59
- });
60
- });
61
-
62
- function resolution(overrides: Partial<SemanticResolutionRecord> = {}): SemanticResolutionRecord {
63
- const frameHash = overrides.frameHash ?? 'a'.repeat(64);
64
- return {
65
- frameHash,
66
- projectRoot: '/project',
67
- projectId: 'project:1',
68
- hostId: 'claude-code',
69
- sessionId: 'session:1',
70
- turnId: 'turn:1',
71
- requestId: 'request:1',
72
- sourceHash: 'source:1',
73
- artifactHash: 'artifact:1',
74
- artifactRevision: 1,
75
- state: 'provisional',
76
- generatedBy: 'deterministic',
77
- routeCatalogVersion: 'catalog.v1',
78
- thresholdVersion: 'threshold.v1',
79
- route: { status: 'unavailable' },
80
- sampling: { attempted: false, status: 'not_needed' },
81
- conflicts: [],
82
- frame: { frameHash, state: 'provisional' },
83
- durationMs: 0,
84
- createdAt: 1,
85
- ...overrides,
86
- };
87
- }
@@ -1,456 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { DevFlowDatabase } from '../src/database';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- import { mkdtempSync, rmSync } from 'fs';
6
-
7
- describe('DevFlowDatabase - Skill Executions', () => {
8
- let db: DevFlowDatabase;
9
- let tmpDir: string;
10
-
11
- beforeEach(() => {
12
- tmpDir = mkdtempSync(join(tmpdir(), 'devflow-test-'));
13
- db = new DevFlowDatabase(tmpDir);
14
- });
15
-
16
- afterEach(() => {
17
- db.close();
18
- rmSync(tmpDir, { recursive: true, force: true });
19
- });
20
-
21
- it('should insert and retrieve a skill execution', () => {
22
- db.insertSkillExecution({
23
- executionId: 'exec_001',
24
- skillName: 'devflow:react',
25
- startedAt: Date.now(),
26
- status: 'running',
27
- });
28
-
29
- const execution = db.getSkillExecution('exec_001');
30
- expect(execution).not.toBeNull();
31
- expect(execution!.executionId).toBe('exec_001');
32
- expect(execution!.skillName).toBe('devflow:react');
33
- expect(execution!.status).toBe('running');
34
- expect(execution!.totalToolCalls).toBe(0);
35
- });
36
-
37
- it('should list skill executions with limit', () => {
38
- db.insertSkillExecution({
39
- executionId: 'exec_001',
40
- skillName: 'devflow:react',
41
- startedAt: Date.now() - 1000,
42
- status: 'completed',
43
- });
44
- db.insertSkillExecution({
45
- executionId: 'exec_002',
46
- skillName: 'devflow:vue',
47
- startedAt: Date.now(),
48
- status: 'running',
49
- });
50
-
51
- const executions = db.listSkillExecutions(10);
52
- expect(executions).toHaveLength(2);
53
- // Most recent first
54
- expect(executions[0].executionId).toBe('exec_002');
55
- });
56
-
57
- it('should update skill execution on complete', () => {
58
- db.insertSkillExecution({
59
- executionId: 'exec_001',
60
- skillName: 'devflow:react',
61
- startedAt: Date.now() - 5000,
62
- status: 'running',
63
- });
64
-
65
- db.updateSkillExecution('exec_001', {
66
- status: 'completed',
67
- finishedAt: Date.now(),
68
- totalDuration: 5000,
69
- totalTokens: 1500,
70
- mcpComplianceRate: 85.5,
71
- });
72
-
73
- const execution = db.getSkillExecution('exec_001');
74
- expect(execution!.status).toBe('completed');
75
- expect(execution!.totalDuration).toBe(5000);
76
- expect(execution!.mcpComplianceRate).toBe(85.5);
77
- });
78
-
79
- it('should insert and list tool call events', () => {
80
- db.insertSkillExecution({
81
- executionId: 'exec_001',
82
- skillName: 'devflow:react',
83
- startedAt: Date.now(),
84
- status: 'running',
85
- });
86
-
87
- db.insertToolCallEvent({
88
- eventId: 'evt_001',
89
- executionId: 'exec_001',
90
- timestamp: Date.now(),
91
- toolName: 'mcp__devflow__react_diagnose_bug',
92
- toolType: 'mcp',
93
- isMcpTool: true,
94
- mcpToolName: 'react_diagnose_bug',
95
- mcpEnforced: true,
96
- mcpFallback: false,
97
- input: { bug: 'white screen' },
98
- tokensUsed: 500,
99
- duration: 2000,
100
- blocked: false,
101
- });
102
-
103
- db.insertToolCallEvent({
104
- eventId: 'evt_002',
105
- executionId: 'exec_001',
106
- timestamp: Date.now() + 100,
107
- toolName: 'Read',
108
- toolType: 'direct',
109
- isMcpTool: false,
110
- mcpEnforced: false,
111
- mcpFallback: true,
112
- input: { file_path: '/src/App.tsx' },
113
- tokensUsed: 100,
114
- duration: 50,
115
- blocked: false,
116
- });
117
-
118
- const events = db.listToolCallEvents('exec_001');
119
- expect(events).toHaveLength(2);
120
- expect(events[0].toolName).toBe('mcp__devflow__react_diagnose_bug');
121
- expect(events[0].isMcpTool).toBe(true);
122
- expect(events[1].toolName).toBe('Read');
123
- expect(events[1].isMcpTool).toBe(false);
124
- });
125
-
126
- it('should filter executions by skill name', () => {
127
- db.insertSkillExecution({
128
- executionId: 'exec_001',
129
- skillName: 'devflow:react',
130
- startedAt: Date.now(),
131
- status: 'completed',
132
- });
133
- db.insertSkillExecution({
134
- executionId: 'exec_002',
135
- skillName: 'devflow:vue',
136
- startedAt: Date.now(),
137
- status: 'completed',
138
- });
139
-
140
- const reactExecs = db.listSkillExecutions(10, 'devflow:react');
141
- expect(reactExecs).toHaveLength(1);
142
- expect(reactExecs[0].skillName).toBe('devflow:react');
143
- });
144
-
145
- it('computes required MCP obligation compliance independently of direct calls', () => {
146
- // React requires successful context plus at least one React domain tool.
147
- db.insertSkillExecution({
148
- executionId: 'exec_001',
149
- skillName: 'devflow:react',
150
- startedAt: Date.now(),
151
- status: 'completed',
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: {}, output: { files: ['src/App.tsx'] }, duration: 20, blocked: false,
158
- });
159
- db.insertToolCallEvent({
160
- eventId: 'evt_001', executionId: 'exec_001', timestamp: Date.now(),
161
- toolName: 'mcp__devflow__react_diagnose_bug', toolType: 'mcp',
162
- isMcpTool: true, mcpEnforced: true, mcpFallback: false,
163
- input: {}, output: { findings: [{ id: 'finding-1' }] }, tokensUsed: 100, duration: 500, blocked: false,
164
- });
165
- db.insertToolCallEvent({
166
- eventId: 'evt_002', executionId: 'exec_001', timestamp: Date.now(),
167
- toolName: 'mcp__devflow__react_review_hooks', toolType: 'mcp',
168
- isMcpTool: true, mcpEnforced: true, mcpFallback: false,
169
- input: {}, output: { findings: [{ id: 'finding-2' }] }, tokensUsed: 100, duration: 500, blocked: false,
170
- });
171
- db.insertToolCallEvent({
172
- eventId: 'evt_003', executionId: 'exec_001', timestamp: Date.now(),
173
- toolName: 'Read', toolType: 'direct',
174
- isMcpTool: false, mcpEnforced: false, mcpFallback: false,
175
- input: {}, tokensUsed: 50, duration: 100, blocked: false,
176
- });
177
-
178
- // Vue has the same context + domain obligations.
179
- db.insertSkillExecution({
180
- executionId: 'exec_002',
181
- skillName: 'devflow:vue',
182
- startedAt: Date.now(),
183
- status: 'completed',
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: {}, output: { files: ['src/App.vue'] }, duration: 20, blocked: false,
190
- });
191
- db.insertToolCallEvent({
192
- eventId: 'evt_004', executionId: 'exec_002', timestamp: Date.now(),
193
- toolName: 'mcp__devflow__vue_diagnose_bug', toolType: 'mcp',
194
- isMcpTool: true, mcpEnforced: true, mcpFallback: false,
195
- input: {}, output: { findings: [{ id: 'finding-vue' }] }, tokensUsed: 100, duration: 500, blocked: false,
196
- });
197
-
198
- const compliance = db.getMcpCompliance();
199
- expect(compliance.overall).toBe(100);
200
- expect(compliance.bySkill['devflow:react']).toBe(100);
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);
205
- });
206
-
207
- it('persists missed tools, actual failures, and fallback reasons on reconciliation', () => {
208
- db.ensureSession({ id: 'session-facts', projectRoot: '/project', startedAt: 100 });
209
- db.insertSkillExecution({
210
- executionId: 'exec-facts',
211
- sessionId: 'session-facts',
212
- skillName: 'devflow:react',
213
- startedAt: 100,
214
- status: 'running',
215
- });
216
- db.insertToolCallEvent({
217
- eventId: 'event-failed', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 150,
218
- toolName: 'mcp__devflow__get_project_context', toolType: 'mcp', isMcpTool: true,
219
- mcpToolName: 'get_project_context', mcpEnforced: true, mcpFallback: false,
220
- input: {}, error: 'route failed', duration: 20, blocked: false,
221
- });
222
- db.insertToolCallEvent({
223
- eventId: 'event-fallback', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 160,
224
- toolName: 'Read', toolType: 'direct', isMcpTool: false, mcpEnforced: false,
225
- mcpFallback: true, input: {}, output: { content: 'fallback' }, duration: 10, blocked: false,
226
- });
227
- db.insertToolCallEvent({
228
- eventId: 'event-memory-receipt', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 165,
229
- toolName: 'mcp__devflow__memory_commit_turn', toolType: 'mcp', isMcpTool: true,
230
- mcpToolName: 'memory_commit_turn', mcpEnforced: true, mcpFallback: false,
231
- input: {}, output: { receiptId: 'memory-receipt:host-commit' }, duration: 5, blocked: false,
232
- });
233
- db.insertToolCallEvent({
234
- eventId: 'event-distill-receipt', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 166,
235
- toolName: 'mcp__devflow__memory_save_distilled', toolType: 'mcp', isMcpTool: true,
236
- mcpToolName: 'memory_save_distilled', mcpEnforced: true, mcpFallback: false,
237
- input: {}, output: { receipt: { receiptId: 'distill-receipt:batch-1' } }, duration: 5, blocked: false,
238
- });
239
- db.insertHookFallback({
240
- id: 'hook-fallback-1', projectRoot: '/project', sessionId: 'session-facts',
241
- requestType: 'post-tool-use', tool: 'Read', reason: 'timeout', durationMs: 1_000,
242
- attempts: 1, createdAt: 170,
243
- });
244
-
245
- db.reconcileSkillExecution('exec-facts', 'completed', 200, {
246
- missedTools: [],
247
- actualFailureCount: 0,
248
- blockedCount: 99,
249
- fallbackCount: 0,
250
- fallbackReasons: ['stale_supplied_reason'],
251
- evidenceContractOutcomes: [{ status: 'resolved' }],
252
- });
253
-
254
- expect(db.getSkillExecution('exec-facts')).toMatchObject({
255
- missedMcpTools: ['react:context', 'react:domain'],
256
- failedToolCalls: 1,
257
- blockedToolCalls: 0,
258
- fallbackCount: 2,
259
- fallbackReasons: ['direct_tool_during_context', 'daemon_timeout'],
260
- metadata: expect.objectContaining({
261
- missedTools: ['react:context', 'react:domain'],
262
- actualFailureCount: 1,
263
- blockedCount: 0,
264
- fallbackCount: 2,
265
- fallbackReasons: ['direct_tool_during_context', 'daemon_timeout'],
266
- memoryReceiptIds: ['memory-receipt:host-commit'],
267
- distillReceiptIds: ['distill-receipt:batch-1'],
268
- evidenceContractOutcomes: [{ status: 'resolved' }],
269
- }),
270
- });
271
- });
272
-
273
- it('unions evidence metadata across retries without allowing stale counters', () => {
274
- db.ensureSession({ id: 'session-evidence-retry', projectRoot: '/project', startedAt: 100 });
275
- db.insertSkillExecution({
276
- executionId: 'exec-evidence-retry',
277
- sessionId: 'session-evidence-retry',
278
- skillName: 'devflow:context',
279
- startedAt: 100,
280
- status: 'running',
281
- });
282
- db.insertToolCallEvent({
283
- eventId: 'event-context', executionId: 'exec-evidence-retry',
284
- sessionId: 'session-evidence-retry', timestamp: 120,
285
- toolName: 'get_project_context', toolType: 'mcp', isMcpTool: true,
286
- mcpToolName: 'get_project_context', mcpEnforced: true, mcpFallback: false,
287
- input: {}, output: { files: ['src/App.tsx'] }, duration: 10, blocked: false,
288
- });
289
- const degradation = {
290
- obligationId: 'obligation-1', contractId: 'contract-1', reason: 'bounded_fail_open',
291
- degradedAt: 150, attempt: 2,
292
- };
293
- const outcome = {
294
- obligationId: 'obligation-1', contractId: 'contract-1', status: 'degraded',
295
- completedAt: 150, attempt: 2, reason: 'bounded_fail_open',
296
- };
297
-
298
- db.reconcileSkillExecution('exec-evidence-retry', 'completed', 200, {
299
- evidenceDegradations: [degradation],
300
- evidenceContractOutcomes: [outcome],
301
- pendingEvidenceContractIds: ['contract-pending'],
302
- });
303
- db.reconcileSkillExecution('exec-evidence-retry', 'completed', 220, {
304
- evidenceDegradations: [],
305
- evidenceContractOutcomes: [
306
- { attempt: 2, completedAt: 150, reason: 'bounded_fail_open', status: 'degraded',
307
- contractId: 'contract-1', obligationId: 'obligation-1' },
308
- ],
309
- pendingEvidenceContractIds: [],
310
- missedTools: ['stale'],
311
- actualFailureCount: 99,
312
- });
313
-
314
- expect(db.getSkillExecution('exec-evidence-retry')?.metadata).toMatchObject({
315
- evidenceDegradations: [degradation],
316
- evidenceContractOutcomes: [outcome],
317
- pendingEvidenceContractIds: ['contract-pending'],
318
- missedTools: [],
319
- actualFailureCount: 0,
320
- });
321
- });
322
-
323
- it('aggregates an empty findings collection as an empty result', () => {
324
- db.insertSkillExecution({
325
- executionId: 'exec_empty_findings',
326
- sessionId: 'session_empty_findings',
327
- skillName: 'devflow:react',
328
- startedAt: 1_000,
329
- status: 'running',
330
- });
331
- db.insertToolCallEvent({
332
- eventId: 'evt_empty_findings',
333
- executionId: 'exec_empty_findings',
334
- sessionId: 'session_empty_findings',
335
- timestamp: 2_000,
336
- toolName: 'react_review_hooks',
337
- toolType: 'mcp',
338
- isMcpTool: true,
339
- mcpToolName: 'react_review_hooks',
340
- mcpEnforced: true,
341
- mcpFallback: false,
342
- input: { query: 'review hooks' },
343
- output: { findings: [] },
344
- duration: 25,
345
- blocked: false,
346
- });
347
-
348
- expect(db.aggregatePendingToolMetrics()).toBe(1);
349
- expect(db.getToolMetrics('session_empty_findings')).toEqual([
350
- expect.objectContaining({ status: 'empty', result_count: 0 }),
351
- ]);
352
- });
353
-
354
- it.each([
355
- ['success false', { success: false }],
356
- ['degraded data', { success: true, data: { degraded: true } }],
357
- ])('aggregates %s as degraded', (_caseName, output) => {
358
- const suffix = output.success === false ? 'success_false' : 'data_degraded';
359
- const executionId = `exec_${suffix}`;
360
- const sessionId = `session_${suffix}`;
361
- db.insertSkillExecution({
362
- executionId,
363
- sessionId,
364
- skillName: 'devflow:react',
365
- startedAt: 1_000,
366
- status: 'running',
367
- });
368
- db.insertToolCallEvent({
369
- eventId: `evt_${suffix}`,
370
- executionId,
371
- sessionId,
372
- timestamp: 2_000,
373
- toolName: 'react_audit_performance',
374
- toolType: 'mcp',
375
- isMcpTool: true,
376
- mcpToolName: 'react_audit_performance',
377
- mcpEnforced: true,
378
- mcpFallback: false,
379
- input: {},
380
- output,
381
- duration: 25,
382
- blocked: false,
383
- });
384
-
385
- expect(db.aggregatePendingToolMetrics()).toBe(1);
386
- expect(db.getToolMetrics(sessionId)).toEqual([
387
- expect.objectContaining({ status: 'degraded' }),
388
- ]);
389
- });
390
-
391
- it('reconciles execution duration from wall-clock start to finish', () => {
392
- db.insertSkillExecution({
393
- executionId: 'exec_wall_clock',
394
- skillName: 'devflow:react',
395
- startedAt: 1_000,
396
- status: 'running',
397
- });
398
- db.insertToolCallEvent({
399
- eventId: 'evt_wall_clock',
400
- executionId: 'exec_wall_clock',
401
- timestamp: 4_000,
402
- toolName: 'react_review_hooks',
403
- toolType: 'mcp',
404
- isMcpTool: true,
405
- mcpToolName: 'react_review_hooks',
406
- mcpEnforced: true,
407
- mcpFallback: false,
408
- input: {},
409
- output: { findings: [] },
410
- duration: 100,
411
- blocked: false,
412
- });
413
-
414
- db.reconcileSkillExecution('exec_wall_clock', 'completed', 11_000);
415
-
416
- expect(db.getSkillExecution('exec_wall_clock')).toEqual(
417
- expect.objectContaining({
418
- status: 'completed',
419
- finishedAt: 11_000,
420
- totalDuration: 10_000,
421
- }),
422
- );
423
- });
424
-
425
- it('reconciles every running execution owned by a closing session', () => {
426
- for (const executionId of ['exec_implicit_a', 'exec_implicit_b']) {
427
- db.insertSkillExecution({
428
- executionId,
429
- sessionId: 'session-closing',
430
- skillName: 'devflow:implicit',
431
- startedAt: 1_000,
432
- status: 'running',
433
- });
434
- }
435
- db.insertSkillExecution({
436
- executionId: 'exec_other_session',
437
- sessionId: 'session-other',
438
- skillName: 'devflow:implicit',
439
- startedAt: 1_000,
440
- status: 'running',
441
- });
442
-
443
- const reconciled = db.reconcileRunningSkillExecutionsForSession(
444
- 'session-closing',
445
- 5_000,
446
- { closureReason: 'session_finalize' },
447
- );
448
-
449
- expect(reconciled.map(execution => execution.executionId)).toEqual([
450
- 'exec_implicit_a',
451
- 'exec_implicit_b',
452
- ]);
453
- expect(reconciled.every(execution => execution.status === 'completed')).toBe(true);
454
- expect(db.getSkillExecution('exec_other_session')).toMatchObject({ status: 'running' });
455
- });
456
- });