@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 +23 -0
- package/__tests__/database.learning-candidates.test.ts +93 -0
- package/__tests__/database.workflow-workers.test.ts +60 -0
- package/dist/database.d.ts +42 -0
- package/dist/database.js +623 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +12 -1
- package/dist/learning-candidates.d.ts +87 -0
- package/dist/learning-candidates.js +81 -0
- package/dist/work-queue.d.ts +1 -1
- package/dist/workflow-workers.d.ts +97 -0
- package/dist/workflow-workers.js +90 -0
- package/package.json +2 -2
- package/src/database.ts +848 -0
- package/src/index.ts +35 -0
- package/src/learning-candidates.ts +181 -0
- package/src/work-queue.ts +6 -0
- package/src/workflow-workers.ts +198 -0
- package/tsconfig.tsbuildinfo +1 -0
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
|
+
});
|
package/dist/database.d.ts
CHANGED
|
@@ -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;
|