@critical-path/core 0.5.2 → 0.7.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 (90) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +57 -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 +153 -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 +32 -2
  28. package/dist/engine/index.d.ts.map +1 -1
  29. package/dist/engine/index.js +416 -6
  30. package/dist/engine/index.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/storage/file-storage.d.ts +24 -0
  36. package/dist/storage/file-storage.d.ts.map +1 -0
  37. package/dist/storage/file-storage.js +73 -0
  38. package/dist/storage/file-storage.js.map +1 -0
  39. package/dist/storage/firebase-storage.d.ts +48 -0
  40. package/dist/storage/firebase-storage.d.ts.map +1 -0
  41. package/dist/storage/firebase-storage.js +142 -0
  42. package/dist/storage/firebase-storage.js.map +1 -0
  43. package/dist/storage/index.d.ts +4 -0
  44. package/dist/storage/index.d.ts.map +1 -0
  45. package/dist/storage/index.js +4 -0
  46. package/dist/storage/index.js.map +1 -0
  47. package/dist/storage/s3.d.ts +36 -0
  48. package/dist/storage/s3.d.ts.map +1 -0
  49. package/dist/storage/s3.js +135 -0
  50. package/dist/storage/s3.js.map +1 -0
  51. package/dist/storage/storage.test.d.ts +2 -0
  52. package/dist/storage/storage.test.d.ts.map +1 -0
  53. package/dist/storage/storage.test.js +117 -0
  54. package/dist/storage/storage.test.js.map +1 -0
  55. package/dist/store/firebase.d.ts +12 -1
  56. package/dist/store/firebase.d.ts.map +1 -1
  57. package/dist/store/firebase.js +63 -1
  58. package/dist/store/firebase.js.map +1 -1
  59. package/dist/store/index.d.ts +49 -2
  60. package/dist/store/index.d.ts.map +1 -1
  61. package/dist/store/index.js +49 -0
  62. package/dist/store/index.js.map +1 -1
  63. package/dist/store/sqlite.d.ts +12 -1
  64. package/dist/store/sqlite.d.ts.map +1 -1
  65. package/dist/store/sqlite.js +128 -6
  66. package/dist/store/sqlite.js.map +1 -1
  67. package/dist/store/sqlite.test.js +47 -1
  68. package/dist/store/sqlite.test.js.map +1 -1
  69. package/dist/types/index.d.ts +57 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/src/domain/custom-fields.ts +131 -0
  73. package/src/domain/ddd.test.ts +307 -0
  74. package/src/domain/entities.ts +390 -0
  75. package/src/domain/events.ts +196 -0
  76. package/src/domain/graph.ts +70 -0
  77. package/src/domain/index.ts +4 -0
  78. package/src/engine/index.ts +518 -8
  79. package/src/index.ts +2 -0
  80. package/src/storage/file-storage.ts +96 -0
  81. package/src/storage/firebase-storage.ts +177 -0
  82. package/src/storage/index.ts +3 -0
  83. package/src/storage/s3.ts +165 -0
  84. package/src/storage/storage.test.ts +146 -0
  85. package/src/store/firebase.ts +64 -1
  86. package/src/store/index.ts +99 -11
  87. package/src/store/sqlite.test.ts +59 -1
  88. package/src/store/sqlite.ts +172 -6
  89. package/src/types/index.ts +70 -0
  90. package/tsconfig.tsbuildinfo +1 -1
@@ -1,10 +1,13 @@
1
- import type { CriticalPathConfig, Project, Task, TaskDependencyGraph, Team, TaskContainer, Iteration, Workflow } from '../types/index.js';
1
+ import type { CriticalPathConfig, Project, Task, TaskDependency, TaskDependencyGraph, Team, TaskContainer, Iteration, Comment, Attachment, FileStorageAdapter, UploadFileInput, PresignedUrlOptions, PresignedUploadResult, TimeEntry, Workflow, CreateTaskInput } from '../types/index.js';
2
2
  import { StorageAdapter } from '../store/index.js';
3
3
  import { PluginRegistry } from '../plugins/index.js';
4
4
  import { type TaskLifecycleState } from '../utils/status.js';
5
+ import { DomainEventBus } from '../domain/events.js';
5
6
  export declare class CriticalPathEngine {
6
7
  readonly store: StorageAdapter;
8
+ readonly fileStorage?: FileStorageAdapter;
7
9
  readonly plugins: PluginRegistry;
10
+ readonly events: DomainEventBus;
8
11
  constructor(config?: CriticalPathConfig);
9
12
  private seedInitialData;
10
13
  getWorkflows(): Promise<Workflow[]>;
@@ -17,13 +20,40 @@ export declare class CriticalPathEngine {
17
20
  getProjects(): Promise<Project[]>;
18
21
  getProject(id: string): Promise<Project | null>;
19
22
  createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project>;
23
+ updateProject(id: string, updates: Partial<Project>): Promise<Project | null>;
20
24
  getTasks(projectId?: string): Promise<Task[]>;
21
25
  getTask(id: string): Promise<Task | null>;
22
- createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
26
+ createTask(taskInput: CreateTaskInput): Promise<Task>;
23
27
  updateTask(id: string, updates: Partial<Task>): Promise<Task | null>;
24
28
  deleteTask(id: string): Promise<boolean>;
29
+ addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency>;
25
30
  getTaskDependencyGraph(taskId: string): Promise<TaskDependencyGraph>;
26
31
  getTaskLifecycleState(taskId: string): Promise<TaskLifecycleState | null>;
32
+ logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt' | 'userId'> & {
33
+ userId?: string;
34
+ }): Promise<TimeEntry>;
35
+ getComments(taskId: string): Promise<Comment[]>;
36
+ getComment(id: string): Promise<Comment | null>;
37
+ addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
38
+ updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null>;
39
+ deleteComment(id: string): Promise<boolean>;
40
+ getAttachments(filter?: {
41
+ taskId?: string;
42
+ projectId?: string;
43
+ commentId?: string;
44
+ }): Promise<Attachment[]>;
45
+ getAttachment(id: string): Promise<Attachment | null>;
46
+ createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
47
+ deleteAttachment(id: string): Promise<boolean>;
48
+ uploadAttachmentFile(input: UploadFileInput & {
49
+ taskId?: string;
50
+ projectId?: string;
51
+ commentId?: string;
52
+ uploaderId: string;
53
+ uploaderType?: 'user' | 'agent' | 'system';
54
+ metadata?: Record<string, unknown>;
55
+ }): Promise<Attachment>;
56
+ getPresignedAttachmentUploadUrl(options: PresignedUrlOptions): Promise<PresignedUploadResult>;
27
57
  getTeams(): Promise<Team[]>;
28
58
  getTeam(id: string): Promise<Team | null>;
29
59
  createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,22 @@
1
1
  import { InMemoryStore } from '../store/index.js';
2
2
  import { PluginRegistry } from '../plugins/index.js';
3
- import { deriveTaskLifecycleState } from '../utils/status.js';
3
+ import { deriveTaskLifecycleState, resolveStatusDefinition } from '../utils/status.js';
4
4
  import { validateTransition, getAllowedNextStatuses, WorkflowValidationError, DEFAULT_SOFTWARE_WORKFLOW } from '../utils/workflow.js';
5
+ import { DomainEventBus } from '../domain/events.js';
6
+ import { validateCustomFieldValues } from '../domain/custom-fields.js';
7
+ import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
5
8
  export class CriticalPathEngine {
6
9
  store;
10
+ fileStorage;
7
11
  plugins;
12
+ events;
8
13
  constructor(config = {}) {
9
14
  this.store = typeof config.store === 'object' && config.store !== null
10
15
  ? config.store
11
16
  : new InMemoryStore();
17
+ this.fileStorage = config.fileStorage;
12
18
  this.plugins = new PluginRegistry();
19
+ this.events = new DomainEventBus();
13
20
  if (config.plugins) {
14
21
  for (const plugin of config.plugins) {
15
22
  this.plugins.register(plugin);
@@ -60,6 +67,16 @@ export class CriticalPathEngine {
60
67
  }
61
68
  async createWorkflow(workflow) {
62
69
  const created = await this.store.createWorkflow(workflow);
70
+ const now = new Date().toISOString();
71
+ const event = {
72
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
73
+ name: 'workflow.created',
74
+ aggregateId: created.id,
75
+ aggregateType: 'Workflow',
76
+ occurredAt: now,
77
+ payload: { workflow: created }
78
+ };
79
+ await this.events.publish(event);
63
80
  await this.store.logActivity({
64
81
  actorId: 'system',
65
82
  action: 'workflow.created',
@@ -74,6 +91,16 @@ export class CriticalPathEngine {
74
91
  return null;
75
92
  const updated = await this.store.updateWorkflow(id, updates);
76
93
  if (updated) {
94
+ const now = new Date().toISOString();
95
+ const event = {
96
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
97
+ name: 'workflow.updated',
98
+ aggregateId: updated.id,
99
+ aggregateType: 'Workflow',
100
+ occurredAt: now,
101
+ payload: { workflow: updated, previous: existing }
102
+ };
103
+ await this.events.publish(event);
77
104
  await this.store.logActivity({
78
105
  actorId: 'system',
79
106
  action: 'workflow.updated',
@@ -89,6 +116,16 @@ export class CriticalPathEngine {
89
116
  return false;
90
117
  const deleted = await this.store.deleteWorkflow(id);
91
118
  if (deleted) {
119
+ const now = new Date().toISOString();
120
+ const event = {
121
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
122
+ name: 'workflow.deleted',
123
+ aggregateId: id,
124
+ aggregateType: 'Workflow',
125
+ occurredAt: now,
126
+ payload: { workflowId: id, name: existing.name }
127
+ };
128
+ await this.events.publish(event);
92
129
  await this.store.logActivity({
93
130
  actorId: 'system',
94
131
  action: 'workflow.deleted',
@@ -131,6 +168,16 @@ export class CriticalPathEngine {
131
168
  }
132
169
  async createProject(project) {
133
170
  const created = await this.store.createProject(project);
171
+ const now = new Date().toISOString();
172
+ const event = {
173
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
174
+ name: 'project.created',
175
+ aggregateId: created.id,
176
+ aggregateType: 'Project',
177
+ occurredAt: now,
178
+ payload: { project: created }
179
+ };
180
+ await this.events.publish(event);
134
181
  await this.store.logActivity({
135
182
  projectId: created.id,
136
183
  actorId: project.ownerId || 'system',
@@ -140,6 +187,32 @@ export class CriticalPathEngine {
140
187
  this.dispatchWebhook('project.created', { project: created });
141
188
  return created;
142
189
  }
190
+ async updateProject(id, updates) {
191
+ const existing = await this.store.getProject(id);
192
+ if (!existing)
193
+ return null;
194
+ const updated = await this.store.updateProject(id, updates);
195
+ if (updated) {
196
+ const now = new Date().toISOString();
197
+ const event = {
198
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
199
+ name: 'project.updated',
200
+ aggregateId: updated.id,
201
+ aggregateType: 'Project',
202
+ occurredAt: now,
203
+ payload: { project: updated, previous: existing }
204
+ };
205
+ await this.events.publish(event);
206
+ await this.store.logActivity({
207
+ projectId: updated.id,
208
+ actorId: 'system',
209
+ action: 'project.updated',
210
+ details: { name: updated.name }
211
+ });
212
+ this.dispatchWebhook('project.updated', { project: updated });
213
+ }
214
+ return updated;
215
+ }
143
216
  // --- Tasks ---
144
217
  async getTasks(projectId) {
145
218
  return this.store.getTasks(projectId);
@@ -150,13 +223,24 @@ export class CriticalPathEngine {
150
223
  async createTask(taskInput) {
151
224
  const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
152
225
  const projectId = processedInput.projectId || taskInput.projectId;
226
+ const project = await this.store.getProject(projectId);
153
227
  const workflow = await this.resolveProjectWorkflow(projectId);
228
+ // Validate custom field domain invariants
229
+ if (project?.customFieldDefinitions && (processedInput.customFields || taskInput.customFields)) {
230
+ validateCustomFieldValues(project.customFieldDefinitions, processedInput.customFields || taskInput.customFields);
231
+ }
154
232
  const defaultStatus = workflow?.defaultStatusKey || 'todo';
233
+ const initialStatus = processedInput.status || taskInput.status || defaultStatus;
234
+ const now = new Date().toISOString();
235
+ // Derive initial lifecycle timestamps
236
+ const statusDef = resolveStatusDefinition(initialStatus, project?.statusDefinitions || workflow?.statuses);
237
+ const actualStartDate = processedInput.actualStartDate ?? taskInput.actualStartDate ?? (statusDef.executionState === 'active' ? now : undefined);
238
+ const actualEndDate = processedInput.actualEndDate ?? taskInput.actualEndDate ?? (statusDef.completionState === 'done' ? now : undefined);
155
239
  const created = await this.store.createTask({
156
240
  projectId,
157
241
  title: processedInput.title || taskInput.title,
158
242
  description: processedInput.description ?? taskInput.description,
159
- status: processedInput.status || taskInput.status || defaultStatus,
243
+ status: initialStatus,
160
244
  priority: processedInput.priority || taskInput.priority || 'medium',
161
245
  taskType: processedInput.taskType || taskInput.taskType || 'task',
162
246
  assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
@@ -166,8 +250,8 @@ export class CriticalPathEngine {
166
250
  teamId: processedInput.teamId ?? taskInput.teamId,
167
251
  containerId: processedInput.containerId ?? taskInput.containerId,
168
252
  plannedStartDate: processedInput.plannedStartDate ?? taskInput.plannedStartDate,
169
- actualStartDate: processedInput.actualStartDate ?? taskInput.actualStartDate,
170
- actualEndDate: processedInput.actualEndDate ?? taskInput.actualEndDate,
253
+ actualStartDate,
254
+ actualEndDate,
171
255
  dueDate: processedInput.dueDate ?? taskInput.dueDate,
172
256
  estimatedHours: processedInput.estimatedHours ?? taskInput.estimatedHours,
173
257
  loggedHours: processedInput.loggedHours ?? taskInput.loggedHours ?? 0,
@@ -176,12 +260,22 @@ export class CriticalPathEngine {
176
260
  estimatedDurationMinutes: processedInput.estimatedDurationMinutes ?? taskInput.estimatedDurationMinutes,
177
261
  actualDurationMinutes: processedInput.actualDurationMinutes ?? taskInput.actualDurationMinutes,
178
262
  billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
179
- progress: processedInput.progress ?? taskInput.progress ?? 0,
263
+ progress: processedInput.progress ?? taskInput.progress ?? (statusDef.completionState === 'done' ? 100 : 0),
180
264
  tags: processedInput.tags ?? taskInput.tags ?? [],
181
265
  customFields: processedInput.customFields ?? taskInput.customFields ?? {},
182
266
  parentId: processedInput.parentId ?? taskInput.parentId
183
267
  });
184
268
  await this.plugins.runAfterTaskCreate(created);
269
+ // Publish typed Domain Event
270
+ const event = {
271
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
272
+ name: 'task.created',
273
+ aggregateId: created.id,
274
+ aggregateType: 'Task',
275
+ occurredAt: now,
276
+ payload: { task: created }
277
+ };
278
+ await this.events.publish(event);
185
279
  await this.store.logActivity({
186
280
  projectId: created.projectId,
187
281
  taskId: created.id,
@@ -196,19 +290,73 @@ export class CriticalPathEngine {
196
290
  const existing = await this.store.getTask(id);
197
291
  if (!existing)
198
292
  return null;
293
+ const project = await this.store.getProject(existing.projectId);
294
+ const workflow = await this.resolveProjectWorkflow(existing.projectId);
295
+ // Validate workflow transition invariant
199
296
  if (updates.status && updates.status !== existing.status) {
200
- const workflow = await this.resolveProjectWorkflow(existing.projectId);
201
297
  const isValid = validateTransition(workflow || undefined, existing.status, updates.status);
202
298
  if (!isValid) {
203
299
  throw new WorkflowValidationError(existing.status, updates.status, workflow?.id);
204
300
  }
205
301
  }
302
+ // Validate custom field invariants
303
+ if (project?.customFieldDefinitions && updates.customFields) {
304
+ validateCustomFieldValues(project.customFieldDefinitions, {
305
+ ...existing.customFields,
306
+ ...updates.customFields
307
+ });
308
+ }
206
309
  const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
310
+ // Auto-update execution/completion timestamps if status changes
311
+ if (processedUpdates.status && processedUpdates.status !== existing.status) {
312
+ const statusDef = resolveStatusDefinition(processedUpdates.status, project?.statusDefinitions || workflow?.statuses);
313
+ const now = new Date().toISOString();
314
+ if (statusDef.executionState === 'active' && !existing.actualStartDate && !processedUpdates.actualStartDate) {
315
+ processedUpdates.actualStartDate = now;
316
+ }
317
+ if (statusDef.completionState === 'done' && !processedUpdates.actualEndDate) {
318
+ processedUpdates.actualEndDate = now;
319
+ if (processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
320
+ processedUpdates.progress = 100;
321
+ }
322
+ }
323
+ }
207
324
  const updated = await this.store.updateTask(id, processedUpdates);
208
325
  if (!updated)
209
326
  return null;
210
327
  await this.plugins.runAfterTaskUpdate(updated, existing);
328
+ const now = new Date().toISOString();
211
329
  const isStatusChange = existing.status !== updated.status;
330
+ // Publish typed Domain Events
331
+ if (isStatusChange) {
332
+ const statusEvent = {
333
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
334
+ name: 'task.status_changed',
335
+ aggregateId: updated.id,
336
+ aggregateType: 'Task',
337
+ occurredAt: now,
338
+ payload: {
339
+ task: updated,
340
+ previousStatus: existing.status,
341
+ newStatus: updated.status
342
+ }
343
+ };
344
+ await this.events.publish(statusEvent);
345
+ }
346
+ else {
347
+ const updateEvent = {
348
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
349
+ name: 'task.updated',
350
+ aggregateId: updated.id,
351
+ aggregateType: 'Task',
352
+ occurredAt: now,
353
+ payload: {
354
+ task: updated,
355
+ previous: existing
356
+ }
357
+ };
358
+ await this.events.publish(updateEvent);
359
+ }
212
360
  await this.store.logActivity({
213
361
  projectId: updated.projectId,
214
362
  taskId: updated.id,
@@ -230,6 +378,20 @@ export class CriticalPathEngine {
230
378
  const deleted = await this.store.deleteTask(id);
231
379
  if (deleted) {
232
380
  await this.plugins.runAfterTaskDelete(id);
381
+ const now = new Date().toISOString();
382
+ const event = {
383
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
384
+ name: 'task.deleted',
385
+ aggregateId: id,
386
+ aggregateType: 'Task',
387
+ occurredAt: now,
388
+ payload: {
389
+ taskId: id,
390
+ projectId: existing.projectId,
391
+ title: existing.title
392
+ }
393
+ };
394
+ await this.events.publish(event);
233
395
  await this.store.logActivity({
234
396
  projectId: existing.projectId,
235
397
  taskId: id,
@@ -242,6 +404,30 @@ export class CriticalPathEngine {
242
404
  return deleted;
243
405
  }
244
406
  // --- Task Dependencies & Graph ---
407
+ async addDependency(dep) {
408
+ const existingDependencies = await this.store.getDependencies(dep.taskId);
409
+ const allProjectDeps = [
410
+ ...existingDependencies,
411
+ ...(await this.store.getDependencies(dep.dependsOnTaskId))
412
+ ];
413
+ // Enforce Directed Acyclic Graph (DAG) Invariant
414
+ const cycleCheck = detectDependencyCycle(allProjectDeps, dep);
415
+ if (cycleCheck.hasCycle) {
416
+ throw new CircularDependencyError(dep.taskId, dep.dependsOnTaskId, cycleCheck.cyclePath);
417
+ }
418
+ const created = await this.store.addDependency(dep);
419
+ const now = new Date().toISOString();
420
+ const event = {
421
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
422
+ name: 'dependency.added',
423
+ aggregateId: created.id,
424
+ aggregateType: 'Dependency',
425
+ occurredAt: now,
426
+ payload: { dependency: created }
427
+ };
428
+ await this.events.publish(event);
429
+ return created;
430
+ }
245
431
  async getTaskDependencyGraph(taskId) {
246
432
  const dependencies = await this.store.getDependencies(taskId);
247
433
  const upstreamTaskIds = new Set();
@@ -272,6 +458,181 @@ export class CriticalPathEngine {
272
458
  const statusDefs = project?.statusDefinitions || workflow?.statuses;
273
459
  return deriveTaskLifecycleState(task, statusDefs);
274
460
  }
461
+ // --- Time Tracking ---
462
+ async logTime(entry) {
463
+ if (entry.hours <= 0) {
464
+ throw new Error('Logged hours must be a positive number.');
465
+ }
466
+ const task = await this.store.getTask(entry.taskId);
467
+ if (task) {
468
+ const newLoggedHours = (task.loggedHours || 0) + entry.hours;
469
+ const newActualHours = (task.actualHours || 0) + entry.hours;
470
+ const newBillableHours = entry.isBillable !== false ? (task.billableHours || 0) + entry.hours : task.billableHours;
471
+ await this.store.updateTask(task.id, {
472
+ loggedHours: newLoggedHours,
473
+ actualHours: newActualHours,
474
+ billableHours: newBillableHours
475
+ });
476
+ }
477
+ const fullEntry = {
478
+ ...entry,
479
+ userId: entry.userId || task?.assigneeId || 'system'
480
+ };
481
+ const created = await this.store.logTime(fullEntry);
482
+ const now = new Date().toISOString();
483
+ const event = {
484
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
485
+ name: 'time.logged',
486
+ aggregateId: entry.taskId,
487
+ aggregateType: 'Task',
488
+ occurredAt: now,
489
+ payload: { taskId: entry.taskId, timeEntry: created }
490
+ };
491
+ await this.events.publish(event);
492
+ return created;
493
+ }
494
+ // --- Comments ---
495
+ async getComments(taskId) {
496
+ return this.store.getComments(taskId);
497
+ }
498
+ async getComment(id) {
499
+ return this.store.getComment(id);
500
+ }
501
+ async addComment(comment) {
502
+ const created = await this.store.addComment(comment);
503
+ const now = new Date().toISOString();
504
+ const event = {
505
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
506
+ name: 'comment.created',
507
+ aggregateId: created.id,
508
+ aggregateType: 'Comment',
509
+ occurredAt: now,
510
+ payload: { taskId: comment.taskId, comment: created }
511
+ };
512
+ await this.events.publish(event);
513
+ this.dispatchWebhook('comment.created', { comment: created });
514
+ return created;
515
+ }
516
+ async updateComment(id, updates) {
517
+ const existing = await this.store.getComment(id);
518
+ if (!existing)
519
+ return null;
520
+ const updated = await this.store.updateComment(id, updates);
521
+ if (!updated)
522
+ return null;
523
+ const now = new Date().toISOString();
524
+ const event = {
525
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
526
+ name: 'comment.updated',
527
+ aggregateId: updated.id,
528
+ aggregateType: 'Comment',
529
+ occurredAt: now,
530
+ payload: { comment: updated, previous: existing }
531
+ };
532
+ await this.events.publish(event);
533
+ this.dispatchWebhook('comment.updated', { comment: updated });
534
+ return updated;
535
+ }
536
+ async deleteComment(id) {
537
+ const existing = await this.store.getComment(id);
538
+ if (!existing)
539
+ return false;
540
+ const deleted = await this.store.deleteComment(id);
541
+ if (deleted) {
542
+ const now = new Date().toISOString();
543
+ const event = {
544
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
545
+ name: 'comment.deleted',
546
+ aggregateId: id,
547
+ aggregateType: 'Comment',
548
+ occurredAt: now,
549
+ payload: { taskId: existing.taskId, commentId: id }
550
+ };
551
+ await this.events.publish(event);
552
+ this.dispatchWebhook('comment.deleted', { commentId: id, taskId: existing.taskId });
553
+ }
554
+ return deleted;
555
+ }
556
+ // --- Attachments ---
557
+ async getAttachments(filter) {
558
+ return this.store.getAttachments(filter);
559
+ }
560
+ async getAttachment(id) {
561
+ return this.store.getAttachment(id);
562
+ }
563
+ async createAttachment(attachment) {
564
+ const created = await this.store.createAttachment(attachment);
565
+ const now = new Date().toISOString();
566
+ const event = {
567
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
568
+ name: 'attachment.created',
569
+ aggregateId: created.id,
570
+ aggregateType: 'Attachment',
571
+ occurredAt: now,
572
+ payload: { attachment: created }
573
+ };
574
+ await this.events.publish(event);
575
+ this.dispatchWebhook('attachment.created', { attachment: created });
576
+ return created;
577
+ }
578
+ async deleteAttachment(id) {
579
+ const existing = await this.store.getAttachment(id);
580
+ if (!existing)
581
+ return false;
582
+ if (existing.storageKey && this.fileStorage) {
583
+ try {
584
+ await this.fileStorage.delete(existing.storageKey);
585
+ }
586
+ catch {
587
+ // Silently continue if underlying file is already gone
588
+ }
589
+ }
590
+ const deleted = await this.store.deleteAttachment(id);
591
+ if (deleted) {
592
+ const now = new Date().toISOString();
593
+ const event = {
594
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
595
+ name: 'attachment.deleted',
596
+ aggregateId: id,
597
+ aggregateType: 'Attachment',
598
+ occurredAt: now,
599
+ payload: { attachmentId: id, storageKey: existing.storageKey, url: existing.url }
600
+ };
601
+ await this.events.publish(event);
602
+ this.dispatchWebhook('attachment.deleted', { attachmentId: id, taskId: existing.taskId, projectId: existing.projectId });
603
+ }
604
+ return deleted;
605
+ }
606
+ async uploadAttachmentFile(input) {
607
+ if (!this.fileStorage) {
608
+ throw new Error('No FileStorageAdapter configured in CriticalPathEngine. Pass "fileStorage" in config to enable direct file uploads.');
609
+ }
610
+ const uploadResult = await this.fileStorage.upload({
611
+ filename: input.filename,
612
+ data: input.data,
613
+ mimeType: input.mimeType,
614
+ pathPrefix: input.pathPrefix || (input.projectId ? `projects/${input.projectId}` : input.taskId ? `tasks/${input.taskId}` : undefined)
615
+ });
616
+ return this.createAttachment({
617
+ taskId: input.taskId,
618
+ projectId: input.projectId,
619
+ commentId: input.commentId,
620
+ uploaderId: input.uploaderId,
621
+ uploaderType: input.uploaderType || 'user',
622
+ filename: input.filename,
623
+ mimeType: uploadResult.mimeType,
624
+ sizeBytes: uploadResult.sizeBytes,
625
+ url: uploadResult.url,
626
+ storageKey: uploadResult.storageKey,
627
+ metadata: input.metadata
628
+ });
629
+ }
630
+ async getPresignedAttachmentUploadUrl(options) {
631
+ if (!this.fileStorage || !this.fileStorage.getPresignedUploadUrl) {
632
+ throw new Error('Presigned uploads are not supported by the configured FileStorageAdapter.');
633
+ }
634
+ return this.fileStorage.getPresignedUploadUrl(options);
635
+ }
275
636
  // --- Teams ---
276
637
  async getTeams() {
277
638
  return this.store.getTeams();
@@ -281,6 +642,16 @@ export class CriticalPathEngine {
281
642
  }
282
643
  async createTeam(team) {
283
644
  const created = await this.store.createTeam(team);
645
+ const now = new Date().toISOString();
646
+ const event = {
647
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
648
+ name: 'team.created',
649
+ aggregateId: created.id,
650
+ aggregateType: 'Team',
651
+ occurredAt: now,
652
+ payload: { team: created }
653
+ };
654
+ await this.events.publish(event);
284
655
  this.dispatchWebhook('team.created', { team: created });
285
656
  return created;
286
657
  }
@@ -299,6 +670,16 @@ export class CriticalPathEngine {
299
670
  }
300
671
  async createContainer(container) {
301
672
  const created = await this.store.createContainer(container);
673
+ const now = new Date().toISOString();
674
+ const event = {
675
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
676
+ name: 'container.created',
677
+ aggregateId: created.id,
678
+ aggregateType: 'Container',
679
+ occurredAt: now,
680
+ payload: { container: created }
681
+ };
682
+ await this.events.publish(event);
302
683
  this.dispatchWebhook('container.created', { container: created });
303
684
  return created;
304
685
  }
@@ -318,6 +699,16 @@ export class CriticalPathEngine {
318
699
  async createIteration(iteration) {
319
700
  const created = await this.store.createIteration(iteration);
320
701
  if (created.status === 'active') {
702
+ const now = new Date().toISOString();
703
+ const event = {
704
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
705
+ name: 'iteration.started',
706
+ aggregateId: created.id,
707
+ aggregateType: 'Iteration',
708
+ occurredAt: now,
709
+ payload: { iteration: created }
710
+ };
711
+ await this.events.publish(event);
321
712
  this.dispatchWebhook('iteration.started', { iteration: created });
322
713
  }
323
714
  return created;
@@ -328,10 +719,29 @@ export class CriticalPathEngine {
328
719
  return null;
329
720
  const updated = await this.store.updateIteration(id, updates);
330
721
  if (updated) {
722
+ const now = new Date().toISOString();
331
723
  if (existing.status !== 'active' && updated.status === 'active') {
724
+ const event = {
725
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
726
+ name: 'iteration.started',
727
+ aggregateId: updated.id,
728
+ aggregateType: 'Iteration',
729
+ occurredAt: now,
730
+ payload: { iteration: updated }
731
+ };
732
+ await this.events.publish(event);
332
733
  this.dispatchWebhook('iteration.started', { iteration: updated });
333
734
  }
334
735
  else if (existing.status !== 'completed' && updated.status === 'completed') {
736
+ const event = {
737
+ id: `evt_${Math.random().toString(36).substring(2, 9)}`,
738
+ name: 'iteration.completed',
739
+ aggregateId: updated.id,
740
+ aggregateType: 'Iteration',
741
+ occurredAt: now,
742
+ payload: { iteration: updated }
743
+ };
744
+ await this.events.publish(event);
335
745
  this.dispatchWebhook('iteration.completed', { iteration: updated });
336
746
  }
337
747
  }