@devflow-tools/database 0.17.8 → 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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/dist/data/LocalDataProvider.d.ts +63 -0
  3. package/dist/data/LocalDataProvider.js +332 -0
  4. package/dist/data/default-enforcer-rules.d.ts +3 -0
  5. package/dist/data/default-enforcer-rules.js +170 -0
  6. package/dist/data/devflow-schema.d.ts +2 -0
  7. package/dist/data/devflow-schema.js +80 -0
  8. package/dist/database.d.ts +45 -7
  9. package/dist/database.js +583 -125
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.js +11 -5
  12. package/dist/retrieval-ledger.d.ts +16 -1
  13. package/dist/retrieval-ledger.js +6 -0
  14. package/dist/retrieval-runtime.d.ts +39 -0
  15. package/dist/retrieval-runtime.js +2 -0
  16. package/dist/task-aggregate.d.ts +49 -0
  17. package/dist/task-aggregate.js +2 -0
  18. package/dist/task-semantic-control.d.ts +8 -1
  19. package/dist/task-semantic-control.js +2 -4
  20. package/package.json +15 -3
  21. package/CHANGELOG.md +0 -844
  22. package/__tests__/database.failure-category.test.ts +0 -44
  23. package/__tests__/database.host-actions.test.ts +0 -405
  24. package/__tests__/database.learning-candidates.test.ts +0 -93
  25. package/__tests__/database.memory-turn-receipts.test.ts +0 -98
  26. package/__tests__/database.retrieval-ledger.test.ts +0 -68
  27. package/__tests__/database.retrieval-sessions.test.ts +0 -79
  28. package/__tests__/database.semantic-resolution.test.ts +0 -87
  29. package/__tests__/database.skill-executions.test.ts +0 -456
  30. package/__tests__/database.task-runtime.test.ts +0 -73
  31. package/__tests__/database.test.ts +0 -177
  32. package/__tests__/database.work-queue.test.ts +0 -274
  33. package/__tests__/database.workflow-workers.test.ts +0 -60
  34. package/__tests__/node-sqlite.test.ts +0 -68
  35. package/src/database.ts +0 -5853
  36. package/src/host-actions.ts +0 -211
  37. package/src/index.ts +0 -153
  38. package/src/learning-candidates.ts +0 -181
  39. package/src/node-sqlite.ts +0 -60
  40. package/src/obligation-ledger.ts +0 -57
  41. package/src/retrieval-ledger.ts +0 -35
  42. package/src/retrieval-sessions.ts +0 -196
  43. package/src/semantic-resolution.ts +0 -181
  44. package/src/task-runtime.ts +0 -169
  45. package/src/task-semantic-control.ts +0 -270
  46. package/src/types.ts +0 -17
  47. package/src/work-queue.ts +0 -123
  48. package/src/workflow-workers.ts +0 -198
  49. package/tsconfig.json +0 -19
  50. package/tsconfig.tsbuildinfo +0 -1
  51. package/vitest.config.ts +0 -41
@@ -1,44 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { DevFlowDatabase } from "../src/database.js";
6
-
7
- describe("tool failure categories", () => {
8
- let root: string;
9
- let database: DevFlowDatabase;
10
-
11
- beforeEach(() => {
12
- root = mkdtempSync(join(tmpdir(), "devflow-failure-category-"));
13
- database = new DevFlowDatabase(root);
14
- });
15
-
16
- afterEach(() => {
17
- database.close();
18
- rmSync(root, { recursive: true, force: true });
19
- });
20
-
21
- it("persists and returns failure_category", () => {
22
- database.insertToolCallEvent({
23
- eventId: "event-1",
24
- executionId: "execution-1",
25
- sessionId: "session-1",
26
- timestamp: Date.now(),
27
- toolName: "Bash",
28
- toolType: "direct",
29
- isMcpTool: false,
30
- mcpEnforced: false,
31
- mcpFallback: false,
32
- input: { command: "false" },
33
- duration: 1,
34
- error: "exit 1",
35
- blocked: false,
36
- failureCategory: "tool_error",
37
- });
38
-
39
- expect(database.listToolCallEvents("execution-1")[0])
40
- .toMatchObject({ failureCategory: "tool_error", error: "exit 1" });
41
- expect(database.listToolCallEventsBySession("session-1")[0])
42
- .toMatchObject({ failureCategory: "tool_error" });
43
- });
44
- });
@@ -1,405 +0,0 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { Worker } from 'node:worker_threads';
5
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
- import { DevFlowDatabase } from '../src/database';
7
- import type { FailHostActionInput, VerifyHostActionInput } from '../src/host-actions';
8
-
9
- describe('DevFlowDatabase durable host actions', () => {
10
- let directory: string;
11
- let database: DevFlowDatabase;
12
- const projectRoot = '/project-a';
13
-
14
- beforeEach(() => {
15
- directory = mkdtempSync(join(tmpdir(), 'devflow-host-actions-'));
16
- database = new DevFlowDatabase(directory);
17
- });
18
-
19
- afterEach(() => {
20
- database.close();
21
- rmSync(directory, { recursive: true, force: true });
22
- });
23
-
24
- function request(actionId: string, stepId = 'execute') {
25
- return database.requestHostAction({
26
- actionId,
27
- runId: 'telemetry-run-1',
28
- engineRunId: 'engine-run-1',
29
- stepId,
30
- projectRoot,
31
- sessionId: 'session-1',
32
- executionId: 'execution-1',
33
- contextReceipt: 'context-1',
34
- });
35
- }
36
-
37
- function eventCount(actionId: string): number {
38
- return Number((database.get(
39
- 'SELECT COUNT(*) AS count FROM devflow_host_action_events WHERE action_id = ?',
40
- actionId,
41
- ) as { count: number }).count);
42
- }
43
-
44
- it('persists the full waiting to running to reported to verified flow', () => {
45
- expect(request('action-full')).toMatchObject({
46
- actionId: 'action-full',
47
- runId: 'telemetry-run-1',
48
- engineRunId: 'engine-run-1',
49
- state: 'waiting',
50
- report: {},
51
- finishedAt: undefined,
52
- });
53
- expect(database.startHostAction({ actionId: 'action-full', projectRoot })).toMatchObject({
54
- state: 'running',
55
- });
56
- expect(database.reportHostAction({
57
- actionId: 'action-full',
58
- projectRoot,
59
- report: { files: ['src/index.ts'], exitCode: 0 },
60
- evidenceHash: 'execution-hash',
61
- })).toMatchObject({ state: 'reported', evidenceHash: 'execution-hash' });
62
- const verified = database.verifyHostAction({
63
- actionId: 'action-full',
64
- projectRoot,
65
- report: { command: 'npm test', exitCode: 0 },
66
- evidenceHash: 'verification-hash',
67
- });
68
-
69
- expect(verified).toMatchObject({
70
- state: 'verified',
71
- report: { command: 'npm test', exitCode: 0 },
72
- evidenceHash: 'verification-hash',
73
- });
74
- expect(verified.finishedAt).toEqual(expect.any(Number));
75
- expect(eventCount('action-full')).toBe(4);
76
- });
77
-
78
- it('supports a direct waiting to reported transition for MCP reports', () => {
79
- request('action-direct');
80
- expect(database.reportHostAction({
81
- actionId: 'action-direct',
82
- projectRoot,
83
- report: { toolCalls: ['write_file'] },
84
- evidenceHash: 'direct-hash',
85
- })).toMatchObject({ state: 'reported', finishedAt: undefined });
86
- expect(eventCount('action-direct')).toBe(2);
87
- });
88
-
89
- it('rolls back invalid transitions without appending an event', () => {
90
- request('action-invalid');
91
- const before = eventCount('action-invalid');
92
-
93
- expect(() => database.verifyHostAction({
94
- actionId: 'action-invalid',
95
- projectRoot,
96
- report: { command: 'npm test', exitCode: 0 },
97
- evidenceHash: 'verify-hash',
98
- })).toThrow('HOST_ACTION_INVALID_TRANSITION:waiting->verified');
99
- expect(database.getHostAction(projectRoot, 'action-invalid')?.state).toBe('waiting');
100
- expect(eventCount('action-invalid')).toBe(before);
101
- });
102
-
103
- it('makes identical terminal reports idempotent and rejects changed duplicates', () => {
104
- request('action-terminal');
105
- database.reportHostAction({
106
- actionId: 'action-terminal',
107
- projectRoot,
108
- report: { exitCode: 0 },
109
- evidenceHash: 'execution-hash',
110
- });
111
- const input = {
112
- actionId: 'action-terminal',
113
- projectRoot,
114
- report: { summary: 'passed', exitCode: 0 },
115
- evidenceHash: 'terminal-hash',
116
- };
117
- const terminal = database.verifyHostAction(input);
118
- const before = eventCount('action-terminal');
119
-
120
- expect(database.verifyHostAction({
121
- ...input,
122
- report: { exitCode: 0, summary: 'passed' },
123
- })).toEqual(terminal);
124
- expect(eventCount('action-terminal')).toBe(before);
125
- expect(() => database.verifyHostAction({
126
- ...input,
127
- evidenceHash: 'changed-hash',
128
- })).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
129
- expect(() => database.verifyHostAction({
130
- ...input,
131
- report: { summary: 'different', exitCode: 0 },
132
- })).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
133
- expect(eventCount('action-terminal')).toBe(before);
134
- });
135
-
136
- it.each(['failed', 'cancelled', 'degraded'] as const)(
137
- 'reaches %s with a completion timestamp',
138
- outcome => {
139
- const actionId = `action-${outcome}`;
140
- request(actionId, outcome);
141
- const terminal = database.failHostAction({
142
- actionId,
143
- projectRoot,
144
- outcome,
145
- report: { reason: `${outcome} by host` },
146
- evidenceHash: `${outcome}-hash`,
147
- });
148
-
149
- expect(terminal.state).toBe(outcome);
150
- expect(terminal.finishedAt).toEqual(expect.any(Number));
151
- expect(eventCount(actionId)).toBe(2);
152
- },
153
- );
154
-
155
- it('rejects request identity conflicts instead of merging metadata', () => {
156
- request('action-identity');
157
- const identical = request('action-identity');
158
- expect(identical.actionId).toBe('action-identity');
159
- expect(eventCount('action-identity')).toBe(1);
160
-
161
- expect(() => database.requestHostAction({
162
- actionId: 'different-action',
163
- runId: 'telemetry-run-1',
164
- engineRunId: 'different-engine-run',
165
- stepId: 'execute',
166
- projectRoot,
167
- sessionId: 'different-session',
168
- executionId: 'execution-1',
169
- contextReceipt: 'context-1',
170
- })).toThrow('HOST_ACTION_IDENTITY_CONFLICT:telemetry-run-1:execute');
171
- expect(database.all('SELECT action_id FROM devflow_host_actions')).toHaveLength(1);
172
- });
173
-
174
- it('scopes action and run reads by project root', () => {
175
- request('action-isolated');
176
-
177
- expect(database.getHostAction('/project-b', 'action-isolated')).toBeNull();
178
- expect(database.listHostActionsForRun('/project-b', 'telemetry-run-1')).toEqual([]);
179
- expect(database.listHostActionsForRun(projectRoot, 'telemetry-run-1')).toHaveLength(1);
180
- expect(() => database.startHostAction({
181
- actionId: 'action-isolated',
182
- projectRoot: '/project-b',
183
- })).toThrow('HOST_ACTION_NOT_FOUND:action-isolated');
184
- });
185
-
186
- it('keeps rows and schema stable across close and reopen', () => {
187
- request('action-durable');
188
- database.reportHostAction({
189
- actionId: 'action-durable',
190
- projectRoot,
191
- report: { result: 'observed' },
192
- evidenceHash: 'durable-hash',
193
- });
194
- const schemaBefore = database.all(`
195
- SELECT type, name, sql FROM sqlite_master
196
- WHERE name LIKE '%host_action%'
197
- ORDER BY type, name
198
- `);
199
- database.close();
200
- database = new DevFlowDatabase(directory);
201
-
202
- expect(database.getHostAction(projectRoot, 'action-durable')).toMatchObject({
203
- state: 'reported',
204
- report: { result: 'observed' },
205
- evidenceHash: 'durable-hash',
206
- });
207
- expect(eventCount('action-durable')).toBe(2);
208
- expect(database.all(`
209
- SELECT type, name, sql FROM sqlite_master
210
- WHERE name LIKE '%host_action%'
211
- ORDER BY type, name
212
- `)).toEqual(schemaBefore);
213
- });
214
-
215
- it('uses UUID text event IDs and enforces append-only event rows', () => {
216
- request('action-events');
217
- const [event] = database.all(`
218
- SELECT event_id FROM devflow_host_action_events WHERE action_id = ?
219
- `, 'action-events') as Array<{ event_id: string }>;
220
-
221
- expect(event?.event_id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
222
- expect(() => database.all(
223
- 'UPDATE devflow_host_action_events SET evidence_hash = ? WHERE event_id = ?',
224
- 'changed',
225
- event!.event_id,
226
- )).toThrow('append-only');
227
- });
228
-
229
- it('rejects INSERT OR REPLACE attempts without mutating an event', () => {
230
- request('action-replace');
231
- const [event] = database.all(`
232
- SELECT * FROM devflow_host_action_events WHERE action_id = ?
233
- `, 'action-replace') as Array<Record<string, unknown>>;
234
-
235
- expect(() => database.all(`
236
- INSERT OR REPLACE INTO devflow_host_action_events (
237
- event_id, action_id, project_root, run_id, step_id, from_state,
238
- to_state, report_json, evidence_hash, created_at
239
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
240
- `,
241
- event!.event_id,
242
- event!.action_id,
243
- event!.project_root,
244
- event!.run_id,
245
- event!.step_id,
246
- event!.from_state,
247
- event!.to_state,
248
- event!.report_json,
249
- 'replacement-hash',
250
- event!.created_at,
251
- )).toThrow('append-only');
252
-
253
- expect(database.get(
254
- 'SELECT evidence_hash FROM devflow_host_action_events WHERE event_id = ?',
255
- event!.event_id,
256
- )).toMatchObject({ evidence_hash: null });
257
- });
258
-
259
- it.each([
260
- ['undefined', { value: undefined }],
261
- ['NaN', { value: Number.NaN }],
262
- ['Infinity', { value: Number.POSITIVE_INFINITY }],
263
- ['negative zero', { value: -0 }],
264
- ['function', { value: () => undefined }],
265
- ['bigint', { value: 1n }],
266
- ['symbol', { value: Symbol('value') }],
267
- ['non-plain object', { value: new Date(0) }],
268
- ['sparse array', { value: Array(1) }],
269
- ])('rejects lossy %s report values', (_name, report) => {
270
- request(`action-json-${_name}`);
271
- expect(() => database.reportHostAction({
272
- actionId: `action-json-${_name}`,
273
- projectRoot,
274
- report,
275
- evidenceHash: 'report-hash',
276
- })).toThrow('Host action report contains a non-JSON value');
277
- });
278
-
279
- it('rejects cyclic reports', () => {
280
- request('action-json-cycle');
281
- const report: Record<string, unknown> = {};
282
- report.self = report;
283
-
284
- expect(() => database.reportHostAction({
285
- actionId: 'action-json-cycle',
286
- projectRoot,
287
- report,
288
- evidenceHash: 'report-hash',
289
- })).toThrow('Host action report contains a cycle');
290
- });
291
-
292
- it.each(['not-json', '[]'])(
293
- 'fails closed when persisted report JSON is corrupt: %s',
294
- reportJson => {
295
- request('action-corrupt');
296
- database.all(
297
- 'UPDATE devflow_host_actions SET report_json = ? WHERE action_id = ?',
298
- reportJson,
299
- 'action-corrupt',
300
- );
301
-
302
- expect(() => database.getHostAction(projectRoot, 'action-corrupt'))
303
- .toThrow('Invalid persisted host action report_json');
304
- },
305
- );
306
-
307
- it('requires non-empty verification evidence and report content', () => {
308
- request('action-verification-input');
309
- database.reportHostAction({
310
- actionId: 'action-verification-input',
311
- projectRoot,
312
- report: { output: 'observed' },
313
- evidenceHash: 'execution-hash',
314
- });
315
- const baseInput = {
316
- actionId: 'action-verification-input',
317
- projectRoot,
318
- report: { command: 'npm test', exitCode: 0 },
319
- };
320
-
321
- expect(() => database.verifyHostAction({
322
- ...baseInput,
323
- evidenceHash: ' ',
324
- })).toThrow('Host action verification requires a non-empty evidence hash');
325
- expect(() => database.verifyHostAction(baseInput as VerifyHostActionInput))
326
- .toThrow('Host action verification requires a non-empty evidence hash');
327
- expect(() => database.verifyHostAction({
328
- ...baseInput,
329
- report: {},
330
- evidenceHash: 'verification-hash',
331
- })).toThrow('Host action verification requires a non-empty report');
332
- expect(() => database.failHostAction({
333
- ...baseInput,
334
- outcome: 'verified',
335
- } as unknown as FailHostActionInput)).toThrow('Invalid host action failure outcome');
336
- expect(database.getHostAction(projectRoot, 'action-verification-input')?.state).toBe('reported');
337
- expect(eventCount('action-verification-input')).toBe(2);
338
- });
339
-
340
- it('waits for a competing writer and resolves the committed request idempotently', async () => {
341
- const worker = new Worker(`
342
- const { parentPort, workerData } = require('node:worker_threads');
343
- const { DatabaseSync } = require('node:sqlite');
344
- const db = new DatabaseSync(workerData.dbPath);
345
- db.exec('PRAGMA busy_timeout = 5000');
346
- db.exec('BEGIN IMMEDIATE');
347
- const now = Date.now();
348
- db.prepare(
349
- 'INSERT INTO devflow_host_actions (' +
350
- 'action_id, run_id, engine_run_id, step_id, project_root, session_id, ' +
351
- 'execution_id, context_receipt, state, report_json, evidence_hash, ' +
352
- 'created_at, updated_at, finished_at' +
353
- ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'waiting', '{}', NULL, ?, ?, NULL)",
354
- ).run(
355
- 'action-contended', 'telemetry-run-1', 'engine-run-1', 'execute', '/project-a',
356
- 'session-1', 'execution-1', 'context-1', now, now,
357
- );
358
- db.prepare(
359
- 'INSERT INTO devflow_host_action_events (' +
360
- 'event_id, action_id, project_root, run_id, step_id, from_state, ' +
361
- 'to_state, report_json, evidence_hash, created_at' +
362
- ") VALUES (?, ?, ?, ?, ?, NULL, 'waiting', '{}', NULL, ?)",
363
- ).run(
364
- 'worker-event-contended', 'action-contended', '/project-a',
365
- 'telemetry-run-1', 'execute', now,
366
- );
367
- parentPort.postMessage('locked');
368
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
369
- db.exec('COMMIT');
370
- db.close();
371
- parentPort.postMessage('committed');
372
- `, {
373
- eval: true,
374
- workerData: { dbPath: join(directory, '.devflow', 'devflow.db') },
375
- });
376
- let duplicate: ReturnType<typeof request> | undefined;
377
-
378
- try {
379
- await new Promise<void>((resolve, reject) => {
380
- worker.on('message', message => {
381
- if (message === 'locked') {
382
- try {
383
- duplicate = request('action-contended');
384
- } catch (error) {
385
- reject(error);
386
- }
387
- }
388
- if (message === 'committed') resolve();
389
- });
390
- worker.on('error', reject);
391
- worker.on('exit', code => {
392
- if (code !== 0) reject(new Error(`Host-action lock worker exited with code ${code}`));
393
- });
394
- });
395
-
396
- expect(duplicate).toMatchObject({
397
- actionId: 'action-contended',
398
- state: 'waiting',
399
- });
400
- expect(eventCount('action-contended')).toBe(1);
401
- } finally {
402
- await worker.terminate();
403
- }
404
- });
405
- });
@@ -1,93 +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 } from '../src/index.js';
6
-
7
- describe('governed learning candidates', () => {
8
- const roots: string[] = [];
9
-
10
- afterEach(() => {
11
- for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
12
- });
13
-
14
- it('requires independent evidence, grader receipt, and rolls active candidates back on contradiction', () => {
15
- const root = mkdtempSync(join(tmpdir(), 'devflow-learning-db-'));
16
- roots.push(root);
17
- const database = new DevFlowDatabase(root);
18
- const candidate = database.upsertLearningCandidate({
19
- id: 'candidate:install-flag',
20
- projectRoot: '/project',
21
- scope: 'project',
22
- kind: 'tool_preference',
23
- trigger: { skills: ['devflow:react'], entities: ['--legacy-peer-deps'] },
24
- instruction: 'Use --legacy-peer-deps when installing dependencies.',
25
- confidence: 0.95,
26
- });
27
- expect(candidate.state).toBe('observed');
28
-
29
- for (let index = 1; index <= 3; index += 1) {
30
- database.addLearningCandidateEvidence({
31
- id: `evidence:${index}`,
32
- candidateId: candidate.id,
33
- projectRoot: '/project',
34
- sessionId: `session:${index}`,
35
- sourceType: 'tool_preference',
36
- polarity: 'supporting',
37
- outcome: index <= 2 ? 'positive' : 'unknown',
38
- evidenceHash: `hash:${index}`,
39
- });
40
- }
41
- expect(database.getLearningCandidate(candidate.id)).toMatchObject({
42
- state: 'candidate', supportingSessions: 3, successfulOutcomes: 2,
43
- });
44
- database.transitionLearningCandidate(candidate.id, { target: 'shadow', reason: 'threshold' });
45
- database.transitionLearningCandidate(candidate.id, {
46
- target: 'evaluated', reason: 'pass^3', graderReceipt: 'grader:receipt:12345678',
47
- });
48
- expect(database.transitionLearningCandidate(candidate.id, {
49
- target: 'active', reason: 'automatic project activation',
50
- }).state).toBe('active');
51
-
52
- expect(database.addLearningCandidateEvidence({
53
- id: 'evidence:contradiction',
54
- candidateId: candidate.id,
55
- projectRoot: '/project',
56
- sessionId: 'session:4',
57
- sourceType: 'correction',
58
- polarity: 'contradicting',
59
- outcome: 'positive',
60
- evidenceHash: 'hash:contradiction',
61
- })).toMatchObject({ state: 'shadow', contradictions: 1 });
62
- database.close();
63
- });
64
-
65
- it('versions changed overlays and requires manual approval for global candidates', () => {
66
- const root = mkdtempSync(join(tmpdir(), 'devflow-learning-version-'));
67
- roots.push(root);
68
- const database = new DevFlowDatabase(root);
69
- const id = 'candidate:global';
70
- database.upsertLearningCandidate({
71
- id, projectRoot: '/project', scope: 'global', kind: 'convention',
72
- trigger: {}, instruction: 'Use the first convention.', confidence: 0.8,
73
- });
74
- database.upsertLearningCandidate({
75
- id, projectRoot: '/project', scope: 'global', kind: 'convention',
76
- trigger: {}, instruction: 'Use the corrected convention.', confidence: 0.9,
77
- });
78
- expect(database.listLearningCandidateVersions(id)).toHaveLength(2);
79
- for (let index = 1; index <= 3; index += 1) {
80
- database.addLearningCandidateEvidence({
81
- id: `global:${index}`, candidateId: id, projectRoot: '/project', sessionId: `s:${index}`,
82
- sourceType: 'convention', polarity: 'supporting', outcome: 'positive', evidenceHash: `g:${index}`,
83
- });
84
- }
85
- database.transitionLearningCandidate(id, { target: 'shadow', reason: 'threshold' });
86
- database.transitionLearningCandidate(id, { target: 'evaluated', reason: 'graded', graderReceipt: 'grader:global:12345678' });
87
- expect(() => database.transitionLearningCandidate(id, { target: 'active', reason: 'auto' }))
88
- .toThrow('manual approval');
89
- expect(database.transitionLearningCandidate(id, { target: 'active', reason: 'manual', manualApproval: true }).state)
90
- .toBe('active');
91
- database.close();
92
- });
93
- });
@@ -1,98 +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 memory turn receipts', () => {
8
- let directory: string;
9
- let database: DevFlowDatabase;
10
-
11
- beforeEach(() => {
12
- directory = mkdtempSync(join(tmpdir(), 'devflow-memory-turn-'));
13
- database = new DevFlowDatabase(directory);
14
- });
15
-
16
- afterEach(() => {
17
- database.close();
18
- rmSync(directory, { recursive: true, force: true });
19
- });
20
-
21
- it('commits a pending turn with one canonical receipt', () => {
22
- database.beginMemoryTurn({
23
- turnId: 'turn:1',
24
- projectRoot: '/project',
25
- sessionId: 'session-1',
26
- promptHash: 'hash',
27
- eventId: 'event-1',
28
- createdAt: 100,
29
- });
30
-
31
- const committed = database.commitMemoryTurn({
32
- turnId: 'turn:1',
33
- receiptId: 'memory-receipt:1',
34
- memoryIds: ['memory-1'],
35
- source: 'explicit_intent',
36
- decidedAt: 200,
37
- });
38
-
39
- expect(committed).toMatchObject({
40
- status: 'committed',
41
- receiptId: 'memory-receipt:1',
42
- memoryIds: ['memory-1'],
43
- stopPromptedAt: undefined,
44
- });
45
- expect(database.getPendingMemoryTurn('/project', 'session-1')).toBeNull();
46
- });
47
-
48
- it('marks a pending Stop prompt only once and supports skip receipts', () => {
49
- database.beginMemoryTurn({
50
- turnId: 'turn:2',
51
- projectRoot: '/project',
52
- sessionId: 'session-1',
53
- promptHash: 'hash-2',
54
- eventId: 'event-2',
55
- createdAt: 300,
56
- });
57
-
58
- expect(database.markMemoryTurnStopPrompted('turn:2', 350)).toBe(true);
59
- expect(database.markMemoryTurnStopPrompted('turn:2', 360)).toBe(false);
60
- expect(database.skipMemoryTurn({
61
- turnId: 'turn:2',
62
- receiptId: 'memory-receipt:2',
63
- reason: 'transient request',
64
- decidedAt: 400,
65
- })).toMatchObject({
66
- status: 'skipped',
67
- reason: 'transient request',
68
- stopPromptedAt: 350,
69
- });
70
- });
71
-
72
- it('lists every session turn in stable ascending order for reconciliation', () => {
73
- for (let index = 0; index < 125; index += 1) {
74
- database.beginMemoryTurn({
75
- turnId: `turn:${String(index).padStart(3, '0')}`,
76
- projectRoot: '/project',
77
- sessionId: 'long-session',
78
- promptHash: `hash-${index}`,
79
- eventId: `event-${index}`,
80
- createdAt: 1_000 + index,
81
- });
82
- }
83
- database.beginMemoryTurn({
84
- turnId: 'turn:other',
85
- projectRoot: '/project',
86
- sessionId: 'other-session',
87
- promptHash: 'other',
88
- eventId: 'event-other',
89
- createdAt: 1,
90
- });
91
-
92
- const turns = database.listSessionMemoryTurnsForReconciliation('/project', 'long-session');
93
- expect(turns).toHaveLength(125);
94
- expect(turns[0]?.turnId).toBe('turn:000');
95
- expect(turns.at(-1)?.turnId).toBe('turn:124');
96
- expect(turns.every((turn, index) => turn.createdAt === 1_000 + index)).toBe(true);
97
- });
98
- });