@critical-path/core 0.5.1 → 0.6.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 +17 -0
  2. package/README.md +51 -21
  3. package/dist/domain/custom-fields.d.ts +9 -0
  4. package/dist/domain/custom-fields.d.ts.map +1 -0
  5. package/dist/domain/custom-fields.js +72 -0
  6. package/dist/domain/custom-fields.js.map +1 -0
  7. package/dist/domain/ddd.test.d.ts +2 -0
  8. package/dist/domain/ddd.test.d.ts.map +1 -0
  9. package/dist/domain/ddd.test.js +246 -0
  10. package/dist/domain/ddd.test.js.map +1 -0
  11. package/dist/domain/entities.d.ts +82 -0
  12. package/dist/domain/entities.d.ts.map +1 -0
  13. package/dist/domain/entities.js +321 -0
  14. package/dist/domain/entities.js.map +1 -0
  15. package/dist/domain/events.d.ts +125 -0
  16. package/dist/domain/events.d.ts.map +1 -0
  17. package/dist/domain/events.js +35 -0
  18. package/dist/domain/events.js.map +1 -0
  19. package/dist/domain/graph.d.ts +19 -0
  20. package/dist/domain/graph.d.ts.map +1 -0
  21. package/dist/domain/graph.js +58 -0
  22. package/dist/domain/graph.js.map +1 -0
  23. package/dist/domain/index.d.ts +5 -0
  24. package/dist/domain/index.d.ts.map +1 -0
  25. package/dist/domain/index.js +5 -0
  26. package/dist/domain/index.js.map +1 -0
  27. package/dist/engine/index.d.ts +10 -2
  28. package/dist/engine/index.d.ts.map +1 -1
  29. package/dist/engine/index.js +288 -6
  30. package/dist/engine/index.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/store/index.d.ts +23 -1
  36. package/dist/store/index.d.ts.map +1 -1
  37. package/dist/store/index.js.map +1 -1
  38. package/dist/types/index.d.ts +5 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/utils/workflow.js +1 -1
  41. package/dist/utils/workflow.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/domain/custom-fields.ts +131 -0
  44. package/src/domain/ddd.test.ts +307 -0
  45. package/src/domain/entities.ts +390 -0
  46. package/src/domain/events.ts +171 -0
  47. package/src/domain/graph.ts +70 -0
  48. package/src/domain/index.ts +4 -0
  49. package/src/engine/index.ts +355 -8
  50. package/src/index.ts +1 -1
  51. package/src/store/index.ts +35 -11
  52. package/src/types/index.ts +7 -0
  53. package/src/utils/workflow.ts +1 -1
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -2,26 +2,53 @@ import type {
2
2
  CriticalPathConfig,
3
3
  Project,
4
4
  Task,
5
+ TaskDependency,
5
6
  TaskDependencyGraph,
6
7
  Team,
7
8
  TaskContainer,
8
9
  Iteration,
10
+ Comment,
11
+ TimeEntry,
9
12
  WebhookEvent,
10
- Workflow
13
+ Workflow,
14
+ CreateTaskInput,
15
+ CreateProjectInput
11
16
  } from '../types/index.js';
12
17
  import { StorageAdapter, InMemoryStore } from '../store/index.js';
13
18
  import { PluginRegistry } from '../plugins/index.js';
14
- import { deriveTaskLifecycleState, type TaskLifecycleState } from '../utils/status.js';
19
+ import { deriveTaskLifecycleState, type TaskLifecycleState, resolveStatusDefinition } from '../utils/status.js';
15
20
  import {
16
21
  validateTransition,
17
22
  getAllowedNextStatuses,
18
23
  WorkflowValidationError,
19
24
  DEFAULT_SOFTWARE_WORKFLOW
20
25
  } from '../utils/workflow.js';
26
+ import {
27
+ DomainEventBus,
28
+ TaskCreatedEvent,
29
+ TaskUpdatedEvent,
30
+ TaskStatusChangedEvent,
31
+ TaskDeletedEvent,
32
+ ProjectCreatedEvent,
33
+ ProjectUpdatedEvent,
34
+ WorkflowCreatedEvent,
35
+ WorkflowUpdatedEvent,
36
+ WorkflowDeletedEvent,
37
+ IterationStartedEvent,
38
+ IterationCompletedEvent,
39
+ TeamCreatedEvent,
40
+ ContainerCreatedEvent,
41
+ TaskDependencyAddedEvent,
42
+ TimeLoggedEvent,
43
+ CommentAddedEvent
44
+ } from '../domain/events.js';
45
+ import { validateCustomFieldValues } from '../domain/custom-fields.js';
46
+ import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
21
47
 
22
48
  export class CriticalPathEngine {
23
49
  public readonly store: StorageAdapter;
24
50
  public readonly plugins: PluginRegistry;
51
+ public readonly events: DomainEventBus;
25
52
 
26
53
  constructor(config: CriticalPathConfig = {}) {
27
54
  this.store = typeof config.store === 'object' && config.store !== null
@@ -29,6 +56,7 @@ export class CriticalPathEngine {
29
56
  : new InMemoryStore();
30
57
 
31
58
  this.plugins = new PluginRegistry();
59
+ this.events = new DomainEventBus();
32
60
 
33
61
  if (config.plugins) {
34
62
  for (const plugin of config.plugins) {
@@ -85,6 +113,18 @@ export class CriticalPathEngine {
85
113
 
86
114
  async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
87
115
  const created = await this.store.createWorkflow(workflow);
116
+ const now = new Date().toISOString();
117
+
118
+ const event: WorkflowCreatedEvent = {
119
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
120
+ name: 'workflow.created',
121
+ aggregateId: created.id,
122
+ aggregateType: 'Workflow',
123
+ occurredAt: now,
124
+ payload: { workflow: created }
125
+ };
126
+ await this.events.publish(event);
127
+
88
128
  await this.store.logActivity({
89
129
  actorId: 'system',
90
130
  action: 'workflow.created',
@@ -100,6 +140,17 @@ export class CriticalPathEngine {
100
140
 
101
141
  const updated = await this.store.updateWorkflow(id, updates);
102
142
  if (updated) {
143
+ const now = new Date().toISOString();
144
+ const event: WorkflowUpdatedEvent = {
145
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
146
+ name: 'workflow.updated',
147
+ aggregateId: updated.id,
148
+ aggregateType: 'Workflow',
149
+ occurredAt: now,
150
+ payload: { workflow: updated, previous: existing }
151
+ };
152
+ await this.events.publish(event);
153
+
103
154
  await this.store.logActivity({
104
155
  actorId: 'system',
105
156
  action: 'workflow.updated',
@@ -116,6 +167,17 @@ export class CriticalPathEngine {
116
167
 
117
168
  const deleted = await this.store.deleteWorkflow(id);
118
169
  if (deleted) {
170
+ const now = new Date().toISOString();
171
+ const event: WorkflowDeletedEvent = {
172
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
173
+ name: 'workflow.deleted',
174
+ aggregateId: id,
175
+ aggregateType: 'Workflow',
176
+ occurredAt: now,
177
+ payload: { workflowId: id, name: existing.name }
178
+ };
179
+ await this.events.publish(event);
180
+
119
181
  await this.store.logActivity({
120
182
  actorId: 'system',
121
183
  action: 'workflow.deleted',
@@ -160,6 +222,18 @@ export class CriticalPathEngine {
160
222
 
161
223
  async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
162
224
  const created = await this.store.createProject(project);
225
+ const now = new Date().toISOString();
226
+
227
+ const event: ProjectCreatedEvent = {
228
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
229
+ name: 'project.created',
230
+ aggregateId: created.id,
231
+ aggregateType: 'Project',
232
+ occurredAt: now,
233
+ payload: { project: created }
234
+ };
235
+ await this.events.publish(event);
236
+
163
237
  await this.store.logActivity({
164
238
  projectId: created.id,
165
239
  actorId: project.ownerId || 'system',
@@ -170,6 +244,34 @@ export class CriticalPathEngine {
170
244
  return created;
171
245
  }
172
246
 
247
+ async updateProject(id: string, updates: Partial<Project>): Promise<Project | null> {
248
+ const existing = await this.store.getProject(id);
249
+ if (!existing) return null;
250
+
251
+ const updated = await this.store.updateProject(id, updates);
252
+ if (updated) {
253
+ const now = new Date().toISOString();
254
+ const event: ProjectUpdatedEvent = {
255
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
256
+ name: 'project.updated',
257
+ aggregateId: updated.id,
258
+ aggregateType: 'Project',
259
+ occurredAt: now,
260
+ payload: { project: updated, previous: existing }
261
+ };
262
+ await this.events.publish(event);
263
+
264
+ await this.store.logActivity({
265
+ projectId: updated.id,
266
+ actorId: 'system',
267
+ action: 'project.updated',
268
+ details: { name: updated.name }
269
+ });
270
+ this.dispatchWebhook('project.updated', { project: updated });
271
+ }
272
+ return updated;
273
+ }
274
+
173
275
  // --- Tasks ---
174
276
  async getTasks(projectId?: string): Promise<Task[]> {
175
277
  return this.store.getTasks(projectId);
@@ -179,18 +281,34 @@ export class CriticalPathEngine {
179
281
  return this.store.getTask(id);
180
282
  }
181
283
 
182
- async createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
284
+ async createTask(taskInput: CreateTaskInput): Promise<Task> {
183
285
  const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
184
286
  const projectId = processedInput.projectId || taskInput.projectId;
287
+ const project = await this.store.getProject(projectId);
185
288
  const workflow = await this.resolveProjectWorkflow(projectId);
186
289
 
290
+ // Validate custom field domain invariants
291
+ if (project?.customFieldDefinitions && (processedInput.customFields || taskInput.customFields)) {
292
+ validateCustomFieldValues(
293
+ project.customFieldDefinitions,
294
+ processedInput.customFields || taskInput.customFields
295
+ );
296
+ }
297
+
187
298
  const defaultStatus = workflow?.defaultStatusKey || 'todo';
299
+ const initialStatus = processedInput.status || taskInput.status || defaultStatus;
300
+ const now = new Date().toISOString();
301
+
302
+ // Derive initial lifecycle timestamps
303
+ const statusDef = resolveStatusDefinition(initialStatus, project?.statusDefinitions || workflow?.statuses);
304
+ const actualStartDate = processedInput.actualStartDate ?? taskInput.actualStartDate ?? (statusDef.executionState === 'active' ? now : undefined);
305
+ const actualEndDate = processedInput.actualEndDate ?? taskInput.actualEndDate ?? (statusDef.completionState === 'done' ? now : undefined);
188
306
 
189
307
  const created = await this.store.createTask({
190
308
  projectId,
191
309
  title: processedInput.title || taskInput.title,
192
310
  description: processedInput.description ?? taskInput.description,
193
- status: processedInput.status || taskInput.status || defaultStatus,
311
+ status: initialStatus,
194
312
  priority: processedInput.priority || taskInput.priority || 'medium',
195
313
  taskType: processedInput.taskType || taskInput.taskType || 'task',
196
314
  assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
@@ -200,8 +318,8 @@ export class CriticalPathEngine {
200
318
  teamId: processedInput.teamId ?? taskInput.teamId,
201
319
  containerId: processedInput.containerId ?? taskInput.containerId,
202
320
  plannedStartDate: processedInput.plannedStartDate ?? taskInput.plannedStartDate,
203
- actualStartDate: processedInput.actualStartDate ?? taskInput.actualStartDate,
204
- actualEndDate: processedInput.actualEndDate ?? taskInput.actualEndDate,
321
+ actualStartDate,
322
+ actualEndDate,
205
323
  dueDate: processedInput.dueDate ?? taskInput.dueDate,
206
324
  estimatedHours: processedInput.estimatedHours ?? taskInput.estimatedHours,
207
325
  loggedHours: processedInput.loggedHours ?? taskInput.loggedHours ?? 0,
@@ -210,7 +328,7 @@ export class CriticalPathEngine {
210
328
  estimatedDurationMinutes: processedInput.estimatedDurationMinutes ?? taskInput.estimatedDurationMinutes,
211
329
  actualDurationMinutes: processedInput.actualDurationMinutes ?? taskInput.actualDurationMinutes,
212
330
  billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
213
- progress: processedInput.progress ?? taskInput.progress ?? 0,
331
+ progress: processedInput.progress ?? taskInput.progress ?? (statusDef.completionState === 'done' ? 100 : 0),
214
332
  tags: processedInput.tags ?? taskInput.tags ?? [],
215
333
  customFields: processedInput.customFields ?? taskInput.customFields ?? {},
216
334
  parentId: processedInput.parentId ?? taskInput.parentId
@@ -218,6 +336,17 @@ export class CriticalPathEngine {
218
336
 
219
337
  await this.plugins.runAfterTaskCreate(created);
220
338
 
339
+ // Publish typed Domain Event
340
+ const event: TaskCreatedEvent = {
341
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
342
+ name: 'task.created',
343
+ aggregateId: created.id,
344
+ aggregateType: 'Task',
345
+ occurredAt: now,
346
+ payload: { task: created }
347
+ };
348
+ await this.events.publish(event);
349
+
221
350
  await this.store.logActivity({
222
351
  projectId: created.projectId,
223
352
  taskId: created.id,
@@ -234,21 +363,83 @@ export class CriticalPathEngine {
234
363
  const existing = await this.store.getTask(id);
235
364
  if (!existing) return null;
236
365
 
366
+ const project = await this.store.getProject(existing.projectId);
367
+ const workflow = await this.resolveProjectWorkflow(existing.projectId);
368
+
369
+ // Validate workflow transition invariant
237
370
  if (updates.status && updates.status !== existing.status) {
238
- const workflow = await this.resolveProjectWorkflow(existing.projectId);
239
371
  const isValid = validateTransition(workflow || undefined, existing.status, updates.status);
240
372
  if (!isValid) {
241
373
  throw new WorkflowValidationError(existing.status, updates.status, workflow?.id);
242
374
  }
243
375
  }
244
376
 
377
+ // Validate custom field invariants
378
+ if (project?.customFieldDefinitions && updates.customFields) {
379
+ validateCustomFieldValues(project.customFieldDefinitions, {
380
+ ...existing.customFields,
381
+ ...updates.customFields
382
+ });
383
+ }
384
+
245
385
  const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
386
+
387
+ // Auto-update execution/completion timestamps if status changes
388
+ if (processedUpdates.status && processedUpdates.status !== existing.status) {
389
+ const statusDef = resolveStatusDefinition(
390
+ processedUpdates.status,
391
+ project?.statusDefinitions || workflow?.statuses
392
+ );
393
+ const now = new Date().toISOString();
394
+ if (statusDef.executionState === 'active' && !existing.actualStartDate && !processedUpdates.actualStartDate) {
395
+ processedUpdates.actualStartDate = now;
396
+ }
397
+ if (statusDef.completionState === 'done' && !processedUpdates.actualEndDate) {
398
+ processedUpdates.actualEndDate = now;
399
+ if (processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
400
+ processedUpdates.progress = 100;
401
+ }
402
+ }
403
+ }
404
+
246
405
  const updated = await this.store.updateTask(id, processedUpdates);
247
406
  if (!updated) return null;
248
407
 
249
408
  await this.plugins.runAfterTaskUpdate(updated, existing);
250
409
 
410
+ const now = new Date().toISOString();
251
411
  const isStatusChange = existing.status !== updated.status;
412
+
413
+ // Publish typed Domain Events
414
+ if (isStatusChange) {
415
+ const statusEvent: TaskStatusChangedEvent = {
416
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
417
+ name: 'task.status_changed',
418
+ aggregateId: updated.id,
419
+ aggregateType: 'Task',
420
+ occurredAt: now,
421
+ payload: {
422
+ task: updated,
423
+ previousStatus: existing.status,
424
+ newStatus: updated.status
425
+ }
426
+ };
427
+ await this.events.publish(statusEvent);
428
+ } else {
429
+ const updateEvent: TaskUpdatedEvent = {
430
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
431
+ name: 'task.updated',
432
+ aggregateId: updated.id,
433
+ aggregateType: 'Task',
434
+ occurredAt: now,
435
+ payload: {
436
+ task: updated,
437
+ previous: existing
438
+ }
439
+ };
440
+ await this.events.publish(updateEvent);
441
+ }
442
+
252
443
  await this.store.logActivity({
253
444
  projectId: updated.projectId,
254
445
  taskId: updated.id,
@@ -274,6 +465,22 @@ export class CriticalPathEngine {
274
465
 
275
466
  if (deleted) {
276
467
  await this.plugins.runAfterTaskDelete(id);
468
+ const now = new Date().toISOString();
469
+
470
+ const event: TaskDeletedEvent = {
471
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
472
+ name: 'task.deleted',
473
+ aggregateId: id,
474
+ aggregateType: 'Task',
475
+ occurredAt: now,
476
+ payload: {
477
+ taskId: id,
478
+ projectId: existing.projectId,
479
+ title: existing.title
480
+ }
481
+ };
482
+ await this.events.publish(event);
483
+
277
484
  await this.store.logActivity({
278
485
  projectId: existing.projectId,
279
486
  taskId: id,
@@ -287,6 +494,35 @@ export class CriticalPathEngine {
287
494
  }
288
495
 
289
496
  // --- Task Dependencies & Graph ---
497
+ async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
498
+ const existingDependencies = await this.store.getDependencies(dep.taskId);
499
+ const allProjectDeps = [
500
+ ...existingDependencies,
501
+ ...(await this.store.getDependencies(dep.dependsOnTaskId))
502
+ ];
503
+
504
+ // Enforce Directed Acyclic Graph (DAG) Invariant
505
+ const cycleCheck = detectDependencyCycle(allProjectDeps, dep);
506
+ if (cycleCheck.hasCycle) {
507
+ throw new CircularDependencyError(dep.taskId, dep.dependsOnTaskId, cycleCheck.cyclePath);
508
+ }
509
+
510
+ const created = await this.store.addDependency(dep);
511
+ const now = new Date().toISOString();
512
+
513
+ const event: TaskDependencyAddedEvent = {
514
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
515
+ name: 'dependency.added',
516
+ aggregateId: created.id,
517
+ aggregateType: 'Dependency',
518
+ occurredAt: now,
519
+ payload: { dependency: created }
520
+ };
521
+ await this.events.publish(event);
522
+
523
+ return created;
524
+ }
525
+
290
526
  async getTaskDependencyGraph(taskId: string): Promise<TaskDependencyGraph> {
291
527
  const dependencies = await this.store.getDependencies(taskId);
292
528
  const upstreamTaskIds = new Set<string>();
@@ -325,6 +561,64 @@ export class CriticalPathEngine {
325
561
  return deriveTaskLifecycleState(task, statusDefs);
326
562
  }
327
563
 
564
+ // --- Time Tracking ---
565
+ async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt' | 'userId'> & { userId?: string }): Promise<TimeEntry> {
566
+ if (entry.hours <= 0) {
567
+ throw new Error('Logged hours must be a positive number.');
568
+ }
569
+
570
+ const task = await this.store.getTask(entry.taskId);
571
+ if (task) {
572
+ const newLoggedHours = (task.loggedHours || 0) + entry.hours;
573
+ const newActualHours = (task.actualHours || 0) + entry.hours;
574
+ const newBillableHours = entry.isBillable !== false ? (task.billableHours || 0) + entry.hours : task.billableHours;
575
+ await this.store.updateTask(task.id, {
576
+ loggedHours: newLoggedHours,
577
+ actualHours: newActualHours,
578
+ billableHours: newBillableHours
579
+ });
580
+ }
581
+
582
+ const fullEntry: Omit<TimeEntry, 'id' | 'loggedAt'> = {
583
+ ...entry,
584
+ userId: entry.userId || task?.assigneeId || 'system'
585
+ };
586
+
587
+ const created = await this.store.logTime(fullEntry);
588
+ const now = new Date().toISOString();
589
+
590
+ const event: TimeLoggedEvent = {
591
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
592
+ name: 'time.logged',
593
+ aggregateId: entry.taskId,
594
+ aggregateType: 'Task',
595
+ occurredAt: now,
596
+ payload: { taskId: entry.taskId, timeEntry: created }
597
+ };
598
+ await this.events.publish(event);
599
+
600
+ return created;
601
+ }
602
+
603
+ // --- Comments ---
604
+ async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
605
+ const created = await this.store.addComment(comment);
606
+ const now = new Date().toISOString();
607
+
608
+ const event: CommentAddedEvent = {
609
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
610
+ name: 'comment.created',
611
+ aggregateId: comment.taskId,
612
+ aggregateType: 'Task',
613
+ occurredAt: now,
614
+ payload: { taskId: comment.taskId, comment: created }
615
+ };
616
+ await this.events.publish(event);
617
+
618
+ this.dispatchWebhook('comment.created', { comment: created });
619
+ return created;
620
+ }
621
+
328
622
  // --- Teams ---
329
623
  async getTeams(): Promise<Team[]> {
330
624
  return this.store.getTeams();
@@ -336,6 +630,18 @@ export class CriticalPathEngine {
336
630
 
337
631
  async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
338
632
  const created = await this.store.createTeam(team);
633
+ const now = new Date().toISOString();
634
+
635
+ const event: TeamCreatedEvent = {
636
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
637
+ name: 'team.created',
638
+ aggregateId: created.id,
639
+ aggregateType: 'Team',
640
+ occurredAt: now,
641
+ payload: { team: created }
642
+ };
643
+ await this.events.publish(event);
644
+
339
645
  this.dispatchWebhook('team.created', { team: created });
340
646
  return created;
341
647
  }
@@ -359,6 +665,18 @@ export class CriticalPathEngine {
359
665
 
360
666
  async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
361
667
  const created = await this.store.createContainer(container);
668
+ const now = new Date().toISOString();
669
+
670
+ const event: ContainerCreatedEvent = {
671
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
672
+ name: 'container.created',
673
+ aggregateId: created.id,
674
+ aggregateType: 'Container',
675
+ occurredAt: now,
676
+ payload: { container: created }
677
+ };
678
+ await this.events.publish(event);
679
+
362
680
  this.dispatchWebhook('container.created', { container: created });
363
681
  return created;
364
682
  }
@@ -383,6 +701,16 @@ export class CriticalPathEngine {
383
701
  async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
384
702
  const created = await this.store.createIteration(iteration);
385
703
  if (created.status === 'active') {
704
+ const now = new Date().toISOString();
705
+ const event: IterationStartedEvent = {
706
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
707
+ name: 'iteration.started',
708
+ aggregateId: created.id,
709
+ aggregateType: 'Iteration',
710
+ occurredAt: now,
711
+ payload: { iteration: created }
712
+ };
713
+ await this.events.publish(event);
386
714
  this.dispatchWebhook('iteration.started', { iteration: created });
387
715
  }
388
716
  return created;
@@ -394,9 +722,28 @@ export class CriticalPathEngine {
394
722
 
395
723
  const updated = await this.store.updateIteration(id, updates);
396
724
  if (updated) {
725
+ const now = new Date().toISOString();
397
726
  if (existing.status !== 'active' && updated.status === 'active') {
727
+ const event: IterationStartedEvent = {
728
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
729
+ name: 'iteration.started',
730
+ aggregateId: updated.id,
731
+ aggregateType: 'Iteration',
732
+ occurredAt: now,
733
+ payload: { iteration: updated }
734
+ };
735
+ await this.events.publish(event);
398
736
  this.dispatchWebhook('iteration.started', { iteration: updated });
399
737
  } else if (existing.status !== 'completed' && updated.status === 'completed') {
738
+ const event: IterationCompletedEvent = {
739
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
740
+ name: 'iteration.completed',
741
+ aggregateId: updated.id,
742
+ aggregateType: 'Iteration',
743
+ occurredAt: now,
744
+ payload: { iteration: updated }
745
+ };
746
+ await this.events.publish(event);
400
747
  this.dispatchWebhook('iteration.completed', { iteration: updated });
401
748
  }
402
749
  }
package/src/index.ts CHANGED
@@ -3,4 +3,4 @@ export * from './utils/index.js';
3
3
  export * from './store/index.js';
4
4
  export * from './plugins/index.js';
5
5
  export * from './engine/index.js';
6
-
6
+ export * from './domain/index.js';
@@ -16,68 +16,92 @@ import type {
16
16
  } from '../types/index.js';
17
17
  import { generateProjectKey } from '../utils/key.js';
18
18
 
19
- export interface StorageAdapter {
20
- // Projects
19
+ export interface ProjectRepository {
21
20
  getProjects(): Promise<Project[]>;
22
21
  getProject(id: string): Promise<Project | null>;
23
22
  createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project>;
24
23
  updateProject(id: string, updates: Partial<Project>): Promise<Project | null>;
25
24
  deleteProject(id: string): Promise<boolean>;
25
+ }
26
26
 
27
- // Workflows
27
+ export interface WorkflowRepository {
28
28
  getWorkflows(): Promise<Workflow[]>;
29
29
  getWorkflow(id: string): Promise<Workflow | null>;
30
30
  createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow>;
31
31
  updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null>;
32
32
  deleteWorkflow(id: string): Promise<boolean>;
33
+ }
33
34
 
34
- // Tasks
35
+ export interface TaskRepository {
35
36
  getTasks(projectId?: string): Promise<Task[]>;
36
37
  getTask(id: string): Promise<Task | null>;
37
38
  createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
38
39
  updateTask(id: string, updates: Partial<Task>): Promise<Task | null>;
39
40
  deleteTask(id: string): Promise<boolean>;
41
+ }
40
42
 
41
- // Teams
43
+ export interface TeamRepository {
42
44
  getTeams(): Promise<Team[]>;
43
45
  getTeam(id: string): Promise<Team | null>;
44
46
  createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team>;
45
47
  updateTeam(id: string, updates: Partial<Team>): Promise<Team | null>;
46
48
  deleteTeam(id: string): Promise<boolean>;
49
+ }
47
50
 
48
- // Task Containers
51
+ export interface ContainerRepository {
49
52
  getContainers(projectId: string): Promise<TaskContainer[]>;
50
53
  getContainer(id: string): Promise<TaskContainer | null>;
51
54
  createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer>;
52
55
  updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null>;
53
56
  deleteContainer(id: string): Promise<boolean>;
57
+ }
54
58
 
55
- // Iterations
59
+ export interface IterationRepository {
56
60
  getIterations(projectId: string): Promise<Iteration[]>;
57
61
  getIteration(id: string): Promise<Iteration | null>;
58
62
  createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration>;
59
63
  updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null>;
60
64
  deleteIteration(id: string): Promise<boolean>;
65
+ }
61
66
 
62
- // Comments & Activity
67
+ export interface CommentRepository {
63
68
  getComments(taskId: string): Promise<Comment[]>;
64
69
  addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
70
+ }
71
+
72
+ export interface ActivityRepository {
65
73
  getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]>;
66
74
  logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity>;
75
+ }
67
76
 
68
- // Time Tracking
77
+ export interface TimeEntryRepository {
69
78
  getTimeEntries(taskId: string): Promise<TimeEntry[]>;
70
79
  logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'>): Promise<TimeEntry>;
80
+ }
71
81
 
72
- // Dependencies
82
+ export interface DependencyRepository {
73
83
  getDependencies(taskId: string): Promise<TaskDependency[]>;
74
84
  addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency>;
85
+ }
75
86
 
76
- // Webhooks
87
+ export interface WebhookRepository {
77
88
  getWebhooks(): Promise<Webhook[]>;
78
89
  addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook>;
79
90
  }
80
91
 
92
+ export interface StorageAdapter
93
+ extends ProjectRepository,
94
+ WorkflowRepository,
95
+ TaskRepository,
96
+ TeamRepository,
97
+ ContainerRepository,
98
+ IterationRepository,
99
+ CommentRepository,
100
+ ActivityRepository,
101
+ TimeEntryRepository,
102
+ DependencyRepository,
103
+ WebhookRepository {}
104
+
81
105
  export class InMemoryStore implements StorageAdapter {
82
106
  private projects = new Map<string, Project>();
83
107
  private workflows = new Map<string, Workflow>();
@@ -155,6 +155,13 @@ export interface Task {
155
155
  updatedAt: string;
156
156
  }
157
157
 
158
+ export type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'status' | 'priority'> & {
159
+ status?: TaskStatus;
160
+ priority?: Priority;
161
+ };
162
+
163
+ export type CreateProjectInput = Omit<Project, 'id' | 'createdAt' | 'updatedAt'>;
164
+
158
165
  export interface Iteration {
159
166
  id: string;
160
167
  projectId: string;
@@ -54,7 +54,7 @@ export const DEFAULT_SOFTWARE_WORKFLOW: Workflow = {
54
54
  ],
55
55
  taskTypes: DEFAULT_TASK_TYPES,
56
56
  defaultStatusKey: 'todo',
57
- isDefault: true,
57
+ isDefault: false,
58
58
  createdAt: '2026-01-01T00:00:00.000Z',
59
59
  updatedAt: '2026-01-01T00:00:00.000Z'
60
60
  };
@@ -1 +1 @@
1
- {"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
1
+ {"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}