hzl-core 0.1.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.
- package/dist/__tests__/backup/backup-restore.test.d.ts +2 -0
- package/dist/__tests__/backup/backup-restore.test.d.ts.map +1 -0
- package/dist/__tests__/backup/backup-restore.test.js +200 -0
- package/dist/__tests__/backup/backup-restore.test.js.map +1 -0
- package/dist/__tests__/backup/import-export.test.d.ts +2 -0
- package/dist/__tests__/backup/import-export.test.d.ts.map +1 -0
- package/dist/__tests__/backup/import-export.test.js +341 -0
- package/dist/__tests__/backup/import-export.test.js.map +1 -0
- package/dist/__tests__/concurrency/stress.test.d.ts +2 -0
- package/dist/__tests__/concurrency/stress.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency/stress.test.js +274 -0
- package/dist/__tests__/concurrency/stress.test.js.map +1 -0
- package/dist/__tests__/concurrency/worker.d.ts +2 -0
- package/dist/__tests__/concurrency/worker.d.ts.map +1 -0
- package/dist/__tests__/concurrency/worker.js +84 -0
- package/dist/__tests__/concurrency/worker.js.map +1 -0
- package/dist/__tests__/migrations/upgrade.test.d.ts +2 -0
- package/dist/__tests__/migrations/upgrade.test.d.ts.map +1 -0
- package/dist/__tests__/migrations/upgrade.test.js +203 -0
- package/dist/__tests__/migrations/upgrade.test.js.map +1 -0
- package/dist/__tests__/projections/rebuild-equivalence.test.d.ts +2 -0
- package/dist/__tests__/projections/rebuild-equivalence.test.d.ts.map +1 -0
- package/dist/__tests__/projections/rebuild-equivalence.test.js +276 -0
- package/dist/__tests__/projections/rebuild-equivalence.test.js.map +1 -0
- package/dist/__tests__/properties/invariants.test.d.ts +2 -0
- package/dist/__tests__/properties/invariants.test.d.ts.map +1 -0
- package/dist/__tests__/properties/invariants.test.js +314 -0
- package/dist/__tests__/properties/invariants.test.js.map +1 -0
- package/dist/db/connection.d.ts +13 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +52 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/connection.test.d.ts +2 -0
- package/dist/db/connection.test.d.ts.map +1 -0
- package/dist/db/connection.test.js +63 -0
- package/dist/db/connection.test.js.map +1 -0
- package/dist/db/migrations/v2.d.ts +2 -0
- package/dist/db/migrations/v2.d.ts.map +1 -0
- package/dist/db/migrations/v2.js +4 -0
- package/dist/db/migrations/v2.js.map +1 -0
- package/dist/db/migrations.d.ts +4 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +45 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/migrations.test.d.ts +2 -0
- package/dist/db/migrations.test.d.ts.map +1 -0
- package/dist/db/migrations.test.js +75 -0
- package/dist/db/migrations.test.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +114 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/events/store.d.ts +33 -0
- package/dist/events/store.d.ts.map +1 -0
- package/dist/events/store.js +81 -0
- package/dist/events/store.js.map +1 -0
- package/dist/events/store.test.d.ts +2 -0
- package/dist/events/store.test.d.ts.map +1 -0
- package/dist/events/store.test.js +138 -0
- package/dist/events/store.test.js.map +1 -0
- package/dist/events/types.d.ts +106 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +87 -0
- package/dist/events/types.js.map +1 -0
- package/dist/events/validation.test.d.ts +2 -0
- package/dist/events/validation.test.d.ts.map +1 -0
- package/dist/events/validation.test.js +83 -0
- package/dist/events/validation.test.js.map +1 -0
- package/dist/fixtures/sample-data.d.ts +16 -0
- package/dist/fixtures/sample-data.d.ts.map +1 -0
- package/dist/fixtures/sample-data.js +148 -0
- package/dist/fixtures/sample-data.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +102 -0
- package/dist/index.test.js.map +1 -0
- package/dist/projections/comments-checkpoints.d.ts +11 -0
- package/dist/projections/comments-checkpoints.d.ts.map +1 -0
- package/dist/projections/comments-checkpoints.js +33 -0
- package/dist/projections/comments-checkpoints.js.map +1 -0
- package/dist/projections/comments-checkpoints.test.d.ts +2 -0
- package/dist/projections/comments-checkpoints.test.d.ts.map +1 -0
- package/dist/projections/comments-checkpoints.test.js +72 -0
- package/dist/projections/comments-checkpoints.test.js.map +1 -0
- package/dist/projections/dependencies.d.ts +12 -0
- package/dist/projections/dependencies.d.ts.map +1 -0
- package/dist/projections/dependencies.js +39 -0
- package/dist/projections/dependencies.js.map +1 -0
- package/dist/projections/dependencies.test.d.ts +2 -0
- package/dist/projections/dependencies.test.d.ts.map +1 -0
- package/dist/projections/dependencies.test.js +97 -0
- package/dist/projections/dependencies.test.js.map +1 -0
- package/dist/projections/engine.d.ts +18 -0
- package/dist/projections/engine.d.ts.map +1 -0
- package/dist/projections/engine.js +56 -0
- package/dist/projections/engine.js.map +1 -0
- package/dist/projections/engine.test.d.ts +2 -0
- package/dist/projections/engine.test.d.ts.map +1 -0
- package/dist/projections/engine.test.js +92 -0
- package/dist/projections/engine.test.js.map +1 -0
- package/dist/projections/rebuild.d.ts +4 -0
- package/dist/projections/rebuild.d.ts.map +1 -0
- package/dist/projections/rebuild.js +26 -0
- package/dist/projections/rebuild.js.map +1 -0
- package/dist/projections/rebuild.test.d.ts +2 -0
- package/dist/projections/rebuild.test.d.ts.map +1 -0
- package/dist/projections/rebuild.test.js +59 -0
- package/dist/projections/rebuild.test.js.map +1 -0
- package/dist/projections/search.d.ts +11 -0
- package/dist/projections/search.d.ts.map +1 -0
- package/dist/projections/search.js +39 -0
- package/dist/projections/search.js.map +1 -0
- package/dist/projections/search.test.d.ts +2 -0
- package/dist/projections/search.test.d.ts.map +1 -0
- package/dist/projections/search.test.js +78 -0
- package/dist/projections/search.test.js.map +1 -0
- package/dist/projections/tags.d.ts +12 -0
- package/dist/projections/tags.d.ts.map +1 -0
- package/dist/projections/tags.js +41 -0
- package/dist/projections/tags.js.map +1 -0
- package/dist/projections/tags.test.d.ts +2 -0
- package/dist/projections/tags.test.d.ts.map +1 -0
- package/dist/projections/tags.test.js +69 -0
- package/dist/projections/tags.test.js.map +1 -0
- package/dist/projections/tasks-current.d.ts +14 -0
- package/dist/projections/tasks-current.d.ts.map +1 -0
- package/dist/projections/tasks-current.js +110 -0
- package/dist/projections/tasks-current.js.map +1 -0
- package/dist/projections/tasks-current.test.d.ts +2 -0
- package/dist/projections/tasks-current.test.d.ts.map +1 -0
- package/dist/projections/tasks-current.test.js +215 -0
- package/dist/projections/tasks-current.test.js.map +1 -0
- package/dist/projections/types.d.ts +13 -0
- package/dist/projections/types.d.ts.map +1 -0
- package/dist/projections/types.js +2 -0
- package/dist/projections/types.js.map +1 -0
- package/dist/services/backup-service.d.ts +16 -0
- package/dist/services/backup-service.d.ts.map +1 -0
- package/dist/services/backup-service.js +114 -0
- package/dist/services/backup-service.js.map +1 -0
- package/dist/services/search-service.d.ts +27 -0
- package/dist/services/search-service.d.ts.map +1 -0
- package/dist/services/search-service.js +33 -0
- package/dist/services/search-service.js.map +1 -0
- package/dist/services/search-service.test.d.ts +2 -0
- package/dist/services/search-service.test.d.ts.map +1 -0
- package/dist/services/search-service.test.js +66 -0
- package/dist/services/search-service.test.js.map +1 -0
- package/dist/services/task-service.d.ts +147 -0
- package/dist/services/task-service.d.ts.map +1 -0
- package/dist/services/task-service.js +442 -0
- package/dist/services/task-service.js.map +1 -0
- package/dist/services/task-service.test.d.ts +2 -0
- package/dist/services/task-service.test.d.ts.map +1 -0
- package/dist/services/task-service.test.js +399 -0
- package/dist/services/task-service.test.js.map +1 -0
- package/dist/services/validation-service.d.ts +29 -0
- package/dist/services/validation-service.d.ts.map +1 -0
- package/dist/services/validation-service.js +74 -0
- package/dist/services/validation-service.js.map +1 -0
- package/dist/services/validation-service.test.d.ts +2 -0
- package/dist/services/validation-service.test.d.ts.map +1 -0
- package/dist/services/validation-service.test.js +67 -0
- package/dist/services/validation-service.test.js.map +1 -0
- package/dist/utils/id.d.ts +3 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +12 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/id.test.d.ts +2 -0
- package/dist/utils/id.test.d.ts.map +1 -0
- package/dist/utils/id.test.js +24 -0
- package/dist/utils/id.test.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// packages/hzl-core/src/services/search-service.test.ts
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { SearchService } from './search-service.js';
|
|
5
|
+
import { runMigrations } from '../db/migrations.js';
|
|
6
|
+
import { EventStore } from '../events/store.js';
|
|
7
|
+
import { ProjectionEngine } from '../projections/engine.js';
|
|
8
|
+
import { TasksCurrentProjector } from '../projections/tasks-current.js';
|
|
9
|
+
import { SearchProjector } from '../projections/search.js';
|
|
10
|
+
import { EventType } from '../events/types.js';
|
|
11
|
+
describe('SearchService', () => {
|
|
12
|
+
let db;
|
|
13
|
+
let eventStore;
|
|
14
|
+
let engine;
|
|
15
|
+
let searchService;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
db = new Database(':memory:');
|
|
18
|
+
runMigrations(db);
|
|
19
|
+
eventStore = new EventStore(db);
|
|
20
|
+
engine = new ProjectionEngine(db);
|
|
21
|
+
engine.register(new TasksCurrentProjector());
|
|
22
|
+
engine.register(new SearchProjector());
|
|
23
|
+
searchService = new SearchService(db);
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => { db.close(); });
|
|
26
|
+
function createTask(taskId, title, project, description) {
|
|
27
|
+
const event = eventStore.append({ task_id: taskId, type: EventType.TaskCreated, data: { title, project, description } });
|
|
28
|
+
engine.applyEvent(event);
|
|
29
|
+
}
|
|
30
|
+
describe('search', () => {
|
|
31
|
+
it('finds tasks by title match', () => {
|
|
32
|
+
createTask('TASK1', 'Implement authentication', 'project-a');
|
|
33
|
+
createTask('TASK2', 'Write documentation', 'project-a');
|
|
34
|
+
const results = searchService.search('authentication');
|
|
35
|
+
expect(results.tasks).toHaveLength(1);
|
|
36
|
+
expect(results.tasks[0].task_id).toBe('TASK1');
|
|
37
|
+
});
|
|
38
|
+
it('finds tasks by description match', () => {
|
|
39
|
+
createTask('TASK1', 'Backend task', 'project-a', 'Implement OAuth2');
|
|
40
|
+
const results = searchService.search('OAuth2');
|
|
41
|
+
expect(results.tasks).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
it('supports project filter', () => {
|
|
44
|
+
createTask('TASK1', 'Auth for A', 'project-a');
|
|
45
|
+
createTask('TASK2', 'Auth for B', 'project-b');
|
|
46
|
+
const results = searchService.search('Auth', { project: 'project-a' });
|
|
47
|
+
expect(results.tasks).toHaveLength(1);
|
|
48
|
+
expect(results.tasks[0].task_id).toBe('TASK1');
|
|
49
|
+
});
|
|
50
|
+
it('supports limit and offset pagination', () => {
|
|
51
|
+
for (let i = 0; i < 5; i++)
|
|
52
|
+
createTask(`TASK${i}`, `Test task ${i}`, 'inbox');
|
|
53
|
+
const page1 = searchService.search('Test', { limit: 2, offset: 0 });
|
|
54
|
+
const page2 = searchService.search('Test', { limit: 2, offset: 2 });
|
|
55
|
+
expect(page1.tasks).toHaveLength(2);
|
|
56
|
+
expect(page2.tasks).toHaveLength(2);
|
|
57
|
+
expect(page1.total).toBe(5);
|
|
58
|
+
});
|
|
59
|
+
it('handles empty query', () => {
|
|
60
|
+
createTask('TASK1', 'Test', 'inbox');
|
|
61
|
+
const results = searchService.search('');
|
|
62
|
+
expect(results.tasks).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=search-service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-service.test.js","sourceRoot":"","sources":["../../src/services/search-service.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import { EventStore } from '../events/store.js';
|
|
3
|
+
import { TaskStatus } from '../events/types.js';
|
|
4
|
+
import { ProjectionEngine } from '../projections/engine.js';
|
|
5
|
+
export interface CreateTaskInput {
|
|
6
|
+
title: string;
|
|
7
|
+
project: string;
|
|
8
|
+
parent_id?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
links?: string[];
|
|
11
|
+
depends_on?: string[];
|
|
12
|
+
tags?: string[];
|
|
13
|
+
priority?: number;
|
|
14
|
+
due_at?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface EventContext {
|
|
18
|
+
author?: string;
|
|
19
|
+
agent_id?: string;
|
|
20
|
+
session_id?: string;
|
|
21
|
+
correlation_id?: string;
|
|
22
|
+
causation_id?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ClaimTaskOptions extends EventContext {
|
|
25
|
+
lease_until?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ClaimNextOptions {
|
|
28
|
+
author?: string;
|
|
29
|
+
agent_id?: string;
|
|
30
|
+
project?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
lease_until?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface StealOptions {
|
|
35
|
+
ifExpired?: boolean;
|
|
36
|
+
force?: boolean;
|
|
37
|
+
author?: string;
|
|
38
|
+
agent_id?: string;
|
|
39
|
+
lease_until?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface StealResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface StuckTask {
|
|
46
|
+
task_id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
project: string;
|
|
49
|
+
claimed_at: string;
|
|
50
|
+
claimed_by_author: string | null;
|
|
51
|
+
claimed_by_agent_id: string | null;
|
|
52
|
+
lease_until: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface AvailableTask {
|
|
55
|
+
task_id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
project: string;
|
|
58
|
+
status: TaskStatus;
|
|
59
|
+
priority: number;
|
|
60
|
+
created_at: string;
|
|
61
|
+
tags: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface Comment {
|
|
64
|
+
event_rowid: number;
|
|
65
|
+
task_id: string;
|
|
66
|
+
author?: string;
|
|
67
|
+
agent_id?: string;
|
|
68
|
+
text: string;
|
|
69
|
+
timestamp: string;
|
|
70
|
+
}
|
|
71
|
+
export interface Checkpoint {
|
|
72
|
+
event_rowid: number;
|
|
73
|
+
task_id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
data: Record<string, unknown>;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
}
|
|
78
|
+
export interface Task {
|
|
79
|
+
task_id: string;
|
|
80
|
+
title: string;
|
|
81
|
+
project: string;
|
|
82
|
+
status: TaskStatus;
|
|
83
|
+
parent_id: string | null;
|
|
84
|
+
description: string | null;
|
|
85
|
+
links: string[];
|
|
86
|
+
tags: string[];
|
|
87
|
+
priority: number;
|
|
88
|
+
due_at: string | null;
|
|
89
|
+
metadata: Record<string, unknown>;
|
|
90
|
+
claimed_at: string | null;
|
|
91
|
+
claimed_by_author: string | null;
|
|
92
|
+
claimed_by_agent_id: string | null;
|
|
93
|
+
lease_until: string | null;
|
|
94
|
+
created_at: string;
|
|
95
|
+
updated_at: string;
|
|
96
|
+
}
|
|
97
|
+
export declare class TaskNotFoundError extends Error {
|
|
98
|
+
constructor(taskId: string);
|
|
99
|
+
}
|
|
100
|
+
export declare class TaskNotClaimableError extends Error {
|
|
101
|
+
constructor(taskId: string, reason: string);
|
|
102
|
+
}
|
|
103
|
+
export declare class DependenciesNotDoneError extends Error {
|
|
104
|
+
constructor(taskId: string, pendingDeps: string[]);
|
|
105
|
+
}
|
|
106
|
+
export declare class TaskService {
|
|
107
|
+
private db;
|
|
108
|
+
private eventStore;
|
|
109
|
+
private projectionEngine;
|
|
110
|
+
private getIncompleteDepsStmt;
|
|
111
|
+
constructor(db: Database.Database, eventStore: EventStore, projectionEngine: ProjectionEngine);
|
|
112
|
+
createTask(input: CreateTaskInput, ctx?: EventContext): Task;
|
|
113
|
+
claimTask(taskId: string, opts?: ClaimTaskOptions): Task;
|
|
114
|
+
setStatus(taskId: string, toStatus: TaskStatus, ctx?: EventContext): Task;
|
|
115
|
+
completeTask(taskId: string, ctx?: EventContext): Task;
|
|
116
|
+
claimNext(opts?: ClaimNextOptions): Task | null;
|
|
117
|
+
releaseTask(taskId: string, opts?: {
|
|
118
|
+
reason?: string;
|
|
119
|
+
} & EventContext): Task;
|
|
120
|
+
archiveTask(taskId: string, opts?: {
|
|
121
|
+
reason?: string;
|
|
122
|
+
} & EventContext): Task;
|
|
123
|
+
reopenTask(taskId: string, opts?: {
|
|
124
|
+
to_status?: TaskStatus.Ready | TaskStatus.Backlog;
|
|
125
|
+
reason?: string;
|
|
126
|
+
} & EventContext): Task;
|
|
127
|
+
stealTask(taskId: string, opts: StealOptions): StealResult;
|
|
128
|
+
getStuckTasks(opts: {
|
|
129
|
+
project?: string;
|
|
130
|
+
olderThan: number;
|
|
131
|
+
}): StuckTask[];
|
|
132
|
+
areAllDepsDone(taskId: string): boolean;
|
|
133
|
+
isTaskAvailable(taskId: string): boolean;
|
|
134
|
+
getAvailableTasks(opts: {
|
|
135
|
+
project?: string;
|
|
136
|
+
tagsAny?: string[];
|
|
137
|
+
tagsAll?: string[];
|
|
138
|
+
limit?: number;
|
|
139
|
+
}): AvailableTask[];
|
|
140
|
+
getTaskById(taskId: string): Task | null;
|
|
141
|
+
addComment(taskId: string, text: string, opts?: EventContext): Comment;
|
|
142
|
+
addCheckpoint(taskId: string, name: string, data?: Record<string, unknown>, opts?: EventContext): Checkpoint;
|
|
143
|
+
getComments(taskId: string): Comment[];
|
|
144
|
+
getCheckpoints(taskId: string): Checkpoint[];
|
|
145
|
+
private rowToTask;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=task-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-service.d.ts","sourceRoot":"","sources":["../../src/services/task-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { EventType, TaskStatus } from '../events/types.js';
|
|
2
|
+
import { withWriteTransaction } from '../db/connection.js';
|
|
3
|
+
import { generateId } from '../utils/id.js';
|
|
4
|
+
export class TaskNotFoundError extends Error {
|
|
5
|
+
constructor(taskId) {
|
|
6
|
+
super(`Task not found: ${taskId}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class TaskNotClaimableError extends Error {
|
|
10
|
+
constructor(taskId, reason) {
|
|
11
|
+
super(`Task ${taskId} is not claimable: ${reason}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class DependenciesNotDoneError extends Error {
|
|
15
|
+
constructor(taskId, pendingDeps) {
|
|
16
|
+
super(`Task ${taskId} has dependencies not done: ${pendingDeps.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class TaskService {
|
|
20
|
+
db;
|
|
21
|
+
eventStore;
|
|
22
|
+
projectionEngine;
|
|
23
|
+
getIncompleteDepsStmt;
|
|
24
|
+
constructor(db, eventStore, projectionEngine) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.eventStore = eventStore;
|
|
27
|
+
this.projectionEngine = projectionEngine;
|
|
28
|
+
this.getIncompleteDepsStmt = db.prepare(`
|
|
29
|
+
SELECT td.depends_on_id
|
|
30
|
+
FROM task_dependencies td
|
|
31
|
+
LEFT JOIN tasks_current tc ON tc.task_id = td.depends_on_id
|
|
32
|
+
WHERE td.task_id = ?
|
|
33
|
+
AND (tc.status IS NULL OR tc.status != 'done')
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
createTask(input, ctx) {
|
|
37
|
+
const taskId = generateId();
|
|
38
|
+
const eventData = {
|
|
39
|
+
title: input.title,
|
|
40
|
+
project: input.project,
|
|
41
|
+
parent_id: input.parent_id,
|
|
42
|
+
description: input.description,
|
|
43
|
+
links: input.links,
|
|
44
|
+
depends_on: input.depends_on,
|
|
45
|
+
tags: input.tags,
|
|
46
|
+
priority: input.priority,
|
|
47
|
+
due_at: input.due_at,
|
|
48
|
+
metadata: input.metadata,
|
|
49
|
+
};
|
|
50
|
+
Object.keys(eventData).forEach((key) => {
|
|
51
|
+
if (eventData[key] === undefined) {
|
|
52
|
+
delete eventData[key];
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const task = withWriteTransaction(this.db, () => {
|
|
56
|
+
const event = this.eventStore.append({
|
|
57
|
+
task_id: taskId,
|
|
58
|
+
type: EventType.TaskCreated,
|
|
59
|
+
data: eventData,
|
|
60
|
+
author: ctx?.author,
|
|
61
|
+
agent_id: ctx?.agent_id,
|
|
62
|
+
session_id: ctx?.session_id,
|
|
63
|
+
correlation_id: ctx?.correlation_id,
|
|
64
|
+
causation_id: ctx?.causation_id,
|
|
65
|
+
});
|
|
66
|
+
this.projectionEngine.applyEvent(event);
|
|
67
|
+
return this.getTaskById(taskId);
|
|
68
|
+
});
|
|
69
|
+
if (!task) {
|
|
70
|
+
throw new Error(`Failed to create task: task not found after creation`);
|
|
71
|
+
}
|
|
72
|
+
return task;
|
|
73
|
+
}
|
|
74
|
+
claimTask(taskId, opts) {
|
|
75
|
+
return withWriteTransaction(this.db, () => {
|
|
76
|
+
const task = this.getTaskById(taskId);
|
|
77
|
+
if (!task)
|
|
78
|
+
throw new TaskNotFoundError(taskId);
|
|
79
|
+
if (task.status !== TaskStatus.Ready) {
|
|
80
|
+
throw new TaskNotClaimableError(taskId, `status is ${task.status}, must be ready`);
|
|
81
|
+
}
|
|
82
|
+
const incompleteDeps = this.getIncompleteDepsStmt.all(taskId);
|
|
83
|
+
if (incompleteDeps.length > 0) {
|
|
84
|
+
throw new DependenciesNotDoneError(taskId, incompleteDeps.map(d => d.depends_on_id));
|
|
85
|
+
}
|
|
86
|
+
const eventData = {
|
|
87
|
+
from: TaskStatus.Ready,
|
|
88
|
+
to: TaskStatus.InProgress,
|
|
89
|
+
};
|
|
90
|
+
if (opts?.lease_until)
|
|
91
|
+
eventData.lease_until = opts.lease_until;
|
|
92
|
+
const event = this.eventStore.append({
|
|
93
|
+
task_id: taskId,
|
|
94
|
+
type: EventType.StatusChanged,
|
|
95
|
+
data: eventData,
|
|
96
|
+
author: opts?.author,
|
|
97
|
+
agent_id: opts?.agent_id,
|
|
98
|
+
});
|
|
99
|
+
this.projectionEngine.applyEvent(event);
|
|
100
|
+
return this.getTaskById(taskId);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
setStatus(taskId, toStatus, ctx) {
|
|
104
|
+
return withWriteTransaction(this.db, () => {
|
|
105
|
+
const task = this.getTaskById(taskId);
|
|
106
|
+
if (!task)
|
|
107
|
+
throw new TaskNotFoundError(taskId);
|
|
108
|
+
const event = this.eventStore.append({
|
|
109
|
+
task_id: taskId,
|
|
110
|
+
type: EventType.StatusChanged,
|
|
111
|
+
data: { from: task.status, to: toStatus },
|
|
112
|
+
author: ctx?.author,
|
|
113
|
+
agent_id: ctx?.agent_id,
|
|
114
|
+
});
|
|
115
|
+
this.projectionEngine.applyEvent(event);
|
|
116
|
+
return this.getTaskById(taskId);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
completeTask(taskId, ctx) {
|
|
120
|
+
return withWriteTransaction(this.db, () => {
|
|
121
|
+
const task = this.getTaskById(taskId);
|
|
122
|
+
if (!task)
|
|
123
|
+
throw new TaskNotFoundError(taskId);
|
|
124
|
+
if (task.status !== TaskStatus.InProgress) {
|
|
125
|
+
throw new Error(`Cannot complete: status is ${task.status}, must be in_progress`);
|
|
126
|
+
}
|
|
127
|
+
const event = this.eventStore.append({
|
|
128
|
+
task_id: taskId,
|
|
129
|
+
type: EventType.StatusChanged,
|
|
130
|
+
data: { from: TaskStatus.InProgress, to: TaskStatus.Done },
|
|
131
|
+
author: ctx?.author,
|
|
132
|
+
agent_id: ctx?.agent_id,
|
|
133
|
+
});
|
|
134
|
+
this.projectionEngine.applyEvent(event);
|
|
135
|
+
return this.getTaskById(taskId);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
claimNext(opts = {}) {
|
|
139
|
+
return withWriteTransaction(this.db, () => {
|
|
140
|
+
let candidate;
|
|
141
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
142
|
+
const tagPlaceholders = opts.tags.map(() => '?').join(', ');
|
|
143
|
+
const tagCount = opts.tags.length;
|
|
144
|
+
let query = `
|
|
145
|
+
SELECT tc.task_id FROM tasks_current tc
|
|
146
|
+
WHERE tc.status = 'ready'
|
|
147
|
+
AND NOT EXISTS (
|
|
148
|
+
SELECT 1 FROM task_dependencies td
|
|
149
|
+
JOIN tasks_current dep ON td.depends_on_id = dep.task_id
|
|
150
|
+
WHERE td.task_id = tc.task_id AND dep.status != 'done'
|
|
151
|
+
)
|
|
152
|
+
AND (SELECT COUNT(DISTINCT tag) FROM task_tags WHERE task_id = tc.task_id AND tag IN (${tagPlaceholders})) = ?
|
|
153
|
+
`;
|
|
154
|
+
const params = [...opts.tags, tagCount];
|
|
155
|
+
if (opts.project) {
|
|
156
|
+
query += ' AND tc.project = ?';
|
|
157
|
+
params.push(opts.project);
|
|
158
|
+
}
|
|
159
|
+
query += ' ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1';
|
|
160
|
+
candidate = this.db.prepare(query).get(...params);
|
|
161
|
+
}
|
|
162
|
+
else if (opts.project) {
|
|
163
|
+
candidate = this.db.prepare(`
|
|
164
|
+
SELECT tc.task_id FROM tasks_current tc
|
|
165
|
+
WHERE tc.status = 'ready' AND tc.project = ?
|
|
166
|
+
AND NOT EXISTS (
|
|
167
|
+
SELECT 1 FROM task_dependencies td
|
|
168
|
+
JOIN tasks_current dep ON td.depends_on_id = dep.task_id
|
|
169
|
+
WHERE td.task_id = tc.task_id AND dep.status != 'done'
|
|
170
|
+
)
|
|
171
|
+
ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1
|
|
172
|
+
`).get(opts.project);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
candidate = this.db.prepare(`
|
|
176
|
+
SELECT tc.task_id FROM tasks_current tc
|
|
177
|
+
WHERE tc.status = 'ready'
|
|
178
|
+
AND NOT EXISTS (
|
|
179
|
+
SELECT 1 FROM task_dependencies td
|
|
180
|
+
JOIN tasks_current dep ON td.depends_on_id = dep.task_id
|
|
181
|
+
WHERE td.task_id = tc.task_id AND dep.status != 'done'
|
|
182
|
+
)
|
|
183
|
+
ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1
|
|
184
|
+
`).get();
|
|
185
|
+
}
|
|
186
|
+
if (!candidate)
|
|
187
|
+
return null;
|
|
188
|
+
const event = this.eventStore.append({
|
|
189
|
+
task_id: candidate.task_id,
|
|
190
|
+
type: EventType.StatusChanged,
|
|
191
|
+
data: { from: TaskStatus.Ready, to: TaskStatus.InProgress, lease_until: opts.lease_until },
|
|
192
|
+
author: opts.author,
|
|
193
|
+
agent_id: opts.agent_id,
|
|
194
|
+
});
|
|
195
|
+
this.projectionEngine.applyEvent(event);
|
|
196
|
+
return this.getTaskById(candidate.task_id);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
releaseTask(taskId, opts) {
|
|
200
|
+
return withWriteTransaction(this.db, () => {
|
|
201
|
+
const task = this.getTaskById(taskId);
|
|
202
|
+
if (!task)
|
|
203
|
+
throw new TaskNotFoundError(taskId);
|
|
204
|
+
if (task.status !== TaskStatus.InProgress) {
|
|
205
|
+
throw new Error(`Cannot release: status is ${task.status}, expected in_progress`);
|
|
206
|
+
}
|
|
207
|
+
const event = this.eventStore.append({
|
|
208
|
+
task_id: taskId,
|
|
209
|
+
type: EventType.StatusChanged,
|
|
210
|
+
data: { from: TaskStatus.InProgress, to: TaskStatus.Ready, reason: opts?.reason },
|
|
211
|
+
author: opts?.author,
|
|
212
|
+
agent_id: opts?.agent_id,
|
|
213
|
+
});
|
|
214
|
+
this.projectionEngine.applyEvent(event);
|
|
215
|
+
return this.getTaskById(taskId);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
archiveTask(taskId, opts) {
|
|
219
|
+
return withWriteTransaction(this.db, () => {
|
|
220
|
+
const task = this.getTaskById(taskId);
|
|
221
|
+
if (!task)
|
|
222
|
+
throw new TaskNotFoundError(taskId);
|
|
223
|
+
if (task.status === TaskStatus.Archived) {
|
|
224
|
+
throw new Error('Task is already archived');
|
|
225
|
+
}
|
|
226
|
+
const event = this.eventStore.append({
|
|
227
|
+
task_id: taskId,
|
|
228
|
+
type: EventType.TaskArchived,
|
|
229
|
+
data: { reason: opts?.reason },
|
|
230
|
+
author: opts?.author,
|
|
231
|
+
agent_id: opts?.agent_id,
|
|
232
|
+
});
|
|
233
|
+
this.projectionEngine.applyEvent(event);
|
|
234
|
+
return this.getTaskById(taskId);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
reopenTask(taskId, opts) {
|
|
238
|
+
return withWriteTransaction(this.db, () => {
|
|
239
|
+
const task = this.getTaskById(taskId);
|
|
240
|
+
if (!task)
|
|
241
|
+
throw new TaskNotFoundError(taskId);
|
|
242
|
+
if (task.status !== TaskStatus.Done) {
|
|
243
|
+
throw new Error(`Cannot reopen: status is ${task.status}, expected done`);
|
|
244
|
+
}
|
|
245
|
+
const toStatus = opts?.to_status ?? TaskStatus.Ready;
|
|
246
|
+
const event = this.eventStore.append({
|
|
247
|
+
task_id: taskId,
|
|
248
|
+
type: EventType.StatusChanged,
|
|
249
|
+
data: { from: TaskStatus.Done, to: toStatus, reason: opts?.reason },
|
|
250
|
+
author: opts?.author,
|
|
251
|
+
agent_id: opts?.agent_id,
|
|
252
|
+
});
|
|
253
|
+
this.projectionEngine.applyEvent(event);
|
|
254
|
+
return this.getTaskById(taskId);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
stealTask(taskId, opts) {
|
|
258
|
+
return withWriteTransaction(this.db, () => {
|
|
259
|
+
const task = this.getTaskById(taskId);
|
|
260
|
+
if (!task)
|
|
261
|
+
return { success: false, error: `Task ${taskId} not found` };
|
|
262
|
+
if (task.status !== TaskStatus.InProgress) {
|
|
263
|
+
return { success: false, error: `Task ${taskId} is not in_progress` };
|
|
264
|
+
}
|
|
265
|
+
if (!opts.force) {
|
|
266
|
+
if (opts.ifExpired) {
|
|
267
|
+
const now = new Date().toISOString();
|
|
268
|
+
if (task.lease_until && task.lease_until >= now) {
|
|
269
|
+
return { success: false, error: `Task ${taskId} lease has not expired` };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return { success: false, error: 'Must specify either force=true or ifExpired=true' };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const event = this.eventStore.append({
|
|
277
|
+
task_id: taskId,
|
|
278
|
+
type: EventType.StatusChanged,
|
|
279
|
+
data: { from: TaskStatus.InProgress, to: TaskStatus.InProgress, reason: 'stolen', lease_until: opts.lease_until },
|
|
280
|
+
author: opts.author,
|
|
281
|
+
agent_id: opts.agent_id,
|
|
282
|
+
});
|
|
283
|
+
this.projectionEngine.applyEvent(event);
|
|
284
|
+
// Update claim info
|
|
285
|
+
this.db.prepare(`
|
|
286
|
+
UPDATE tasks_current SET
|
|
287
|
+
claimed_at = ?, claimed_by_author = ?, claimed_by_agent_id = ?, lease_until = ?, updated_at = ?, last_event_id = ?
|
|
288
|
+
WHERE task_id = ?
|
|
289
|
+
`).run(new Date().toISOString(), opts.author ?? null, opts.agent_id ?? null, opts.lease_until ?? null, new Date().toISOString(), event.rowid, taskId);
|
|
290
|
+
return { success: true };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
getStuckTasks(opts) {
|
|
294
|
+
const cutoffTime = new Date(Date.now() - opts.olderThan).toISOString();
|
|
295
|
+
let query = `
|
|
296
|
+
SELECT task_id, title, project, claimed_at, claimed_by_author, claimed_by_agent_id, lease_until
|
|
297
|
+
FROM tasks_current WHERE status = 'in_progress' AND claimed_at < ?
|
|
298
|
+
`;
|
|
299
|
+
const params = [cutoffTime];
|
|
300
|
+
if (opts.project) {
|
|
301
|
+
query += ' AND project = ?';
|
|
302
|
+
params.push(opts.project);
|
|
303
|
+
}
|
|
304
|
+
query += ' ORDER BY claimed_at ASC';
|
|
305
|
+
return this.db.prepare(query).all(...params);
|
|
306
|
+
}
|
|
307
|
+
areAllDepsDone(taskId) {
|
|
308
|
+
const result = this.db.prepare(`
|
|
309
|
+
SELECT COUNT(*) as count FROM task_dependencies td
|
|
310
|
+
JOIN tasks_current tc ON td.depends_on_id = tc.task_id
|
|
311
|
+
WHERE td.task_id = ? AND tc.status != 'done'
|
|
312
|
+
`).get(taskId);
|
|
313
|
+
return result.count === 0;
|
|
314
|
+
}
|
|
315
|
+
isTaskAvailable(taskId) {
|
|
316
|
+
const task = this.getTaskById(taskId);
|
|
317
|
+
if (!task)
|
|
318
|
+
return false;
|
|
319
|
+
if (task.status !== TaskStatus.Ready)
|
|
320
|
+
return false;
|
|
321
|
+
return this.areAllDepsDone(taskId);
|
|
322
|
+
}
|
|
323
|
+
getAvailableTasks(opts) {
|
|
324
|
+
let query = `
|
|
325
|
+
SELECT tc.task_id, tc.title, tc.project, tc.status, tc.priority, tc.created_at, tc.tags
|
|
326
|
+
FROM tasks_current tc
|
|
327
|
+
WHERE tc.status = 'ready'
|
|
328
|
+
AND NOT EXISTS (
|
|
329
|
+
SELECT 1 FROM task_dependencies td
|
|
330
|
+
JOIN tasks_current dep ON td.depends_on_id = dep.task_id
|
|
331
|
+
WHERE td.task_id = tc.task_id AND dep.status != 'done'
|
|
332
|
+
)
|
|
333
|
+
`;
|
|
334
|
+
const params = [];
|
|
335
|
+
if (opts.project) {
|
|
336
|
+
query += ' AND tc.project = ?';
|
|
337
|
+
params.push(opts.project);
|
|
338
|
+
}
|
|
339
|
+
if (opts.tagsAny?.length) {
|
|
340
|
+
query += ` AND EXISTS (SELECT 1 FROM task_tags tt WHERE tt.task_id = tc.task_id AND tt.tag IN (${opts.tagsAny.map(() => '?').join(',')}))`;
|
|
341
|
+
params.push(...opts.tagsAny);
|
|
342
|
+
}
|
|
343
|
+
if (opts.tagsAll?.length) {
|
|
344
|
+
query += ` AND (SELECT COUNT(DISTINCT tt.tag) FROM task_tags tt WHERE tt.task_id = tc.task_id AND tt.tag IN (${opts.tagsAll.map(() => '?').join(',')})) = ?`;
|
|
345
|
+
params.push(...opts.tagsAll, opts.tagsAll.length);
|
|
346
|
+
}
|
|
347
|
+
query += ' ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC';
|
|
348
|
+
if (opts.limit) {
|
|
349
|
+
query += ' LIMIT ?';
|
|
350
|
+
params.push(opts.limit);
|
|
351
|
+
}
|
|
352
|
+
const rows = this.db.prepare(query).all(...params);
|
|
353
|
+
return rows.map(row => ({
|
|
354
|
+
task_id: row.task_id,
|
|
355
|
+
title: row.title,
|
|
356
|
+
project: row.project,
|
|
357
|
+
status: row.status,
|
|
358
|
+
priority: row.priority,
|
|
359
|
+
created_at: row.created_at,
|
|
360
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
getTaskById(taskId) {
|
|
364
|
+
const row = this.db.prepare('SELECT * FROM tasks_current WHERE task_id = ?').get(taskId);
|
|
365
|
+
if (!row)
|
|
366
|
+
return null;
|
|
367
|
+
return this.rowToTask(row);
|
|
368
|
+
}
|
|
369
|
+
addComment(taskId, text, opts) {
|
|
370
|
+
if (!text?.trim())
|
|
371
|
+
throw new Error('Comment text cannot be empty');
|
|
372
|
+
const task = this.getTaskById(taskId);
|
|
373
|
+
if (!task)
|
|
374
|
+
throw new TaskNotFoundError(taskId);
|
|
375
|
+
return withWriteTransaction(this.db, () => {
|
|
376
|
+
const event = this.eventStore.append({
|
|
377
|
+
task_id: taskId,
|
|
378
|
+
type: EventType.CommentAdded,
|
|
379
|
+
data: { text },
|
|
380
|
+
author: opts?.author,
|
|
381
|
+
agent_id: opts?.agent_id,
|
|
382
|
+
});
|
|
383
|
+
this.projectionEngine.applyEvent(event);
|
|
384
|
+
return { event_rowid: event.rowid, task_id: taskId, author: opts?.author, agent_id: opts?.agent_id, text, timestamp: event.timestamp };
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
addCheckpoint(taskId, name, data, opts) {
|
|
388
|
+
if (!name?.trim())
|
|
389
|
+
throw new Error('Checkpoint name cannot be empty');
|
|
390
|
+
const task = this.getTaskById(taskId);
|
|
391
|
+
if (!task)
|
|
392
|
+
throw new TaskNotFoundError(taskId);
|
|
393
|
+
const checkpointData = data ?? {};
|
|
394
|
+
return withWriteTransaction(this.db, () => {
|
|
395
|
+
const event = this.eventStore.append({
|
|
396
|
+
task_id: taskId,
|
|
397
|
+
type: EventType.CheckpointRecorded,
|
|
398
|
+
data: { name, data: checkpointData },
|
|
399
|
+
author: opts?.author,
|
|
400
|
+
agent_id: opts?.agent_id,
|
|
401
|
+
});
|
|
402
|
+
this.projectionEngine.applyEvent(event);
|
|
403
|
+
return { event_rowid: event.rowid, task_id: taskId, name, data: checkpointData, timestamp: event.timestamp };
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
getComments(taskId) {
|
|
407
|
+
const rows = this.db.prepare(`
|
|
408
|
+
SELECT event_rowid, task_id, author, agent_id, text, timestamp
|
|
409
|
+
FROM task_comments WHERE task_id = ? ORDER BY event_rowid ASC
|
|
410
|
+
`).all(taskId);
|
|
411
|
+
return rows.map(r => ({ event_rowid: r.event_rowid, task_id: r.task_id, author: r.author ?? undefined, agent_id: r.agent_id ?? undefined, text: r.text, timestamp: r.timestamp }));
|
|
412
|
+
}
|
|
413
|
+
getCheckpoints(taskId) {
|
|
414
|
+
const rows = this.db.prepare(`
|
|
415
|
+
SELECT event_rowid, task_id, name, data, timestamp
|
|
416
|
+
FROM task_checkpoints WHERE task_id = ? ORDER BY event_rowid ASC
|
|
417
|
+
`).all(taskId);
|
|
418
|
+
return rows.map(r => ({ event_rowid: r.event_rowid, task_id: r.task_id, name: r.name, data: JSON.parse(r.data), timestamp: r.timestamp }));
|
|
419
|
+
}
|
|
420
|
+
rowToTask(row) {
|
|
421
|
+
return {
|
|
422
|
+
task_id: row.task_id,
|
|
423
|
+
title: row.title,
|
|
424
|
+
project: row.project,
|
|
425
|
+
status: row.status,
|
|
426
|
+
parent_id: row.parent_id,
|
|
427
|
+
description: row.description,
|
|
428
|
+
links: JSON.parse(row.links),
|
|
429
|
+
tags: JSON.parse(row.tags),
|
|
430
|
+
priority: row.priority,
|
|
431
|
+
due_at: row.due_at,
|
|
432
|
+
metadata: JSON.parse(row.metadata),
|
|
433
|
+
claimed_at: row.claimed_at,
|
|
434
|
+
claimed_by_author: row.claimed_by_author,
|
|
435
|
+
claimed_by_agent_id: row.claimed_by_agent_id,
|
|
436
|
+
lease_until: row.lease_until,
|
|
437
|
+
created_at: row.created_at,
|
|
438
|
+
updated_at: row.updated_at,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=task-service.js.map
|