@critical-path/core 0.3.0 → 0.5.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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/engine/index.d.ts +8 -1
  4. package/dist/engine/index.d.ts.map +1 -1
  5. package/dist/engine/index.js +93 -3
  6. package/dist/engine/index.js.map +1 -1
  7. package/dist/index.test.js +44 -0
  8. package/dist/index.test.js.map +1 -1
  9. package/dist/store/firebase.d.ts +6 -1
  10. package/dist/store/firebase.d.ts.map +1 -1
  11. package/dist/store/firebase.js +37 -0
  12. package/dist/store/firebase.js.map +1 -1
  13. package/dist/store/firebase.test.js +16 -0
  14. package/dist/store/firebase.test.js.map +1 -1
  15. package/dist/store/index.d.ts +12 -1
  16. package/dist/store/index.d.ts.map +1 -1
  17. package/dist/store/index.js +35 -0
  18. package/dist/store/index.js.map +1 -1
  19. package/dist/store/sqlite.d.ts +7 -1
  20. package/dist/store/sqlite.d.ts.map +1 -1
  21. package/dist/store/sqlite.js +81 -10
  22. package/dist/store/sqlite.js.map +1 -1
  23. package/dist/store/sqlite.test.js +21 -0
  24. package/dist/store/sqlite.test.js.map +1 -1
  25. package/dist/types/index.d.ts +32 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/use-case-validation.test.d.ts +2 -0
  28. package/dist/use-case-validation.test.d.ts.map +1 -0
  29. package/dist/use-case-validation.test.js +281 -0
  30. package/dist/use-case-validation.test.js.map +1 -0
  31. package/dist/utils/index.d.ts +1 -0
  32. package/dist/utils/index.d.ts.map +1 -1
  33. package/dist/utils/index.js +1 -0
  34. package/dist/utils/index.js.map +1 -1
  35. package/dist/utils/status.js +1 -1
  36. package/dist/utils/status.js.map +1 -1
  37. package/dist/utils/workflow.d.ts +14 -0
  38. package/dist/utils/workflow.d.ts.map +1 -0
  39. package/dist/utils/workflow.js +136 -0
  40. package/dist/utils/workflow.js.map +1 -0
  41. package/package.json +1 -1
  42. package/src/engine/index.ts +106 -4
  43. package/src/index.test.ts +54 -0
  44. package/src/store/firebase.test.ts +21 -0
  45. package/src/store/firebase.ts +42 -1
  46. package/src/store/index.ts +48 -1
  47. package/src/store/sqlite.test.ts +28 -0
  48. package/src/store/sqlite.ts +114 -7
  49. package/src/types/index.ts +38 -1
  50. package/src/use-case-validation.test.ts +324 -0
  51. package/src/utils/index.ts +2 -0
  52. package/src/utils/status.ts +1 -1
  53. package/src/utils/workflow.ts +164 -0
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -6,11 +6,18 @@ import type {
6
6
  Team,
7
7
  TaskContainer,
8
8
  Iteration,
9
- WebhookEvent
9
+ WebhookEvent,
10
+ Workflow
10
11
  } from '../types/index.js';
11
12
  import { StorageAdapter, InMemoryStore } from '../store/index.js';
12
13
  import { PluginRegistry } from '../plugins/index.js';
13
14
  import { deriveTaskLifecycleState, type TaskLifecycleState } from '../utils/status.js';
15
+ import {
16
+ validateTransition,
17
+ getAllowedNextStatuses,
18
+ WorkflowValidationError,
19
+ DEFAULT_SOFTWARE_WORKFLOW
20
+ } from '../utils/workflow.js';
14
21
 
15
22
  export class CriticalPathEngine {
16
23
  public readonly store: StorageAdapter;
@@ -35,6 +42,11 @@ export class CriticalPathEngine {
35
42
  }
36
43
 
37
44
  private async seedInitialData(data: NonNullable<CriticalPathConfig['initialData']>): Promise<void> {
45
+ if (data.workflows) {
46
+ for (const wf of data.workflows) {
47
+ await this.store.createWorkflow(wf);
48
+ }
49
+ }
38
50
  if (data.projects) {
39
51
  for (const p of data.projects) {
40
52
  await this.store.createProject(p);
@@ -62,6 +74,81 @@ export class CriticalPathEngine {
62
74
  }
63
75
  }
64
76
 
77
+ // --- Workflows ---
78
+ async getWorkflows(): Promise<Workflow[]> {
79
+ return this.store.getWorkflows();
80
+ }
81
+
82
+ async getWorkflow(id: string): Promise<Workflow | null> {
83
+ return this.store.getWorkflow(id);
84
+ }
85
+
86
+ async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
87
+ const created = await this.store.createWorkflow(workflow);
88
+ await this.store.logActivity({
89
+ actorId: 'system',
90
+ action: 'workflow.created',
91
+ details: { name: created.name }
92
+ });
93
+ this.dispatchWebhook('workflow.created', { workflow: created });
94
+ return created;
95
+ }
96
+
97
+ async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
98
+ const existing = await this.store.getWorkflow(id);
99
+ if (!existing) return null;
100
+
101
+ const updated = await this.store.updateWorkflow(id, updates);
102
+ if (updated) {
103
+ await this.store.logActivity({
104
+ actorId: 'system',
105
+ action: 'workflow.updated',
106
+ details: { name: updated.name }
107
+ });
108
+ this.dispatchWebhook('workflow.updated', { workflow: updated, previous: existing });
109
+ }
110
+ return updated;
111
+ }
112
+
113
+ async deleteWorkflow(id: string): Promise<boolean> {
114
+ const existing = await this.store.getWorkflow(id);
115
+ if (!existing) return false;
116
+
117
+ const deleted = await this.store.deleteWorkflow(id);
118
+ if (deleted) {
119
+ await this.store.logActivity({
120
+ actorId: 'system',
121
+ action: 'workflow.deleted',
122
+ details: { name: existing.name }
123
+ });
124
+ this.dispatchWebhook('workflow.deleted', { workflowId: id, name: existing.name });
125
+ }
126
+ return deleted;
127
+ }
128
+
129
+ async resolveProjectWorkflow(projectId: string): Promise<Workflow | null> {
130
+ const project = await this.store.getProject(projectId);
131
+ if (project?.workflow) return project.workflow;
132
+ if (project?.workflowId) {
133
+ const wf = await this.store.getWorkflow(project.workflowId);
134
+ if (wf) return wf;
135
+ }
136
+
137
+ const workflows = await this.store.getWorkflows();
138
+ const defaultWf = workflows.find((w) => w.isDefault);
139
+ if (defaultWf) return defaultWf;
140
+ if (workflows.length > 0) return workflows[0];
141
+
142
+ return DEFAULT_SOFTWARE_WORKFLOW;
143
+ }
144
+
145
+ async getAllowedTaskTransitions(taskId: string): Promise<string[]> {
146
+ const task = await this.store.getTask(taskId);
147
+ if (!task) return [];
148
+ const workflow = await this.resolveProjectWorkflow(task.projectId);
149
+ return getAllowedNextStatuses(workflow || undefined, task.status);
150
+ }
151
+
65
152
  // --- Projects ---
66
153
  async getProjects(): Promise<Project[]> {
67
154
  return this.store.getProjects();
@@ -94,13 +181,18 @@ export class CriticalPathEngine {
94
181
 
95
182
  async createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
96
183
  const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
184
+ const projectId = processedInput.projectId || taskInput.projectId;
185
+ const workflow = await this.resolveProjectWorkflow(projectId);
186
+
187
+ const defaultStatus = workflow?.defaultStatusKey || 'todo';
97
188
 
98
189
  const created = await this.store.createTask({
99
- projectId: processedInput.projectId || taskInput.projectId,
190
+ projectId,
100
191
  title: processedInput.title || taskInput.title,
101
192
  description: processedInput.description ?? taskInput.description,
102
- status: processedInput.status || taskInput.status || 'todo',
193
+ status: processedInput.status || taskInput.status || defaultStatus,
103
194
  priority: processedInput.priority || taskInput.priority || 'medium',
195
+ taskType: processedInput.taskType || taskInput.taskType || 'task',
104
196
  assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
105
197
  reporterId: processedInput.reporterId ?? taskInput.reporterId,
106
198
  reviewerId: processedInput.reviewerId ?? taskInput.reviewerId,
@@ -142,6 +234,14 @@ export class CriticalPathEngine {
142
234
  const existing = await this.store.getTask(id);
143
235
  if (!existing) return null;
144
236
 
237
+ if (updates.status && updates.status !== existing.status) {
238
+ const workflow = await this.resolveProjectWorkflow(existing.projectId);
239
+ const isValid = validateTransition(workflow || undefined, existing.status, updates.status);
240
+ if (!isValid) {
241
+ throw new WorkflowValidationError(existing.status, updates.status, workflow?.id);
242
+ }
243
+ }
244
+
145
245
  const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
146
246
  const updated = await this.store.updateTask(id, processedUpdates);
147
247
  if (!updated) return null;
@@ -220,7 +320,9 @@ export class CriticalPathEngine {
220
320
  const task = await this.getTask(taskId);
221
321
  if (!task) return null;
222
322
  const project = await this.getProject(task.projectId);
223
- return deriveTaskLifecycleState(task, project?.statusDefinitions);
323
+ const workflow = await this.resolveProjectWorkflow(task.projectId);
324
+ const statusDefs = project?.statusDefinitions || workflow?.statuses;
325
+ return deriveTaskLifecycleState(task, statusDefs);
224
326
  }
225
327
 
226
328
  // --- Teams ---
package/src/index.test.ts CHANGED
@@ -172,4 +172,58 @@ describe('CriticalPathEngine Core Tests', () => {
172
172
  const graphB = await engine.getTaskDependencyGraph(taskB.id);
173
173
  expect(graphB.upstreamTasks.map((t) => t.id)).toContain(taskA.id);
174
174
  });
175
+
176
+ it('manages workflows and enforces status transitions', async () => {
177
+ const engine = new CriticalPathEngine();
178
+
179
+ // Create a strict workflow
180
+ const workflow = await engine.createWorkflow({
181
+ name: 'Strict Software Workflow',
182
+ defaultStatusKey: 'backlog',
183
+ statuses: [
184
+ { key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' },
185
+ { key: 'in_progress', label: 'In Progress', completionState: 'not_done', executionState: 'active' },
186
+ { key: 'done', label: 'Done', completionState: 'done', executionState: 'inactive' }
187
+ ],
188
+ transitions: [
189
+ { id: 't1', fromStatusKey: 'backlog', toStatusKey: 'in_progress', name: 'Start Work' },
190
+ { id: 't2', fromStatusKey: 'in_progress', toStatusKey: 'done', name: 'Complete Work' }
191
+ ]
192
+ });
193
+
194
+ expect(workflow.id).toBeDefined();
195
+
196
+ // Create project tied to workflow
197
+ const proj = await engine.createProject({
198
+ key: 'WF',
199
+ name: 'Workflow Project',
200
+ workflowId: workflow.id
201
+ });
202
+
203
+ // Create task
204
+ const task = await engine.createTask({
205
+ projectId: proj.id,
206
+ title: 'Workflow Task',
207
+ status: 'backlog',
208
+ priority: 'high'
209
+ });
210
+
211
+ expect(task.status).toBe('backlog');
212
+ expect(task.taskType).toBe('task');
213
+
214
+ // Check allowed transitions
215
+ const allowed = await engine.getAllowedTaskTransitions(task.id);
216
+ expect(allowed).toEqual(['in_progress']);
217
+
218
+ // Valid transition: backlog -> in_progress
219
+ const updated = await engine.updateTask(task.id, { status: 'in_progress' });
220
+ expect(updated?.status).toBe('in_progress');
221
+
222
+ // Invalid transition: in_progress -> backlog (not allowed in strict workflow)
223
+ await expect(engine.updateTask(task.id, { status: 'backlog' })).rejects.toThrow();
224
+
225
+ // Valid transition: in_progress -> done
226
+ const doneTask = await engine.updateTask(task.id, { status: 'done' });
227
+ expect(doneTask?.status).toBe('done');
228
+ });
175
229
  });
@@ -56,4 +56,25 @@ describe('FirebaseStore', () => {
56
56
  const remaining = await store.getTasks(project.id);
57
57
  expect(remaining).toHaveLength(0);
58
58
  });
59
+
60
+ it('should create, update, and delete workflows in FirebaseStore', async () => {
61
+ const workflow = await store.createWorkflow({
62
+ name: 'Firebase Workflow',
63
+ isDefault: false,
64
+ defaultStatusKey: 'backlog',
65
+ statuses: [{ key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' }],
66
+ transitions: []
67
+ });
68
+
69
+ expect(workflow.id).toBeDefined();
70
+
71
+ const fetched = await store.getWorkflow(workflow.id);
72
+ expect(fetched?.name).toBe('Firebase Workflow');
73
+
74
+ const updated = await store.updateWorkflow(workflow.id, { isDefault: true });
75
+ expect(updated?.isDefault).toBe(true);
76
+
77
+ const deleted = await store.deleteWorkflow(workflow.id);
78
+ expect(deleted).toBe(true);
79
+ });
59
80
  });
@@ -9,7 +9,8 @@ import type {
9
9
  TimeEntry,
10
10
  Activity,
11
11
  Webhook,
12
- TaskDependency
12
+ TaskDependency,
13
+ Workflow
13
14
  } from '../types/index.js';
14
15
  import { generateProjectKey } from '../utils/key.js';
15
16
 
@@ -153,6 +154,46 @@ export class FirebaseStore implements StorageAdapter {
153
154
  return true;
154
155
  }
155
156
 
157
+ // --- Workflows ---
158
+ async getWorkflows(): Promise<Workflow[]> {
159
+ const snap = await this.db.collection('workflows').get();
160
+ return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
161
+ }
162
+
163
+ async getWorkflow(id: string): Promise<Workflow | null> {
164
+ const snap = await this.db.collection('workflows').doc(id).get();
165
+ if (!snap.exists) return null;
166
+ return { ...snap.data(), id: snap.id };
167
+ }
168
+
169
+ async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
170
+ const docRef = this.db.collection('workflows').doc();
171
+ const now = new Date().toISOString();
172
+ const newWf: Workflow = { ...workflow, id: docRef.id, createdAt: now, updatedAt: now };
173
+ await docRef.set(newWf);
174
+ return newWf;
175
+ }
176
+
177
+ async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
178
+ const existing = await this.getWorkflow(id);
179
+ if (!existing) return null;
180
+
181
+ const updated: Workflow = {
182
+ ...existing,
183
+ ...updates,
184
+ updatedAt: new Date().toISOString()
185
+ };
186
+ await this.db.collection('workflows').doc(id).set(updated, { merge: true });
187
+ return updated;
188
+ }
189
+
190
+ async deleteWorkflow(id: string): Promise<boolean> {
191
+ const existing = await this.getWorkflow(id);
192
+ if (!existing) return false;
193
+ await this.db.collection('workflows').doc(id).delete();
194
+ return true;
195
+ }
196
+
156
197
  // --- Teams ---
157
198
  async getTeams(): Promise<Team[]> {
158
199
  const snap = await this.db.collection('teams').get();
@@ -11,7 +11,8 @@ import type {
11
11
  TimeEntry,
12
12
  Activity,
13
13
  Webhook,
14
- TaskDependency
14
+ TaskDependency,
15
+ Workflow
15
16
  } from '../types/index.js';
16
17
  import { generateProjectKey } from '../utils/key.js';
17
18
 
@@ -23,6 +24,13 @@ export interface StorageAdapter {
23
24
  updateProject(id: string, updates: Partial<Project>): Promise<Project | null>;
24
25
  deleteProject(id: string): Promise<boolean>;
25
26
 
27
+ // Workflows
28
+ getWorkflows(): Promise<Workflow[]>;
29
+ getWorkflow(id: string): Promise<Workflow | null>;
30
+ createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow>;
31
+ updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null>;
32
+ deleteWorkflow(id: string): Promise<boolean>;
33
+
26
34
  // Tasks
27
35
  getTasks(projectId?: string): Promise<Task[]>;
28
36
  getTask(id: string): Promise<Task | null>;
@@ -72,6 +80,7 @@ export interface StorageAdapter {
72
80
 
73
81
  export class InMemoryStore implements StorageAdapter {
74
82
  private projects = new Map<string, Project>();
83
+ private workflows = new Map<string, Workflow>();
75
84
  private tasks = new Map<string, Task>();
76
85
  private teams = new Map<string, Team>();
77
86
  private containers = new Map<string, TaskContainer>();
@@ -121,6 +130,44 @@ export class InMemoryStore implements StorageAdapter {
121
130
  return this.projects.delete(id);
122
131
  }
123
132
 
133
+ // Workflows
134
+ async getWorkflows(): Promise<Workflow[]> {
135
+ return Array.from(this.workflows.values());
136
+ }
137
+
138
+ async getWorkflow(id: string): Promise<Workflow | null> {
139
+ return this.workflows.get(id) || null;
140
+ }
141
+
142
+ async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
143
+ const id = `wf_${Math.random().toString(36).substring(2, 9)}`;
144
+ const now = new Date().toISOString();
145
+ const newWorkflow: Workflow = {
146
+ ...workflow,
147
+ id,
148
+ createdAt: now,
149
+ updatedAt: now
150
+ };
151
+ this.workflows.set(id, newWorkflow);
152
+ return newWorkflow;
153
+ }
154
+
155
+ async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
156
+ const existing = this.workflows.get(id);
157
+ if (!existing) return null;
158
+ const updated: Workflow = {
159
+ ...existing,
160
+ ...updates,
161
+ updatedAt: new Date().toISOString()
162
+ };
163
+ this.workflows.set(id, updated);
164
+ return updated;
165
+ }
166
+
167
+ async deleteWorkflow(id: string): Promise<boolean> {
168
+ return this.workflows.delete(id);
169
+ }
170
+
124
171
  async getTasks(projectId?: string): Promise<Task[]> {
125
172
  const all = Array.from(this.tasks.values());
126
173
  if (projectId) {
@@ -75,4 +75,32 @@ describe('SQLiteStore', () => {
75
75
  const entries = await store.getTimeEntries('task_1');
76
76
  expect(entries).toHaveLength(1);
77
77
  });
78
+
79
+ it('should create, retrieve, update, and delete workflows', async () => {
80
+ const workflow = await store.createWorkflow({
81
+ name: 'SQLite Workflow',
82
+ isDefault: true,
83
+ defaultStatusKey: 'todo',
84
+ statuses: [{ key: 'todo', label: 'To Do', completionState: 'not_done', executionState: 'inactive' }],
85
+ transitions: []
86
+ });
87
+
88
+ expect(workflow.id).toMatch(/^wf_/);
89
+ expect(workflow.name).toBe('SQLite Workflow');
90
+
91
+ const fetched = await store.getWorkflow(workflow.id);
92
+ expect(fetched?.isDefault).toBe(true);
93
+
94
+ const updated = await store.updateWorkflow(workflow.id, { name: 'Updated SQLite Workflow' });
95
+ expect(updated?.name).toBe('Updated SQLite Workflow');
96
+
97
+ const all = await store.getWorkflows();
98
+ expect(all).toHaveLength(1);
99
+
100
+ const deleted = await store.deleteWorkflow(workflow.id);
101
+ expect(deleted).toBe(true);
102
+
103
+ const remaining = await store.getWorkflows();
104
+ expect(remaining).toHaveLength(0);
105
+ });
78
106
  });
@@ -10,7 +10,8 @@ import type {
10
10
  TimeEntry,
11
11
  Activity,
12
12
  Webhook,
13
- TaskDependency
13
+ TaskDependency,
14
+ Workflow
14
15
  } from '../types/index.js';
15
16
  import { generateProjectKey } from '../utils/key.js';
16
17
 
@@ -48,6 +49,8 @@ export class SQLiteStore implements StorageAdapter {
48
49
  ownerId TEXT,
49
50
  members TEXT,
50
51
  teamIds TEXT,
52
+ workflowId TEXT,
53
+ taskTypes TEXT,
51
54
  statusDefinitions TEXT,
52
55
  priorityDefinitions TEXT,
53
56
  customFieldDefinitions TEXT,
@@ -55,6 +58,19 @@ export class SQLiteStore implements StorageAdapter {
55
58
  updatedAt TEXT NOT NULL
56
59
  );
57
60
 
61
+ CREATE TABLE IF NOT EXISTS workflows (
62
+ id TEXT PRIMARY KEY,
63
+ name TEXT NOT NULL,
64
+ description TEXT,
65
+ statuses TEXT NOT NULL,
66
+ transitions TEXT NOT NULL,
67
+ taskTypes TEXT,
68
+ defaultStatusKey TEXT,
69
+ isDefault INTEGER,
70
+ createdAt TEXT NOT NULL,
71
+ updatedAt TEXT NOT NULL
72
+ );
73
+
58
74
  CREATE TABLE IF NOT EXISTS teams (
59
75
  id TEXT PRIMARY KEY,
60
76
  name TEXT NOT NULL,
@@ -84,6 +100,7 @@ export class SQLiteStore implements StorageAdapter {
84
100
  description TEXT,
85
101
  status TEXT NOT NULL,
86
102
  priority TEXT NOT NULL,
103
+ taskType TEXT,
87
104
  assigneeId TEXT,
88
105
  reporterId TEXT,
89
106
  reviewerId TEXT,
@@ -188,8 +205,8 @@ export class SQLiteStore implements StorageAdapter {
188
205
  const newProj: Project = { ...project, key, id, createdAt: now, updatedAt: now };
189
206
 
190
207
  const stmt = this.db.prepare(`
191
- INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, statusDefinitions, priorityDefinitions, customFieldDefinitions, createdAt, updatedAt)
192
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
208
+ INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, workflowId, taskTypes, statusDefinitions, priorityDefinitions, customFieldDefinitions, createdAt, updatedAt)
209
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
193
210
  `);
194
211
  stmt.run(
195
212
  newProj.id,
@@ -199,6 +216,8 @@ export class SQLiteStore implements StorageAdapter {
199
216
  newProj.ownerId || null,
200
217
  JSON.stringify(newProj.members || []),
201
218
  JSON.stringify(newProj.teamIds || []),
219
+ newProj.workflowId || null,
220
+ newProj.taskTypes ? JSON.stringify(newProj.taskTypes) : null,
202
221
  JSON.stringify(newProj.statusDefinitions || []),
203
222
  JSON.stringify(newProj.priorityDefinitions || []),
204
223
  JSON.stringify(newProj.customFieldDefinitions || []),
@@ -220,7 +239,7 @@ export class SQLiteStore implements StorageAdapter {
220
239
 
221
240
  const stmt = this.db.prepare(`
222
241
  UPDATE projects
223
- SET key = ?, name = ?, description = ?, ownerId = ?, members = ?, teamIds = ?, statusDefinitions = ?, priorityDefinitions = ?, customFieldDefinitions = ?, updatedAt = ?
242
+ SET key = ?, name = ?, description = ?, ownerId = ?, members = ?, teamIds = ?, workflowId = ?, taskTypes = ?, statusDefinitions = ?, priorityDefinitions = ?, customFieldDefinitions = ?, updatedAt = ?
224
243
  WHERE id = ?
225
244
  `);
226
245
  stmt.run(
@@ -230,6 +249,8 @@ export class SQLiteStore implements StorageAdapter {
230
249
  updated.ownerId || null,
231
250
  JSON.stringify(updated.members || []),
232
251
  JSON.stringify(updated.teamIds || []),
252
+ updated.workflowId || null,
253
+ updated.taskTypes ? JSON.stringify(updated.taskTypes) : null,
233
254
  JSON.stringify(updated.statusDefinitions || []),
234
255
  JSON.stringify(updated.priorityDefinitions || []),
235
256
  JSON.stringify(updated.customFieldDefinitions || []),
@@ -239,6 +260,77 @@ export class SQLiteStore implements StorageAdapter {
239
260
  return updated;
240
261
  }
241
262
 
263
+ // --- Workflows ---
264
+ async getWorkflows(): Promise<Workflow[]> {
265
+ const stmt = this.db.prepare('SELECT * FROM workflows');
266
+ const rows = stmt.all() as any[];
267
+ return rows.map((r) => this.mapWorkflow(r));
268
+ }
269
+
270
+ async getWorkflow(id: string): Promise<Workflow | null> {
271
+ const stmt = this.db.prepare('SELECT * FROM workflows WHERE id = ?');
272
+ const row = stmt.get(id) as any;
273
+ return row ? this.mapWorkflow(row) : null;
274
+ }
275
+
276
+ async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
277
+ const id = `wf_${Math.random().toString(36).substring(2, 9)}`;
278
+ const now = new Date().toISOString();
279
+ const newWf: Workflow = { ...workflow, id, createdAt: now, updatedAt: now };
280
+
281
+ const stmt = this.db.prepare(`
282
+ INSERT INTO workflows (id, name, description, statuses, transitions, taskTypes, defaultStatusKey, isDefault, createdAt, updatedAt)
283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
+ `);
285
+ stmt.run(
286
+ newWf.id,
287
+ newWf.name,
288
+ newWf.description || null,
289
+ JSON.stringify(newWf.statuses || []),
290
+ JSON.stringify(newWf.transitions || []),
291
+ newWf.taskTypes ? JSON.stringify(newWf.taskTypes) : null,
292
+ newWf.defaultStatusKey || null,
293
+ newWf.isDefault ? 1 : 0,
294
+ newWf.createdAt,
295
+ newWf.updatedAt
296
+ );
297
+ return newWf;
298
+ }
299
+
300
+ async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
301
+ const existing = await this.getWorkflow(id);
302
+ if (!existing) return null;
303
+
304
+ const updated: Workflow = {
305
+ ...existing,
306
+ ...updates,
307
+ updatedAt: new Date().toISOString()
308
+ };
309
+
310
+ const stmt = this.db.prepare(`
311
+ UPDATE workflows SET name = ?, description = ?, statuses = ?, transitions = ?, taskTypes = ?, defaultStatusKey = ?, isDefault = ?, updatedAt = ?
312
+ WHERE id = ?
313
+ `);
314
+ stmt.run(
315
+ updated.name,
316
+ updated.description || null,
317
+ JSON.stringify(updated.statuses || []),
318
+ JSON.stringify(updated.transitions || []),
319
+ updated.taskTypes ? JSON.stringify(updated.taskTypes) : null,
320
+ updated.defaultStatusKey || null,
321
+ updated.isDefault ? 1 : 0,
322
+ updated.updatedAt,
323
+ id
324
+ );
325
+ return updated;
326
+ }
327
+
328
+ async deleteWorkflow(id: string): Promise<boolean> {
329
+ const stmt = this.db.prepare('DELETE FROM workflows WHERE id = ?');
330
+ const result = stmt.run(id);
331
+ return (result.changes ?? 0) > 0;
332
+ }
333
+
242
334
  async deleteProject(id: string): Promise<boolean> {
243
335
  const stmt = this.db.prepare('DELETE FROM projects WHERE id = ?');
244
336
  const result = stmt.run(id);
@@ -394,12 +486,12 @@ export class SQLiteStore implements StorageAdapter {
394
486
 
395
487
  const stmt = this.db.prepare(`
396
488
  INSERT INTO tasks (
397
- id, projectId, title, description, status, priority, assigneeId, reporterId,
489
+ id, projectId, title, description, status, priority, taskType, assigneeId, reporterId,
398
490
  reviewerId, iterationId, teamId, containerId, plannedStartDate, actualStartDate, actualEndDate,
399
491
  dueDate, estimatedHours, loggedHours, actualHours, billableHours,
400
492
  estimatedDurationMinutes, actualDurationMinutes, billableDurationMinutes, progress,
401
493
  tags, customFields, parentId, createdAt, updatedAt
402
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
494
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
403
495
  `);
404
496
  stmt.run(
405
497
  newTask.id,
@@ -408,6 +500,7 @@ export class SQLiteStore implements StorageAdapter {
408
500
  newTask.description || null,
409
501
  newTask.status,
410
502
  newTask.priority,
503
+ newTask.taskType || null,
411
504
  newTask.assigneeId || null,
412
505
  newTask.reporterId || null,
413
506
  newTask.reviewerId || null,
@@ -447,7 +540,7 @@ export class SQLiteStore implements StorageAdapter {
447
540
 
448
541
  const stmt = this.db.prepare(`
449
542
  UPDATE tasks SET
450
- projectId = ?, title = ?, description = ?, status = ?, priority = ?,
543
+ projectId = ?, title = ?, description = ?, status = ?, priority = ?, taskType = ?,
451
544
  assigneeId = ?, reporterId = ?, reviewerId = ?, iterationId = ?, teamId = ?, containerId = ?,
452
545
  plannedStartDate = ?, actualStartDate = ?, actualEndDate = ?, dueDate = ?,
453
546
  estimatedHours = ?, loggedHours = ?, actualHours = ?, billableHours = ?,
@@ -461,6 +554,7 @@ export class SQLiteStore implements StorageAdapter {
461
554
  updated.description || null,
462
555
  updated.status,
463
556
  updated.priority,
557
+ updated.taskType || null,
464
558
  updated.assigneeId || null,
465
559
  updated.reporterId || null,
466
560
  updated.reviewerId || null,
@@ -706,6 +800,8 @@ export class SQLiteStore implements StorageAdapter {
706
800
  private mapProject(row: any): Project {
707
801
  return {
708
802
  ...row,
803
+ workflowId: row.workflowId || undefined,
804
+ taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
709
805
  members: row.members ? JSON.parse(row.members) : [],
710
806
  teamIds: row.teamIds ? JSON.parse(row.teamIds) : [],
711
807
  statusDefinitions: row.statusDefinitions ? JSON.parse(row.statusDefinitions) : [],
@@ -714,6 +810,16 @@ export class SQLiteStore implements StorageAdapter {
714
810
  };
715
811
  }
716
812
 
813
+ private mapWorkflow(row: any): Workflow {
814
+ return {
815
+ ...row,
816
+ statuses: row.statuses ? JSON.parse(row.statuses) : [],
817
+ transitions: row.transitions ? JSON.parse(row.transitions) : [],
818
+ taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
819
+ isDefault: row.isDefault !== null && row.isDefault !== undefined ? Boolean(row.isDefault) : undefined
820
+ };
821
+ }
822
+
717
823
  private mapTeam(row: any): Team {
718
824
  return {
719
825
  ...row,
@@ -724,6 +830,7 @@ export class SQLiteStore implements StorageAdapter {
724
830
  private mapTask(row: any): Task {
725
831
  return {
726
832
  ...row,
833
+ taskType: row.taskType || undefined,
727
834
  tags: row.tags ? JSON.parse(row.tags) : [],
728
835
  customFields: row.customFields ? JSON.parse(row.customFields) : {}
729
836
  };