@devflow-tools/database 0.16.21 → 0.16.23

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,29 @@
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.23](https://github.com/shilongfeicool/dev-flow/compare/v0.16.22...v0.16.23) (2026-07-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * **database:** persist workflow worker lifecycle ([6f57fc7](https://github.com/shilongfeicool/dev-flow/commit/6f57fc7d7f13915b4f9b7faaa5e37c1a04182cad))
12
+ * **workflow:** execute durable worker lifecycle ([799cc9d](https://github.com/shilongfeicool/dev-flow/commit/799cc9d341b6d4251717e20665654809ed84e29d))
13
+
14
+
15
+
16
+
17
+
18
+ ## [0.16.22](https://github.com/shilongfeicool/dev-flow/compare/v0.16.21...v0.16.22) (2026-07-28)
19
+
20
+
21
+ ### Features
22
+
23
+ * **learning:** add governed overlays and evaluation ([795747d](https://github.com/shilongfeicool/dev-flow/commit/795747db10b3f13dfe5b442e8332673084774d5e))
24
+
25
+
26
+
27
+
28
+
6
29
  ## [0.16.21](https://github.com/shilongfeicool/dev-flow/compare/v0.16.20...v0.16.21) (2026-07-28)
7
30
 
8
31
 
@@ -0,0 +1,93 @@
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
+ });
@@ -0,0 +1,60 @@
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/database.js';
6
+
7
+ const roots: string[] = [];
8
+
9
+ function open(): { db: DevFlowDatabase; root: string } {
10
+ const root = mkdtempSync(join(tmpdir(), 'devflow-workers-'));
11
+ roots.push(root);
12
+ return { db: new DevFlowDatabase(root), root };
13
+ }
14
+
15
+ afterEach(() => {
16
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
17
+ });
18
+
19
+ describe('workflow worker persistence', () => {
20
+ it('persists legal transitions and refuses terminal mutation', () => {
21
+ const { db, root } = open();
22
+ const worker = db.createWorkflowWorker({
23
+ runId: 'run-1', stepId: 'step-1', projectRoot: root,
24
+ branch: 'devflow/run-1/step-1', worktreePath: join(root, '.worker'),
25
+ host: 'codex', contextReceipt: 'receipt-1', baseRevision: 'a'.repeat(40),
26
+ });
27
+ db.leaseWorkflowWorker(root, worker.workerId, 'owner', 1_000, 100);
28
+ db.transitionWorkflowWorker({ projectRoot: root, workerId: worker.workerId, target: 'starting', leaseOwner: 'owner', now: 110 });
29
+ db.transitionWorkflowWorker({ projectRoot: root, workerId: worker.workerId, target: 'running', leaseOwner: 'owner', now: 120 });
30
+ db.transitionWorkflowWorker({ projectRoot: root, workerId: worker.workerId, target: 'failed', leaseOwner: 'owner', reason: 'test', now: 130 });
31
+ expect(() => db.transitionWorkflowWorker({ projectRoot: root, workerId: worker.workerId, target: 'hold' }))
32
+ .toThrow(/TERMINAL_CONFLICT/);
33
+ expect(db.listWorkflowWorkerEvents(root, worker.workerId)).toHaveLength(5);
34
+ db.close();
35
+ });
36
+
37
+ it('recovers expired leases to hold without retrying the worktree', () => {
38
+ const { db, root } = open();
39
+ const worker = db.createWorkflowWorker({
40
+ runId: 'run-2', stepId: 'step-2', projectRoot: root,
41
+ branch: 'devflow/run-2/step-2', worktreePath: join(root, '.worker-2'),
42
+ host: 'claude-code', contextReceipt: 'receipt-2', baseRevision: 'b'.repeat(40),
43
+ });
44
+ db.leaseWorkflowWorker(root, worker.workerId, 'owner', 10, 100);
45
+ expect(db.recoverExpiredWorkflowWorkers(root, 111)[0]?.state).toBe('hold');
46
+ db.close();
47
+ });
48
+
49
+ it('keeps merge queue insertion idempotent', () => {
50
+ const { db, root } = open();
51
+ const input = {
52
+ workerId: 'worker-3', projectRoot: root, baseRevision: 'a', parentRevision: 'b',
53
+ candidateRevision: 'c', graderReceipts: ['grader:1'], simulationHash: 'sha256:1',
54
+ };
55
+ const first = db.enqueueWorkflowMerge(input);
56
+ expect(db.enqueueWorkflowMerge(input).mergeId).toBe(first.mergeId);
57
+ expect(db.transitionWorkflowMerge(root, first.mergeId, 'ready').state).toBe('ready');
58
+ db.close();
59
+ });
60
+ });
@@ -2,6 +2,8 @@ import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, Sess
2
2
  import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
3
3
  import { type FailHostActionInput, type HostActionRecord, type ReportHostActionInput, type RequestHostActionInput, type StartHostActionInput, type VerifyHostActionInput } from './host-actions';
4
4
  import { type AppendRetrievalCycleInput, type CreateRetrievalSessionInput, type RetrievalCycleRecord, type RetrievalSessionRecord, type RetrievalSessionState } from './retrieval-sessions';
5
+ import { type AddLearningCandidateEvidenceInput, type LearningCandidateEvidenceRecord, type LearningCandidateRecord, type LearningCandidateState, type LearningCandidateVersionRecord, type TransitionLearningCandidateInput, type UpsertLearningCandidateInput } from './learning-candidates';
6
+ import { type CreateWorkflowWorkerInput, type EnqueueWorkflowMergeInput, type TransitionWorkflowWorkerInput, type WorkflowMergeRecord, type WorkflowMergeState, type WorkflowWorkerEventRecord, type WorkflowWorkerRecord } from './workflow-workers';
5
7
  export interface BenchmarkReportRecord {
6
8
  runId: string;
7
9
  suiteId: string;
@@ -325,6 +327,22 @@ export declare class DevFlowDatabase {
325
327
  items: GovernanceAuditRecord[];
326
328
  total: number;
327
329
  };
330
+ upsertLearningCandidate(input: UpsertLearningCandidateInput): LearningCandidateRecord;
331
+ getLearningCandidate(id: string): LearningCandidateRecord | null;
332
+ listLearningCandidates(options?: {
333
+ projectRoot?: string;
334
+ states?: LearningCandidateState[];
335
+ limit?: number;
336
+ }): LearningCandidateRecord[];
337
+ addLearningCandidateEvidence(input: AddLearningCandidateEvidenceInput): LearningCandidateRecord;
338
+ listLearningCandidateEvidence(candidateId: string): LearningCandidateEvidenceRecord[];
339
+ resolveLearningContradiction(candidateId: string, evidenceId: string, resolvedAt?: number): LearningCandidateRecord;
340
+ transitionLearningCandidate(candidateId: string, input: TransitionLearningCandidateInput): LearningCandidateRecord;
341
+ listLearningCandidateVersions(candidateId: string): LearningCandidateVersionRecord[];
342
+ private assertLearningTransition;
343
+ private getLearningEvidenceAggregates;
344
+ private insertLearningCandidateVersion;
345
+ private insertLearningActivation;
328
346
  enqueueWork(input: EnqueueWorkInput): WorkItemRecord;
329
347
  getWorkByIdempotencyKey(idempotencyKey: string): WorkItemRecord | null;
330
348
  requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord;
@@ -333,6 +351,7 @@ export declare class DevFlowDatabase {
333
351
  listSessionClosures(projectRoot: string, limit?: number): SessionClosureRecord[];
334
352
  leaseWork(input: LeaseWorkInput): WorkItemRecord[];
335
353
  completeWork(id: string, owner: string, now?: number): boolean;
354
+ heartbeatWork(id: string, owner: string, leaseMs: number, now?: number): boolean;
336
355
  retryWork(id: string, owner: string, error: WorkError, nextAttemptAt: number): boolean;
337
356
  deferWork(id: string, owner: string, error: WorkError, nextAttemptAt: number): boolean;
338
357
  deadLetterWork(id: string, owner: string, error: WorkError): boolean;
@@ -431,6 +450,29 @@ export declare class DevFlowDatabase {
431
450
  private withImmediateTransaction;
432
451
  private isTerminalHostActionState;
433
452
  private isLegalHostActionTransition;
453
+ createWorkflowWorker(input: CreateWorkflowWorkerInput): WorkflowWorkerRecord;
454
+ getWorkflowWorker(projectRoot: string, workerId: string): WorkflowWorkerRecord | null;
455
+ listWorkflowWorkers(projectRoot: string, options?: {
456
+ states?: WorkflowWorkerRecord['state'][];
457
+ runId?: string;
458
+ limit?: number;
459
+ }): WorkflowWorkerRecord[];
460
+ listWorkflowWorkerEvents(projectRoot: string, workerId: string): WorkflowWorkerEventRecord[];
461
+ leaseWorkflowWorker(projectRoot: string, workerId: string, owner: string, leaseMs: number, now?: number): WorkflowWorkerRecord;
462
+ heartbeatWorkflowWorker(projectRoot: string, workerId: string, owner: string, leaseMs: number, now?: number): WorkflowWorkerRecord;
463
+ transitionWorkflowWorker(input: TransitionWorkflowWorkerInput): WorkflowWorkerRecord;
464
+ recoverExpiredWorkflowWorkers(projectRoot: string, now?: number): WorkflowWorkerRecord[];
465
+ enqueueWorkflowMerge(input: EnqueueWorkflowMergeInput): WorkflowMergeRecord;
466
+ getWorkflowMerge(projectRoot: string, mergeId: string): WorkflowMergeRecord | null;
467
+ getWorkflowMergeByWorker(projectRoot: string, workerId: string): WorkflowMergeRecord | null;
468
+ listWorkflowMerges(projectRoot: string, limit?: number): WorkflowMergeRecord[];
469
+ transitionWorkflowMerge(projectRoot: string, mergeId: string, target: WorkflowMergeState, update?: {
470
+ parentRevision?: string;
471
+ simulationHash?: string;
472
+ mergeCommit?: string;
473
+ reason?: string;
474
+ }, now?: number): WorkflowMergeRecord;
475
+ private appendWorkflowWorkerEvent;
434
476
  all(sql: string, ...params: unknown[]): unknown[];
435
477
  get(sql: string, ...params: unknown[]): unknown;
436
478
  close(): void;