@critical-path/core 0.36.0 → 0.37.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 (214) hide show
  1. package/README.md +1 -1
  2. package/dist/domain/authorization.d.ts.map +1 -1
  3. package/dist/domain/authorization.js +10 -7
  4. package/dist/domain/authorization.js.map +1 -1
  5. package/dist/engine/index.d.ts +6 -0
  6. package/dist/engine/index.d.ts.map +1 -1
  7. package/dist/engine/index.js +28 -2
  8. package/dist/engine/index.js.map +1 -1
  9. package/dist/schemas/index.d.ts +42 -12
  10. package/dist/schemas/index.d.ts.map +1 -1
  11. package/dist/schemas/index.js +4 -1
  12. package/dist/schemas/index.js.map +1 -1
  13. package/dist/store/firebase.d.ts +2 -2
  14. package/dist/store/firebase.d.ts.map +1 -1
  15. package/dist/store/firebase.js +4 -2
  16. package/dist/store/firebase.js.map +1 -1
  17. package/dist/store/index.d.ts +7 -2
  18. package/dist/store/index.d.ts.map +1 -1
  19. package/dist/store/index.js +3 -2
  20. package/dist/store/index.js.map +1 -1
  21. package/dist/store/sqlite.d.ts +2 -2
  22. package/dist/store/sqlite.d.ts.map +1 -1
  23. package/dist/store/sqlite.js +5 -3
  24. package/dist/store/sqlite.js.map +1 -1
  25. package/dist/testing/conformance.d.ts.map +1 -1
  26. package/dist/testing/conformance.js +9 -0
  27. package/dist/testing/conformance.js.map +1 -1
  28. package/dist/types/index.d.ts +13 -2
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/package.json +5 -1
  31. package/CHANGELOG.md +0 -593
  32. package/dist/deliverables.test.d.ts +0 -2
  33. package/dist/deliverables.test.d.ts.map +0 -1
  34. package/dist/deliverables.test.js +0 -232
  35. package/dist/deliverables.test.js.map +0 -1
  36. package/dist/domain/calendar.test.d.ts +0 -2
  37. package/dist/domain/calendar.test.d.ts.map +0 -1
  38. package/dist/domain/calendar.test.js +0 -181
  39. package/dist/domain/calendar.test.js.map +0 -1
  40. package/dist/domain/cpm.test.d.ts +0 -2
  41. package/dist/domain/cpm.test.d.ts.map +0 -1
  42. package/dist/domain/cpm.test.js +0 -166
  43. package/dist/domain/cpm.test.js.map +0 -1
  44. package/dist/domain/ddd.test.d.ts +0 -2
  45. package/dist/domain/ddd.test.d.ts.map +0 -1
  46. package/dist/domain/ddd.test.js +0 -349
  47. package/dist/domain/ddd.test.js.map +0 -1
  48. package/dist/domain/ladder.test.d.ts +0 -2
  49. package/dist/domain/ladder.test.d.ts.map +0 -1
  50. package/dist/domain/ladder.test.js +0 -213
  51. package/dist/domain/ladder.test.js.map +0 -1
  52. package/dist/domain/metrics.test.d.ts +0 -2
  53. package/dist/domain/metrics.test.d.ts.map +0 -1
  54. package/dist/domain/metrics.test.js +0 -252
  55. package/dist/domain/metrics.test.js.map +0 -1
  56. package/dist/domain/task-duration.test.d.ts +0 -2
  57. package/dist/domain/task-duration.test.d.ts.map +0 -1
  58. package/dist/domain/task-duration.test.js +0 -307
  59. package/dist/domain/task-duration.test.js.map +0 -1
  60. package/dist/domain/workload.test.d.ts +0 -2
  61. package/dist/domain/workload.test.d.ts.map +0 -1
  62. package/dist/domain/workload.test.js +0 -261
  63. package/dist/domain/workload.test.js.map +0 -1
  64. package/dist/engine/actor.test.d.ts +0 -2
  65. package/dist/engine/actor.test.d.ts.map +0 -1
  66. package/dist/engine/actor.test.js +0 -54
  67. package/dist/engine/actor.test.js.map +0 -1
  68. package/dist/engine/authorization.test.d.ts +0 -2
  69. package/dist/engine/authorization.test.d.ts.map +0 -1
  70. package/dist/engine/authorization.test.js +0 -154
  71. package/dist/engine/authorization.test.js.map +0 -1
  72. package/dist/engine/cascade.test.d.ts +0 -2
  73. package/dist/engine/cascade.test.d.ts.map +0 -1
  74. package/dist/engine/cascade.test.js +0 -99
  75. package/dist/engine/cascade.test.js.map +0 -1
  76. package/dist/engine/config.test.d.ts +0 -2
  77. package/dist/engine/config.test.d.ts.map +0 -1
  78. package/dist/engine/config.test.js +0 -46
  79. package/dist/engine/config.test.js.map +0 -1
  80. package/dist/engine/entity-events.test.d.ts +0 -2
  81. package/dist/engine/entity-events.test.d.ts.map +0 -1
  82. package/dist/engine/entity-events.test.js +0 -61
  83. package/dist/engine/entity-events.test.js.map +0 -1
  84. package/dist/index.test.d.ts +0 -2
  85. package/dist/index.test.d.ts.map +0 -1
  86. package/dist/index.test.js +0 -429
  87. package/dist/index.test.js.map +0 -1
  88. package/dist/plugins/plugins.test.d.ts +0 -2
  89. package/dist/plugins/plugins.test.d.ts.map +0 -1
  90. package/dist/plugins/plugins.test.js +0 -137
  91. package/dist/plugins/plugins.test.js.map +0 -1
  92. package/dist/schemas/schemas.test.d.ts +0 -2
  93. package/dist/schemas/schemas.test.d.ts.map +0 -1
  94. package/dist/schemas/schemas.test.js +0 -58
  95. package/dist/schemas/schemas.test.js.map +0 -1
  96. package/dist/status.test.d.ts +0 -2
  97. package/dist/status.test.d.ts.map +0 -1
  98. package/dist/status.test.js +0 -295
  99. package/dist/status.test.js.map +0 -1
  100. package/dist/storage/storage.test.d.ts +0 -2
  101. package/dist/storage/storage.test.d.ts.map +0 -1
  102. package/dist/storage/storage.test.js +0 -236
  103. package/dist/storage/storage.test.js.map +0 -1
  104. package/dist/storage/upload-limits.test.d.ts +0 -2
  105. package/dist/storage/upload-limits.test.d.ts.map +0 -1
  106. package/dist/storage/upload-limits.test.js +0 -31
  107. package/dist/storage/upload-limits.test.js.map +0 -1
  108. package/dist/store/conformance.test.d.ts +0 -2
  109. package/dist/store/conformance.test.d.ts.map +0 -1
  110. package/dist/store/conformance.test.js +0 -15
  111. package/dist/store/conformance.test.js.map +0 -1
  112. package/dist/store/firebase.test.d.ts +0 -2
  113. package/dist/store/firebase.test.d.ts.map +0 -1
  114. package/dist/store/firebase.test.js +0 -103
  115. package/dist/store/firebase.test.js.map +0 -1
  116. package/dist/store/query.test.d.ts +0 -2
  117. package/dist/store/query.test.d.ts.map +0 -1
  118. package/dist/store/query.test.js +0 -89
  119. package/dist/store/query.test.js.map +0 -1
  120. package/dist/store/sqlite.test.d.ts +0 -2
  121. package/dist/store/sqlite.test.d.ts.map +0 -1
  122. package/dist/store/sqlite.test.js +0 -177
  123. package/dist/store/sqlite.test.js.map +0 -1
  124. package/dist/task-state.test.d.ts +0 -2
  125. package/dist/task-state.test.d.ts.map +0 -1
  126. package/dist/task-state.test.js +0 -423
  127. package/dist/task-state.test.js.map +0 -1
  128. package/dist/use-case-validation.test.d.ts +0 -2
  129. package/dist/use-case-validation.test.d.ts.map +0 -1
  130. package/dist/use-case-validation.test.js +0 -281
  131. package/dist/use-case-validation.test.js.map +0 -1
  132. package/dist/utils/fractional-index.test.d.ts +0 -2
  133. package/dist/utils/fractional-index.test.d.ts.map +0 -1
  134. package/dist/utils/fractional-index.test.js +0 -182
  135. package/dist/utils/fractional-index.test.js.map +0 -1
  136. package/dist/utils/key.test.d.ts +0 -2
  137. package/dist/utils/key.test.d.ts.map +0 -1
  138. package/dist/utils/key.test.js +0 -65
  139. package/dist/utils/key.test.js.map +0 -1
  140. package/dist/utils/mentions.test.d.ts +0 -2
  141. package/dist/utils/mentions.test.d.ts.map +0 -1
  142. package/dist/utils/mentions.test.js +0 -114
  143. package/dist/utils/mentions.test.js.map +0 -1
  144. package/dist/webhooks/webhooks.test.d.ts +0 -2
  145. package/dist/webhooks/webhooks.test.d.ts.map +0 -1
  146. package/dist/webhooks/webhooks.test.js +0 -154
  147. package/dist/webhooks/webhooks.test.js.map +0 -1
  148. package/src/deliverables.test.ts +0 -284
  149. package/src/domain/authorization.ts +0 -82
  150. package/src/domain/calendar.test.ts +0 -216
  151. package/src/domain/calendar.ts +0 -502
  152. package/src/domain/cpm.test.ts +0 -189
  153. package/src/domain/cpm.ts +0 -258
  154. package/src/domain/custom-fields.ts +0 -172
  155. package/src/domain/ddd.test.ts +0 -442
  156. package/src/domain/entities.ts +0 -798
  157. package/src/domain/errors.ts +0 -38
  158. package/src/domain/events.ts +0 -369
  159. package/src/domain/graph.ts +0 -70
  160. package/src/domain/index.ts +0 -12
  161. package/src/domain/ladder.test.ts +0 -251
  162. package/src/domain/ladder.ts +0 -474
  163. package/src/domain/metrics.test.ts +0 -288
  164. package/src/domain/metrics.ts +0 -497
  165. package/src/domain/task-duration.test.ts +0 -354
  166. package/src/domain/workload.test.ts +0 -311
  167. package/src/domain/workload.ts +0 -457
  168. package/src/engine/actor.test.ts +0 -63
  169. package/src/engine/authorization.test.ts +0 -199
  170. package/src/engine/cascade.test.ts +0 -121
  171. package/src/engine/config.test.ts +0 -51
  172. package/src/engine/entity-events.test.ts +0 -67
  173. package/src/engine/index.ts +0 -2495
  174. package/src/index.test.ts +0 -514
  175. package/src/index.ts +0 -9
  176. package/src/plugins/index.ts +0 -103
  177. package/src/plugins/plugins.test.ts +0 -161
  178. package/src/schemas/index.ts +0 -402
  179. package/src/schemas/schemas.test.ts +0 -116
  180. package/src/status.test.ts +0 -354
  181. package/src/storage/file-storage.ts +0 -200
  182. package/src/storage/firebase-storage.ts +0 -209
  183. package/src/storage/index.ts +0 -3
  184. package/src/storage/s3.ts +0 -151
  185. package/src/storage/storage.test.ts +0 -274
  186. package/src/storage/upload-limits.test.ts +0 -41
  187. package/src/store/conformance.test.ts +0 -15
  188. package/src/store/firebase.test.ts +0 -131
  189. package/src/store/firebase.ts +0 -658
  190. package/src/store/index.ts +0 -623
  191. package/src/store/query.test.ts +0 -110
  192. package/src/store/query.ts +0 -113
  193. package/src/store/sqlite.test.ts +0 -218
  194. package/src/store/sqlite.ts +0 -1480
  195. package/src/task-state.test.ts +0 -506
  196. package/src/testing/conformance.ts +0 -319
  197. package/src/testing/index.ts +0 -1
  198. package/src/types/index.ts +0 -871
  199. package/src/use-case-validation.test.ts +0 -324
  200. package/src/utils/fractional-index.test.ts +0 -228
  201. package/src/utils/fractional-index.ts +0 -254
  202. package/src/utils/index.ts +0 -7
  203. package/src/utils/key.test.ts +0 -76
  204. package/src/utils/key.ts +0 -62
  205. package/src/utils/mentions.test.ts +0 -132
  206. package/src/utils/mentions.ts +0 -131
  207. package/src/utils/status.ts +0 -419
  208. package/src/utils/workflow.ts +0 -306
  209. package/src/webhooks/dispatcher.ts +0 -190
  210. package/src/webhooks/index.ts +0 -2
  211. package/src/webhooks/signature.ts +0 -57
  212. package/src/webhooks/webhooks.test.ts +0 -201
  213. package/tsconfig.json +0 -8
  214. package/tsconfig.tsbuildinfo +0 -1
@@ -1,798 +0,0 @@
1
- import type {
2
- ProjectMember,
3
- Task,
4
- Project,
5
- Workflow,
6
- TimeEntry,
7
- Priority,
8
- TaskStatus,
9
- TaskTodoItem,
10
- CustomFieldDefinition,
11
- CreateTaskInput,
12
- CreateProjectInput,
13
- Deliverable,
14
- DeliverableStatus,
15
- CreateDeliverableInput,
16
- SemanticStatus
17
- } from '../types/index.js';
18
- import type {
19
- DomainEvent,
20
- TaskCreatedEvent,
21
- TaskStatusChangedEvent,
22
- TaskUpdatedEvent,
23
- TimeLoggedEvent,
24
- ProjectCreatedEvent,
25
- DeliverableCreatedEvent,
26
- DeliverableUpdatedEvent,
27
- DeliverableStatusChangedEvent
28
- } from './events.js';
29
- import { validateTransition, WorkflowValidationError } from '../utils/workflow.js';
30
- import { resolveStatusDefinition } from '../utils/status.js';
31
- import { validateCustomFieldValues } from './custom-fields.js';
32
- import { generateProjectKey } from '../utils/key.js';
33
- import { ValidationError } from './errors.js';
34
-
35
- export abstract class BaseEntity {
36
- public readonly id: string;
37
- public createdAt: string;
38
- public updatedAt: string;
39
- private uncommittedEvents: DomainEvent[] = [];
40
-
41
- constructor(id: string, createdAt?: string, updatedAt?: string) {
42
- const now = new Date().toISOString();
43
- this.id = id;
44
- this.createdAt = createdAt || now;
45
- this.updatedAt = updatedAt || now;
46
- }
47
-
48
- protected raiseEvent(event: DomainEvent): void {
49
- this.uncommittedEvents.push(event);
50
- }
51
-
52
- public getUncommittedEvents(): DomainEvent[] {
53
- return [...this.uncommittedEvents];
54
- }
55
-
56
- public clearEvents(): void {
57
- this.uncommittedEvents = [];
58
- }
59
-
60
- protected markUpdated(): void {
61
- this.updatedAt = new Date().toISOString();
62
- }
63
- }
64
-
65
- export class TaskEntity extends BaseEntity {
66
- public projectId: string;
67
- public key?: string;
68
- public title: string;
69
- public description?: string;
70
- public status: TaskStatus;
71
- public semanticStatus?: SemanticStatus;
72
- public priority: Priority;
73
- public taskType?: string;
74
- public assigneeId?: string;
75
- public reporterId?: string;
76
- public reviewerId?: string;
77
- public iterationId?: string;
78
- public teamId?: string;
79
- public containerId?: string;
80
- public deliverableId?: string;
81
- public plannedStartDate?: string;
82
- public actualStartDate?: string;
83
- public actualEndDate?: string;
84
- public dueDate?: string;
85
- public estimatedHours?: number;
86
- public loggedHours?: number;
87
- public actualHours?: number;
88
- public billableHours?: number;
89
- public estimatedDurationMinutes?: number;
90
- public actualDurationMinutes?: number;
91
- public billableDurationMinutes?: number;
92
- public actualDurationSeconds?: number;
93
- public inProgressSince?: string | null;
94
- public blockedDurationSeconds?: number;
95
- public blockedSince?: string | null;
96
- public progress?: number;
97
- public isBlocked?: boolean;
98
- public blockedReason?: string | null;
99
- public tags?: string[];
100
- public todos?: TaskTodoItem[];
101
- public customFields?: Record<string, unknown>;
102
- public parentId?: string;
103
-
104
- constructor(data: Task) {
105
- super(data.id, data.createdAt, data.updatedAt);
106
- this.projectId = data.projectId;
107
- this.key = data.key;
108
- this.title = data.title;
109
- this.description = data.description;
110
- this.status = data.status;
111
- this.semanticStatus = data.semanticStatus;
112
- this.priority = data.priority;
113
- this.taskType = data.taskType;
114
- this.assigneeId = data.assigneeId;
115
- this.reporterId = data.reporterId;
116
- this.reviewerId = data.reviewerId;
117
- this.iterationId = data.iterationId;
118
- this.teamId = data.teamId;
119
- this.containerId = data.containerId;
120
- this.deliverableId = data.deliverableId;
121
- this.plannedStartDate = data.plannedStartDate;
122
- this.actualStartDate = data.actualStartDate;
123
- this.actualEndDate = data.actualEndDate;
124
- this.dueDate = data.dueDate;
125
- this.estimatedHours = data.estimatedHours;
126
- this.loggedHours = data.loggedHours ?? 0;
127
- this.actualHours = data.actualHours;
128
- this.billableHours = data.billableHours;
129
- this.estimatedDurationMinutes = data.estimatedDurationMinutes;
130
- this.actualDurationMinutes = data.actualDurationMinutes;
131
- this.billableDurationMinutes = data.billableDurationMinutes;
132
- this.actualDurationSeconds = data.actualDurationSeconds;
133
- this.inProgressSince = data.inProgressSince;
134
- this.blockedDurationSeconds = data.blockedDurationSeconds;
135
- this.blockedSince = data.blockedSince;
136
- this.progress = data.progress ?? 0;
137
- this.isBlocked = data.isBlocked;
138
- this.blockedReason = data.blockedReason;
139
- this.tags = data.tags ? [...data.tags] : [];
140
- this.todos = data.todos ? data.todos.map((t) => ({ ...t })) : [];
141
- this.customFields = data.customFields ? { ...data.customFields } : {};
142
- this.parentId = data.parentId;
143
- }
144
-
145
- public static create(
146
- input: CreateTaskInput,
147
- options?: { id?: string; workflow?: Workflow; customFieldDefs?: CustomFieldDefinition[] }
148
- ): TaskEntity {
149
- const id = options?.id || `task_${crypto.randomUUID()}`;
150
- const now = new Date().toISOString();
151
-
152
- if (options?.customFieldDefs) {
153
- validateCustomFieldValues(options.customFieldDefs, input.customFields);
154
- }
155
-
156
- const status = input.status || options?.workflow?.defaultStatusKey || 'todo';
157
- const statusDef = resolveStatusDefinition(status, options?.workflow?.statuses);
158
- const semanticStatus = input.semanticStatus || statusDef.category;
159
- const actualStartDate = input.actualStartDate ?? (semanticStatus === 'in_progress' ? now : undefined);
160
- const inProgressSince = input.inProgressSince ?? (semanticStatus === 'in_progress' ? now : undefined);
161
- const actualEndDate = input.actualEndDate ?? ((semanticStatus === 'completed' || semanticStatus === 'canceled') ? now : undefined);
162
- const isBlocked = input.isBlocked ?? false;
163
- const blockedSince = input.blockedSince ?? (isBlocked && semanticStatus === 'in_progress' ? now : undefined);
164
-
165
- const task = new TaskEntity({
166
- ...input,
167
- id,
168
- createdAt: now,
169
- updatedAt: now,
170
- status,
171
- semanticStatus,
172
- actualStartDate,
173
- inProgressSince,
174
- actualEndDate,
175
- actualDurationSeconds: input.actualDurationSeconds,
176
- blockedDurationSeconds: input.blockedDurationSeconds,
177
- blockedSince,
178
- isBlocked,
179
- blockedReason: input.blockedReason ?? null,
180
- priority: input.priority || 'medium',
181
- loggedHours: input.loggedHours ?? 0,
182
- progress: input.progress ?? 0,
183
- tags: input.tags ?? [],
184
- todos: input.todos ?? [],
185
- customFields: input.customFields ?? {}
186
- });
187
-
188
- const createdEvent: TaskCreatedEvent = {
189
- id: `evt_${crypto.randomUUID()}`,
190
- name: 'task.created',
191
- aggregateId: task.id,
192
- aggregateType: 'Task',
193
- occurredAt: now,
194
- payload: { task: task.toPlain() }
195
- };
196
- task.raiseEvent(createdEvent);
197
-
198
- return task;
199
- }
200
-
201
- public transitionTo(
202
- newStatus: string,
203
- workflow?: Workflow,
204
- options?: { statusDefs?: Workflow['statuses']; actorId?: string }
205
- ): this {
206
- if (this.status === newStatus) return this;
207
-
208
- const isValid = validateTransition(workflow, this.status, newStatus);
209
- if (!isValid) {
210
- throw new WorkflowValidationError(this.status, newStatus, workflow?.id);
211
- }
212
-
213
- const previousStatus = this.status;
214
- this.status = newStatus;
215
- const now = new Date().toISOString();
216
-
217
- const statusDef = resolveStatusDefinition(newStatus, options?.statusDefs || workflow?.statuses);
218
- this.semanticStatus = statusDef.category;
219
-
220
- const prevStatusDef = resolveStatusDefinition(previousStatus, options?.statusDefs || workflow?.statuses);
221
-
222
- // 1. Leaving in_progress: finalize current session duration
223
- if (prevStatusDef.category === 'in_progress' && statusDef.category !== 'in_progress') {
224
- if (this.inProgressSince) {
225
- const startMs = new Date(this.inProgressSince).getTime();
226
- const endMs = new Date(now).getTime();
227
- if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
228
- const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
229
- this.actualDurationSeconds = (this.actualDurationSeconds || 0) + sessionSeconds;
230
- this.actualHours = Math.round(((this.actualDurationSeconds || 0) / 3600) * 100) / 100;
231
- this.actualDurationMinutes = Math.round((this.actualDurationSeconds || 0) / 60);
232
- }
233
- this.inProgressSince = null;
234
- }
235
- if (this.blockedSince) {
236
- const startMs = new Date(this.blockedSince).getTime();
237
- const endMs = new Date(now).getTime();
238
- if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
239
- const blockedSeconds = Math.max(0, (endMs - startMs) / 1000);
240
- this.blockedDurationSeconds = (this.blockedDurationSeconds || 0) + blockedSeconds;
241
- }
242
- this.blockedSince = null;
243
- }
244
- }
245
-
246
- // 2. Entering in_progress: retain earliest start date & start session timer
247
- if (statusDef.category === 'in_progress') {
248
- if (!this.actualStartDate) {
249
- this.actualStartDate = now;
250
- }
251
- if (!this.inProgressSince) {
252
- this.inProgressSince = now;
253
- }
254
- if (this.isBlocked && !this.blockedSince) {
255
- this.blockedSince = now;
256
- }
257
- if (prevStatusDef.category === 'completed' || prevStatusDef.category === 'canceled') {
258
- this.actualEndDate = undefined;
259
- }
260
- } else if (
261
- (prevStatusDef.category === 'completed' || prevStatusDef.category === 'canceled') &&
262
- statusDef.category === 'not_started'
263
- ) {
264
- this.actualEndDate = undefined;
265
- }
266
-
267
- // 3. Entering completed or canceled
268
- if (statusDef.category === 'completed' || statusDef.category === 'canceled') {
269
- if (!this.actualEndDate) {
270
- this.actualEndDate = now;
271
- }
272
- if (statusDef.category === 'completed' && this.progress !== undefined && this.progress < 100) {
273
- this.progress = 100;
274
- }
275
- }
276
-
277
- this.markUpdated();
278
-
279
- const event: TaskStatusChangedEvent = {
280
- id: `evt_${crypto.randomUUID()}`,
281
- name: 'task.status_changed',
282
- aggregateId: this.id,
283
- aggregateType: 'Task',
284
- occurredAt: now,
285
- payload: {
286
- task: this.toPlain(),
287
- previousStatus,
288
- newStatus
289
- }
290
- };
291
- this.raiseEvent(event);
292
-
293
- return this;
294
- }
295
-
296
- public logTime(
297
- entry: { hours: number; isBillable?: boolean; userId?: string; description?: string }
298
- ): TimeEntry {
299
- if (!Number.isFinite(entry.hours) || entry.hours <= 0) {
300
- throw new ValidationError('Logged hours must be a positive number.');
301
- }
302
-
303
- this.loggedHours = (this.loggedHours || 0) + entry.hours;
304
- this.actualHours = (this.actualHours || 0) + entry.hours;
305
- if (entry.isBillable !== false) {
306
- this.billableHours = (this.billableHours || 0) + entry.hours;
307
- }
308
-
309
- const now = new Date().toISOString();
310
- this.markUpdated();
311
-
312
- const timeEntry: TimeEntry = {
313
- id: `time_${crypto.randomUUID()}`,
314
- taskId: this.id,
315
- userId: entry.userId || this.assigneeId || 'system',
316
- hours: entry.hours,
317
- isBillable: entry.isBillable ?? true,
318
- description: entry.description,
319
- loggedAt: now
320
- };
321
-
322
- const event: TimeLoggedEvent = {
323
- id: `evt_${crypto.randomUUID()}`,
324
- name: 'time.logged',
325
- aggregateId: this.id,
326
- aggregateType: 'Task',
327
- occurredAt: now,
328
- payload: {
329
- taskId: this.id,
330
- timeEntry
331
- }
332
- };
333
- this.raiseEvent(event);
334
-
335
- return timeEntry;
336
- }
337
-
338
- public addTodo(title: string, id?: string): TaskTodoItem {
339
- const previous = this.toPlain();
340
- const item: TaskTodoItem = {
341
- id: id || `todo_${crypto.randomUUID()}`,
342
- title,
343
- completed: false,
344
- createdAt: new Date().toISOString()
345
- };
346
- if (!this.todos) {
347
- this.todos = [];
348
- }
349
- this.todos.push(item);
350
- this.markUpdated();
351
-
352
- const event: TaskUpdatedEvent = {
353
- id: `evt_${crypto.randomUUID()}`,
354
- name: 'task.updated',
355
- aggregateId: this.id,
356
- aggregateType: 'Task',
357
- occurredAt: new Date().toISOString(),
358
- payload: {
359
- task: this.toPlain(),
360
- previous
361
- }
362
- };
363
- this.raiseEvent(event);
364
-
365
- return item;
366
- }
367
-
368
- public toggleTodo(todoId: string, completed?: boolean): this {
369
- if (!this.todos) return this;
370
- const item = this.todos.find((t) => t.id === todoId);
371
- if (item) {
372
- const previous = this.toPlain();
373
- item.completed = completed !== undefined ? completed : !item.completed;
374
- item.completedAt = item.completed ? new Date().toISOString() : undefined;
375
- this.markUpdated();
376
-
377
- const event: TaskUpdatedEvent = {
378
- id: `evt_${crypto.randomUUID()}`,
379
- name: 'task.updated',
380
- aggregateId: this.id,
381
- aggregateType: 'Task',
382
- occurredAt: new Date().toISOString(),
383
- payload: {
384
- task: this.toPlain(),
385
- previous
386
- }
387
- };
388
- this.raiseEvent(event);
389
- }
390
- return this;
391
- }
392
-
393
- public removeTodo(todoId: string): this {
394
- if (!this.todos) return this;
395
- const previous = this.toPlain();
396
- this.todos = this.todos.filter((t) => t.id !== todoId);
397
- this.markUpdated();
398
-
399
- const event: TaskUpdatedEvent = {
400
- id: `evt_${crypto.randomUUID()}`,
401
- name: 'task.updated',
402
- aggregateId: this.id,
403
- aggregateType: 'Task',
404
- occurredAt: new Date().toISOString(),
405
- payload: {
406
- task: this.toPlain(),
407
- previous
408
- }
409
- };
410
- this.raiseEvent(event);
411
-
412
- return this;
413
- }
414
-
415
- public update(updates: Partial<Task>, customFieldDefs?: CustomFieldDefinition[]): this {
416
- const previous = this.toPlain();
417
-
418
- if (updates.customFields && customFieldDefs) {
419
- validateCustomFieldValues(customFieldDefs, {
420
- ...this.customFields,
421
- ...updates.customFields
422
- });
423
- }
424
-
425
- const wasBlocked = Boolean(this.isBlocked);
426
- const isNowBlocked = updates.isBlocked !== undefined ? Boolean(updates.isBlocked) : wasBlocked;
427
- const now = new Date().toISOString();
428
- const effectiveSemantic = updates.semanticStatus ?? this.semanticStatus;
429
-
430
- if (effectiveSemantic === 'in_progress') {
431
- if (!wasBlocked && isNowBlocked) {
432
- if (!updates.blockedSince) {
433
- updates.blockedSince = now;
434
- }
435
- } else if (wasBlocked && !isNowBlocked) {
436
- if (this.blockedSince) {
437
- const startMs = new Date(this.blockedSince).getTime();
438
- const endMs = new Date(now).getTime();
439
- if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
440
- const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
441
- updates.blockedDurationSeconds =
442
- (updates.blockedDurationSeconds ?? this.blockedDurationSeconds ?? 0) + sessionSeconds;
443
- }
444
- updates.blockedSince = null;
445
- }
446
- }
447
- } else if (!isNowBlocked && this.blockedSince) {
448
- updates.blockedSince = null;
449
- }
450
-
451
- Object.assign(this, updates);
452
- if (updates.actualDurationSeconds !== undefined && updates.actualHours === undefined) {
453
- this.actualHours = Math.round((updates.actualDurationSeconds / 3600) * 100) / 100;
454
- this.actualDurationMinutes = updates.actualDurationMinutes ?? Math.round(updates.actualDurationSeconds / 60);
455
- } else if (updates.actualHours !== undefined && updates.actualDurationSeconds === undefined) {
456
- this.actualDurationSeconds = Math.round(updates.actualHours * 3600);
457
- this.actualDurationMinutes = updates.actualDurationMinutes ?? Math.round(updates.actualHours * 60);
458
- }
459
- this.markUpdated();
460
-
461
- const event: TaskUpdatedEvent = {
462
- id: `evt_${crypto.randomUUID()}`,
463
- name: 'task.updated',
464
- aggregateId: this.id,
465
- aggregateType: 'Task',
466
- occurredAt: new Date().toISOString(),
467
- payload: {
468
- task: this.toPlain(),
469
- previous
470
- }
471
- };
472
- this.raiseEvent(event);
473
-
474
- return this;
475
- }
476
-
477
- public toPlain(): Task {
478
- return {
479
- id: this.id,
480
- projectId: this.projectId,
481
- key: this.key,
482
- title: this.title,
483
- description: this.description,
484
- status: this.status,
485
- semanticStatus: this.semanticStatus,
486
- priority: this.priority,
487
- taskType: this.taskType,
488
- assigneeId: this.assigneeId,
489
- reporterId: this.reporterId,
490
- reviewerId: this.reviewerId,
491
- iterationId: this.iterationId,
492
- teamId: this.teamId,
493
- containerId: this.containerId,
494
- deliverableId: this.deliverableId,
495
- plannedStartDate: this.plannedStartDate,
496
- actualStartDate: this.actualStartDate,
497
- actualEndDate: this.actualEndDate,
498
- dueDate: this.dueDate,
499
- estimatedHours: this.estimatedHours,
500
- loggedHours: this.loggedHours,
501
- actualHours: this.actualHours,
502
- billableHours: this.billableHours,
503
- estimatedDurationMinutes: this.estimatedDurationMinutes,
504
- actualDurationMinutes: this.actualDurationMinutes,
505
- billableDurationMinutes: this.billableDurationMinutes,
506
- actualDurationSeconds: this.actualDurationSeconds,
507
- inProgressSince: this.inProgressSince,
508
- blockedDurationSeconds: this.blockedDurationSeconds,
509
- blockedSince: this.blockedSince,
510
- progress: this.progress,
511
- isBlocked: this.isBlocked,
512
- blockedReason: this.blockedReason,
513
- tags: this.tags ? [...this.tags] : [],
514
- todos: this.todos ? this.todos.map((t) => ({ ...t })) : [],
515
- customFields: this.customFields ? { ...this.customFields } : {},
516
- parentId: this.parentId,
517
- createdAt: this.createdAt,
518
- updatedAt: this.updatedAt
519
- };
520
- }
521
- }
522
-
523
- export class ProjectEntity extends BaseEntity {
524
- public key: string;
525
- public name: string;
526
- public description?: string;
527
- public ownerId?: string;
528
- public members?: ProjectMember[];
529
- public teamIds?: string[];
530
- public workflowId?: string;
531
- public workflow?: Workflow;
532
- public taskTypes?: Project['taskTypes'];
533
- public statusDefinitions?: Project['statusDefinitions'];
534
- public priorityDefinitions?: Project['priorityDefinitions'];
535
- public customFieldDefinitions?: CustomFieldDefinition[];
536
-
537
- constructor(data: Project) {
538
- super(data.id, data.createdAt, data.updatedAt);
539
- this.key = data.key || generateProjectKey(data.name);
540
- this.name = data.name;
541
- this.description = data.description;
542
- this.ownerId = data.ownerId;
543
- this.members = data.members ? [...data.members] : [];
544
- this.teamIds = data.teamIds ? [...data.teamIds] : [];
545
- this.workflowId = data.workflowId;
546
- this.workflow = data.workflow;
547
- this.taskTypes = data.taskTypes;
548
- this.statusDefinitions = data.statusDefinitions;
549
- this.priorityDefinitions = data.priorityDefinitions;
550
- this.customFieldDefinitions = data.customFieldDefinitions ? [...data.customFieldDefinitions] : [];
551
- }
552
-
553
- public static create(
554
- input: CreateProjectInput,
555
- options?: { id?: string }
556
- ): ProjectEntity {
557
- const id = options?.id || `proj_${crypto.randomUUID()}`;
558
- const now = new Date().toISOString();
559
- const key = input.key || generateProjectKey(input.name);
560
-
561
- const project = new ProjectEntity({
562
- ...input,
563
- id,
564
- key,
565
- createdAt: now,
566
- updatedAt: now
567
- });
568
-
569
- const event: ProjectCreatedEvent = {
570
- id: `evt_${crypto.randomUUID()}`,
571
- name: 'project.created',
572
- aggregateId: project.id,
573
- aggregateType: 'Project',
574
- occurredAt: now,
575
- payload: { project: project.toPlain() }
576
- };
577
- project.raiseEvent(event);
578
-
579
- return project;
580
- }
581
-
582
- public validateCustomFields(values?: Record<string, unknown>): void {
583
- validateCustomFieldValues(this.customFieldDefinitions, values);
584
- }
585
-
586
- public toPlain(): Project {
587
- return {
588
- id: this.id,
589
- key: this.key,
590
- name: this.name,
591
- description: this.description,
592
- ownerId: this.ownerId,
593
- members: this.members ? [...this.members] : [],
594
- teamIds: this.teamIds ? [...this.teamIds] : [],
595
- workflowId: this.workflowId,
596
- workflow: this.workflow,
597
- taskTypes: this.taskTypes,
598
- statusDefinitions: this.statusDefinitions,
599
- priorityDefinitions: this.priorityDefinitions,
600
- customFieldDefinitions: this.customFieldDefinitions ? [...this.customFieldDefinitions] : [],
601
- createdAt: this.createdAt,
602
- updatedAt: this.updatedAt
603
- };
604
- }
605
- }
606
-
607
- export class DeliverableEntity extends BaseEntity {
608
- public projectId: string;
609
- public title: string;
610
- public description?: string;
611
- public status: DeliverableStatus;
612
- public format?: string;
613
- public specs?: Record<string, unknown>;
614
- public leadId?: string;
615
- public reviewerId?: string;
616
- public dueDate?: string;
617
- public deliveredAt?: string;
618
- public outputUrls?: string[];
619
- public customFields?: Record<string, unknown>;
620
-
621
- constructor(data: Deliverable) {
622
- super(data.id, data.createdAt, data.updatedAt);
623
- this.projectId = data.projectId;
624
- this.title = data.title;
625
- this.description = data.description;
626
- this.status = data.status;
627
- this.format = data.format;
628
- this.specs = data.specs ? { ...data.specs } : undefined;
629
- this.leadId = data.leadId;
630
- this.reviewerId = data.reviewerId;
631
- this.dueDate = data.dueDate;
632
- this.deliveredAt = data.deliveredAt;
633
- this.outputUrls = data.outputUrls ? [...data.outputUrls] : [];
634
- this.customFields = data.customFields ? { ...data.customFields } : {};
635
- }
636
-
637
- public static create(
638
- input: CreateDeliverableInput,
639
- options?: { id?: string; customFieldDefs?: CustomFieldDefinition[] }
640
- ): DeliverableEntity {
641
- const id = options?.id || `deliv_${crypto.randomUUID()}`;
642
- const now = new Date().toISOString();
643
-
644
- if (options?.customFieldDefs) {
645
- validateCustomFieldValues(options.customFieldDefs, input.customFields);
646
- }
647
-
648
- const deliverable = new DeliverableEntity({
649
- ...input,
650
- id,
651
- createdAt: now,
652
- updatedAt: now,
653
- status: input.status || 'planned',
654
- outputUrls: input.outputUrls ?? [],
655
- customFields: input.customFields ?? {}
656
- });
657
-
658
- const createdEvent: DeliverableCreatedEvent = {
659
- id: `evt_${crypto.randomUUID()}`,
660
- name: 'deliverable.created',
661
- aggregateId: deliverable.id,
662
- aggregateType: 'Deliverable',
663
- occurredAt: now,
664
- payload: { deliverable: deliverable.toPlain() }
665
- };
666
- deliverable.raiseEvent(createdEvent);
667
-
668
- return deliverable;
669
- }
670
-
671
- public transitionTo(newStatus: DeliverableStatus): this {
672
- if (this.status === newStatus) return this;
673
-
674
- const previousStatus = this.status;
675
- this.status = newStatus;
676
- const now = new Date().toISOString();
677
-
678
- if (newStatus === 'delivered' && !this.deliveredAt) {
679
- this.deliveredAt = now;
680
- }
681
-
682
- this.markUpdated();
683
-
684
- const event: DeliverableStatusChangedEvent = {
685
- id: `evt_${crypto.randomUUID()}`,
686
- name: 'deliverable.status_changed',
687
- aggregateId: this.id,
688
- aggregateType: 'Deliverable',
689
- occurredAt: now,
690
- payload: {
691
- deliverable: this.toPlain(),
692
- previousStatus,
693
- newStatus
694
- }
695
- };
696
- this.raiseEvent(event);
697
-
698
- return this;
699
- }
700
-
701
- public addOutputUrl(url: string): this {
702
- if (!this.outputUrls) {
703
- this.outputUrls = [];
704
- }
705
- if (!this.outputUrls.includes(url)) {
706
- const previous = this.toPlain();
707
- this.outputUrls.push(url);
708
- this.markUpdated();
709
-
710
- const event: DeliverableUpdatedEvent = {
711
- id: `evt_${crypto.randomUUID()}`,
712
- name: 'deliverable.updated',
713
- aggregateId: this.id,
714
- aggregateType: 'Deliverable',
715
- occurredAt: new Date().toISOString(),
716
- payload: {
717
- deliverable: this.toPlain(),
718
- previous
719
- }
720
- };
721
- this.raiseEvent(event);
722
- }
723
- return this;
724
- }
725
-
726
- public update(updates: Partial<Deliverable>, customFieldDefs?: CustomFieldDefinition[]): this {
727
- const previous = this.toPlain();
728
-
729
- if (updates.customFields && customFieldDefs) {
730
- validateCustomFieldValues(customFieldDefs, {
731
- ...this.customFields,
732
- ...updates.customFields
733
- });
734
- }
735
-
736
- if (updates.status && updates.status !== this.status) {
737
- this.transitionTo(updates.status);
738
- delete updates.status;
739
- }
740
-
741
- Object.assign(this, updates);
742
- this.markUpdated();
743
-
744
- const event: DeliverableUpdatedEvent = {
745
- id: `evt_${crypto.randomUUID()}`,
746
- name: 'deliverable.updated',
747
- aggregateId: this.id,
748
- aggregateType: 'Deliverable',
749
- occurredAt: new Date().toISOString(),
750
- payload: {
751
- deliverable: this.toPlain(),
752
- previous
753
- }
754
- };
755
- this.raiseEvent(event);
756
-
757
- return this;
758
- }
759
-
760
- public toPlain(): Deliverable {
761
- return {
762
- id: this.id,
763
- projectId: this.projectId,
764
- title: this.title,
765
- description: this.description,
766
- status: this.status,
767
- format: this.format,
768
- specs: this.specs ? { ...this.specs } : undefined,
769
- leadId: this.leadId,
770
- reviewerId: this.reviewerId,
771
- dueDate: this.dueDate,
772
- deliveredAt: this.deliveredAt,
773
- outputUrls: this.outputUrls ? [...this.outputUrls] : [],
774
- customFields: this.customFields ? { ...this.customFields } : {},
775
- createdAt: this.createdAt,
776
- updatedAt: this.updatedAt
777
- };
778
- }
779
- }
780
-
781
- export class AttachmentValidationError extends Error {
782
- constructor(message: string) {
783
- super(message);
784
- this.name = 'AttachmentValidationError';
785
- }
786
- }
787
-
788
- export function validateAttachmentUrl(url?: string): void {
789
- if (!url || typeof url !== 'string' || url.trim() === '') {
790
- throw new AttachmentValidationError('Attachment URL must be a non-empty string.');
791
- }
792
- if (url.startsWith('data:') && url.length > 2048) {
793
- throw new AttachmentValidationError(
794
- 'Attachment URL cannot be a large data URI. Please upload the file using a FileStorageAdapter or Cloud Storage and provide the resulting URL.'
795
- );
796
- }
797
- }
798
-