@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
package/src/index.test.ts DELETED
@@ -1,514 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { CriticalPathEngine, deriveTaskLifecycleState, DEFAULT_VFX_WORKFLOW } from './index.js';
3
- import type { CriticalPathPlugin } from './types/index.js';
4
-
5
- describe('CriticalPathEngine Core Tests', () => {
6
- it('deletes a project with its tasks and publishes deletion events', async () => {
7
- const engine = new CriticalPathEngine();
8
- const proj = await engine.createProject({ key: 'DEL', name: 'Doomed Project' });
9
- const keep = await engine.createProject({ key: 'KEEP', name: 'Survivor' });
10
- const t1 = await engine.createTask({ projectId: proj.id, title: 'One' });
11
- const t2 = await engine.createTask({ projectId: proj.id, title: 'Two' });
12
- const other = await engine.createTask({ projectId: keep.id, title: 'Other' });
13
-
14
- const published: string[] = [];
15
- engine.events.subscribe('*', (event) => {
16
- published.push(event.name);
17
- });
18
-
19
- expect(await engine.deleteProject(proj.id)).toBe(true);
20
- expect(await engine.getProject(proj.id)).toBeNull();
21
- expect(await engine.getTask(t1.id)).toBeNull();
22
- expect(await engine.getTask(t2.id)).toBeNull();
23
- expect(await engine.getTask(other.id)).not.toBeNull();
24
- expect(published.filter((n) => n === 'task.deleted')).toHaveLength(2);
25
- expect(published.at(-1)).toBe('project.deleted');
26
-
27
- expect(await engine.deleteProject(proj.id)).toBe(false);
28
- });
29
-
30
- it('creates projects and tasks with activity logs', async () => {
31
- const engine = new CriticalPathEngine();
32
-
33
- const proj = await engine.createProject({
34
- key: 'TEST',
35
- name: 'Test Project',
36
- description: 'A test project'
37
- });
38
-
39
- expect(proj.id).toBeDefined();
40
- expect(proj.key).toBe('TEST');
41
-
42
- const task = await engine.createTask({
43
- projectId: proj.id,
44
- title: 'First Task',
45
- status: 'todo',
46
- priority: 'high'
47
- });
48
-
49
- expect(task.id).toBeDefined();
50
- expect(task.title).toBe('First Task');
51
-
52
- const activities = await engine.store.getActivities({ projectId: proj.id });
53
- expect(activities.length).toBeGreaterThanOrEqual(2);
54
- });
55
-
56
- it('runs plugin hooks on task lifecycle', async () => {
57
- let hookExecuted = false;
58
-
59
- const testPlugin: CriticalPathPlugin = {
60
- id: 'auto-tag-plugin',
61
- name: 'Auto Tag Plugin',
62
- version: '1.0.0',
63
- hooks: {
64
- beforeTaskCreate: (task) => {
65
- hookExecuted = true;
66
- return {
67
- ...task,
68
- tags: [...(task.tags || []), 'auto-tagged']
69
- };
70
- }
71
- }
72
- };
73
-
74
- const engine = new CriticalPathEngine({ plugins: [testPlugin] });
75
-
76
- const proj = await engine.createProject({ key: 'PLUG', name: 'Plugin Proj' });
77
- const task = await engine.createTask({
78
- projectId: proj.id,
79
- title: 'Plugin Task',
80
- status: 'todo',
81
- priority: 'medium'
82
- });
83
-
84
- expect(hookExecuted).toBe(true);
85
- expect(task.tags).toContain('auto-tagged');
86
- });
87
-
88
- it('handles teams, containers, and iterations', async () => {
89
- const engine = new CriticalPathEngine();
90
-
91
- const team = await engine.createTeam({
92
- name: 'Engineering',
93
- memberIds: ['user_1', 'user_2']
94
- });
95
-
96
- const proj = await engine.createProject({
97
- key: 'ENG',
98
- name: 'Engineering Project',
99
- teamIds: [team.id]
100
- });
101
-
102
- const container = await engine.createContainer({
103
- projectId: proj.id,
104
- name: 'Core Platform Epic',
105
- type: 'epic'
106
- });
107
-
108
- const iteration = await engine.createIteration({
109
- projectId: proj.id,
110
- name: 'Sprint 1',
111
- type: 'sprint',
112
- status: 'active'
113
- });
114
-
115
- const task = await engine.createTask({
116
- projectId: proj.id,
117
- title: 'Setup Database Schema',
118
- status: 'in_progress',
119
- priority: 'urgent',
120
- reviewerId: 'user_2',
121
- teamId: team.id,
122
- containerId: container.id,
123
- iterationId: iteration.id,
124
- plannedStartDate: '2026-09-01T00:00:00.000Z',
125
- dueDate: '2026-09-15T00:00:00.000Z',
126
- estimatedHours: 10,
127
- actualHours: 8,
128
- billableHours: 8,
129
- estimatedDurationMinutes: 600,
130
- actualDurationMinutes: 480,
131
- billableDurationMinutes: 480,
132
- progress: 80
133
- });
134
-
135
- expect(task.reviewerId).toBe('user_2');
136
- expect(task.teamId).toBe(team.id);
137
- expect(task.containerId).toBe(container.id);
138
- expect(task.iterationId).toBe(iteration.id);
139
- expect(task.estimatedHours).toBe(10);
140
- expect(task.actualHours).toBe(8);
141
- expect(task.billableHours).toBe(8);
142
- expect(task.estimatedDurationMinutes).toBe(600);
143
- expect(task.actualDurationMinutes).toBe(480);
144
- expect(task.billableDurationMinutes).toBe(480);
145
- expect(task.progress).toBe(80);
146
- });
147
-
148
- it('evaluates status definitions and task lifecycle state', async () => {
149
- const engine = new CriticalPathEngine();
150
-
151
- const proj = await engine.createProject({
152
- key: 'CUST',
153
- name: 'Custom Workflow Project',
154
- statusDefinitions: [
155
- { key: 'draft', label: 'Draft', category: 'not_started' },
156
- { key: 'active_work', label: 'Active Work', category: 'in_progress' },
157
- { key: 'finished', label: 'Finished', category: 'completed' }
158
- ]
159
- });
160
-
161
- const task = await engine.createTask({
162
- projectId: proj.id,
163
- title: 'Workflow Task',
164
- status: 'active_work',
165
- priority: 'medium',
166
- plannedStartDate: '2026-08-01T00:00:00.000Z',
167
- dueDate: '2026-08-10T00:00:00.000Z'
168
- });
169
-
170
- const state = await engine.getTaskLifecycleState(task.id);
171
- expect(state).not.toBeNull();
172
- expect(state?.semanticStatus).toBe('in_progress');
173
- expect(state?.isActive).toBe(true);
174
- expect(state?.isDone).toBe(false);
175
- expect(state?.isBlocked).toBe(false);
176
- });
177
-
178
- it('builds task dependency graph with upstream and downstream tasks', async () => {
179
- const engine = new CriticalPathEngine();
180
-
181
- const proj = await engine.createProject({ key: 'DEP', name: 'Dependency Project' });
182
-
183
- const taskA = await engine.createTask({ projectId: proj.id, title: 'Task A', status: 'todo', priority: 'medium' });
184
- const taskB = await engine.createTask({ projectId: proj.id, title: 'Task B', status: 'todo', priority: 'medium' });
185
-
186
- // Task B depends on Task A
187
- await engine.store.addDependency({
188
- taskId: taskB.id,
189
- dependsOnTaskId: taskA.id,
190
- type: 'blocking'
191
- });
192
-
193
- const graphA = await engine.getTaskDependencyGraph(taskA.id);
194
- expect(graphA.downstreamTasks.map((t) => t.id)).toContain(taskB.id);
195
-
196
- const graphB = await engine.getTaskDependencyGraph(taskB.id);
197
- expect(graphB.upstreamTasks.map((t) => t.id)).toContain(taskA.id);
198
- });
199
-
200
- it('manages workflows and enforces status transitions', async () => {
201
- const engine = new CriticalPathEngine();
202
-
203
- // Create a strict workflow
204
- const workflow = await engine.createWorkflow({
205
- name: 'Strict Software Workflow',
206
- defaultStatusKey: 'backlog',
207
- statuses: [
208
- { key: 'backlog', label: 'Backlog', category: 'not_started' },
209
- { key: 'in_progress', label: 'In Progress', category: 'in_progress' },
210
- { key: 'done', label: 'Done', category: 'completed' }
211
- ],
212
- transitions: [
213
- { id: 't1', fromStatusKey: 'backlog', toStatusKey: 'in_progress', name: 'Start Work' },
214
- { id: 't2', fromStatusKey: 'in_progress', toStatusKey: 'done', name: 'Complete Work' }
215
- ]
216
- });
217
-
218
- expect(workflow.id).toBeDefined();
219
-
220
- // Create project tied to workflow
221
- const proj = await engine.createProject({
222
- key: 'WF',
223
- name: 'Workflow Project',
224
- workflowId: workflow.id
225
- });
226
-
227
- // Create task
228
- const task = await engine.createTask({
229
- projectId: proj.id,
230
- title: 'Workflow Task',
231
- status: 'backlog',
232
- priority: 'high'
233
- });
234
-
235
- expect(task.status).toBe('backlog');
236
- expect(task.taskType).toBe('task');
237
-
238
- // Check allowed transitions
239
- const allowed = await engine.getAllowedTaskTransitions(task.id);
240
- expect(allowed).toEqual(['in_progress']);
241
-
242
- // Valid transition: backlog -> in_progress
243
- const updated = await engine.updateTask(task.id, { status: 'in_progress' });
244
- expect(updated?.status).toBe('in_progress');
245
-
246
- // Invalid transition: in_progress -> backlog (not allowed in strict workflow)
247
- await expect(engine.updateTask(task.id, { status: 'backlog' })).rejects.toThrow();
248
-
249
- // Valid transition: in_progress -> done
250
- const doneTask = await engine.updateTask(task.id, { status: 'done' });
251
- expect(doneTask?.status).toBe('done');
252
- });
253
-
254
- it('validates attachment URLs to prevent massive data URIs', async () => {
255
- const engine = new CriticalPathEngine();
256
- const proj = await engine.createProject({ key: 'ATT', name: 'Attachment Project' });
257
- const task = await engine.createTask({ projectId: proj.id, title: 'Task with attachment', status: 'todo' });
258
-
259
- // Valid HTTPS URL
260
- const validAtt = await engine.createAttachment({
261
- taskId: task.id,
262
- uploaderId: 'user_1',
263
- filename: 'spec.pdf',
264
- mimeType: 'application/pdf',
265
- sizeBytes: 1024,
266
- url: 'https://storage.googleapis.com/bucket/spec.pdf'
267
- });
268
- expect(validAtt.id).toBeDefined();
269
- expect(validAtt.url).toBe('https://storage.googleapis.com/bucket/spec.pdf');
270
-
271
- // Invalid large data URI (>2048 chars)
272
- const largeDataUri = `data:application/pdf;base64,${'A'.repeat(3000)}`;
273
- await expect(
274
- engine.createAttachment({
275
- taskId: task.id,
276
- uploaderId: 'user_1',
277
- filename: 'huge.pdf',
278
- mimeType: 'application/pdf',
279
- sizeBytes: 3000,
280
- url: largeDataUri
281
- })
282
- ).rejects.toThrow('Attachment URL cannot be a large data URI');
283
- });
284
-
285
- it('supports adding and removing emoji reactions on comments with events and deduplication', async () => {
286
- const engine = new CriticalPathEngine();
287
- const proj = await engine.createProject({ key: 'CMT', name: 'Comment Project' });
288
- const task = await engine.createTask({ projectId: proj.id, title: 'Comment Task', status: 'todo' });
289
-
290
- const comment = await engine.addComment({
291
- taskId: task.id,
292
- authorId: 'user_1',
293
- content: 'Great progress!'
294
- });
295
-
296
- const receivedEvents: string[] = [];
297
- engine.events.subscribe('*', (event) => {
298
- receivedEvents.push(event.name);
299
- });
300
-
301
- // Add reaction 👍
302
- const withReaction = await engine.addCommentReaction(comment.id, {
303
- emoji: '👍',
304
- userId: 'user_2'
305
- });
306
- expect(withReaction?.reactions).toHaveLength(1);
307
- expect(withReaction?.reactions?.[0]).toMatchObject({
308
- emoji: '👍',
309
- userId: 'user_2'
310
- });
311
- expect(withReaction?.reactions?.[0].createdAt).toBeDefined();
312
- expect(receivedEvents).toContain('comment.reaction.added');
313
-
314
- // Add duplicate reaction from same user (should deduplicate)
315
- const duplicateReaction = await engine.addCommentReaction(comment.id, {
316
- emoji: '👍',
317
- userId: 'user_2'
318
- });
319
- expect(duplicateReaction?.reactions).toHaveLength(1);
320
-
321
- // Add different reaction from another user
322
- const secondReaction = await engine.addCommentReaction(comment.id, {
323
- emoji: '🚀',
324
- userId: 'user_3'
325
- });
326
- expect(secondReaction?.reactions).toHaveLength(2);
327
-
328
- // Remove reaction 👍
329
- const afterRemoval = await engine.removeCommentReaction(comment.id, {
330
- emoji: '👍',
331
- userId: 'user_2'
332
- });
333
- expect(afterRemoval?.reactions).toHaveLength(1);
334
- expect(afterRemoval?.reactions?.[0].emoji).toBe('🚀');
335
- expect(receivedEvents).toContain('comment.reaction.removed');
336
- });
337
-
338
- it('supports mentions in comments with auto-extraction and explicit override', async () => {
339
- const engine = new CriticalPathEngine();
340
- const proj = await engine.createProject({ key: 'MNT', name: 'Mentions Project' });
341
- const task = await engine.createTask({ projectId: proj.id, title: 'Mentions Task', status: 'todo' });
342
-
343
- // Auto-extracted mentions
344
- const comment1 = await engine.addComment({
345
- taskId: task.id,
346
- authorId: 'user_1',
347
- content: 'Hey @planner and @user_2, please check with @"Supervisor Agent"!'
348
- });
349
- expect(comment1.mentions).toEqual(['planner', 'user_2', 'Supervisor Agent']);
350
-
351
- // Explicit mentions passed
352
- const comment2 = await engine.addComment({
353
- taskId: task.id,
354
- authorId: 'user_1',
355
- content: 'Custom mention assignment',
356
- mentions: ['custom_agent_id']
357
- });
358
- expect(comment2.mentions).toEqual(['custom_agent_id']);
359
-
360
- // Update comment content updates mentions
361
- const updated = await engine.updateComment(comment1.id, {
362
- content: 'Now calling only @coordinator'
363
- });
364
- expect(updated?.mentions).toEqual(['coordinator']);
365
- });
366
-
367
- it('supports multi-assignees and backward status transitions', async () => {
368
- const engine = new CriticalPathEngine();
369
- const proj = await engine.createProject({ key: 'TRN', name: 'Transitions Project' });
370
-
371
- // Task with multi-assignees
372
- const task = await engine.createTask({
373
- projectId: proj.id,
374
- title: 'Multi-assignee Task',
375
- status: 'in_progress',
376
- assignees: [
377
- { id: 'agent_1', name: 'Agent Alpha', role: 'Planner', type: 'agent' },
378
- { id: 'user_1', name: 'Alice Developer', role: 'Developer', type: 'user', avatarUrl: 'https://example.com/alice.png' }
379
- ]
380
- });
381
-
382
- expect(task.assignees).toHaveLength(2);
383
- expect(task.assignees?.[0].type).toBe('agent');
384
- expect(task.assignees?.[1].avatarUrl).toBe('https://example.com/alice.png');
385
-
386
- // Backward transitions
387
- const previousStatuses = await engine.getAllowedPreviousTaskTransitions(task.id);
388
- expect(previousStatuses).toContain('todo');
389
-
390
- // First status (backlog) should have no previous status
391
- const backlogTask = await engine.createTask({
392
- projectId: proj.id,
393
- title: 'Backlog Task',
394
- status: 'backlog'
395
- });
396
- const backlogPrev = await engine.getAllowedPreviousTaskTransitions(backlogTask.id);
397
- expect(backlogPrev).toEqual([]);
398
- });
399
-
400
- it('automatically unblocks downstream tasks and publishes task.unblocked event when upstream task completes', async () => {
401
- const engine = new CriticalPathEngine();
402
- const proj = await engine.createProject({ key: 'UNBLK', name: 'Auto-Unblock Project' });
403
-
404
- const upstream = await engine.createTask({
405
- projectId: proj.id,
406
- title: 'Upstream Task',
407
- status: 'in_progress'
408
- });
409
-
410
- const downstream = await engine.createTask({
411
- projectId: proj.id,
412
- title: 'Downstream Task',
413
- status: 'todo',
414
- isBlocked: true,
415
- blockedReason: 'Waiting on upstream'
416
- });
417
-
418
- await engine.addDependency({
419
- taskId: downstream.id,
420
- dependsOnTaskId: upstream.id,
421
- type: 'blocking'
422
- });
423
-
424
- const unblockedEvents: any[] = [];
425
- engine.events.subscribe('task.unblocked', async (evt) => {
426
- unblockedEvents.push(evt);
427
- });
428
-
429
- // Complete upstream task
430
- const completedUpstream = await engine.updateTask(upstream.id, { status: 'done' });
431
- expect(completedUpstream?.status).toBe('done');
432
-
433
- // Downstream task should now be unblocked
434
- expect(unblockedEvents).toHaveLength(1);
435
- expect(unblockedEvents[0].payload.task.id).toBe(downstream.id);
436
- expect(unblockedEvents[0].payload.upstreamTaskId).toBe(upstream.id);
437
-
438
- const refreshedDownstream = await engine.getTask(downstream.id);
439
- expect(refreshedDownstream?.status).toBe('todo');
440
- expect(refreshedDownstream?.isBlocked).toBe(false);
441
- });
442
-
443
- it('allows VFX workflow transitions from revision_requested to in_production', async () => {
444
- const engine = new CriticalPathEngine();
445
- const vfxWorkflow = await engine.createWorkflow(DEFAULT_VFX_WORKFLOW);
446
- const proj = await engine.createProject({
447
- key: 'VFX',
448
- name: 'VFX Film Project',
449
- workflowId: vfxWorkflow.id
450
- });
451
-
452
- const task = await engine.createTask({
453
- projectId: proj.id,
454
- title: 'Compositing Shot 010',
455
- status: 'revision_requested'
456
- });
457
-
458
- const allowed = await engine.getAllowedTaskTransitions(task.id);
459
- expect(allowed).toContain('in_production');
460
- expect(allowed).toContain('internal_review');
461
-
462
- const allowedPrevious = await engine.getAllowedPreviousTaskTransitions(task.id);
463
- expect(allowedPrevious).toContain('in_production');
464
-
465
- const updated = await engine.updateTask(task.id, { status: 'in_production' });
466
- expect(updated?.status).toBe('in_production');
467
- });
468
-
469
- it('attributes activities and domain events to the real actor rather than the assignee', async () => {
470
- const engine = new CriticalPathEngine();
471
- const proj = await engine.createProject({ key: 'ACT', name: 'Actor Attribution Project' });
472
- const task = await engine.createTask({
473
- projectId: proj.id,
474
- title: 'Assigned Task',
475
- status: 'todo',
476
- assigneeId: 'agent_worker_1'
477
- });
478
-
479
- const receivedEvents: any[] = [];
480
- engine.events.subscribe('task.status_changed', async (evt) => {
481
- receivedEvents.push(evt);
482
- });
483
- engine.events.subscribe('task.updated', async (evt) => {
484
- receivedEvents.push(evt);
485
- });
486
-
487
- // Update with explicit actor (e.g. human user updating an assigned agent task)
488
- await engine.updateTask(task.id, { status: 'in_progress' }, { actorId: 'user_human_42', actorName: 'Jack James' });
489
-
490
- const activities = await engine.store.getActivities({ taskId: task.id });
491
- const statusActivity = activities.find((a) => a.action === 'task.status_changed');
492
- expect(statusActivity).toBeDefined();
493
- expect(statusActivity?.actorId).toBe('user_human_42');
494
- expect((statusActivity?.details as any)?.actorName).toBe('Jack James');
495
- expect(statusActivity?.actorId).not.toBe('agent_worker_1');
496
-
497
- expect(receivedEvents).toHaveLength(1);
498
- expect(receivedEvents[0].payload.actorId).toBe('user_human_42');
499
- expect(receivedEvents[0].payload.actorName).toBe('Jack James');
500
-
501
- // Update without explicit actor - should fall back to 'system', NEVER task.assigneeId
502
- await engine.updateTask(task.id, {
503
- title: 'Updated title'
504
- });
505
-
506
- const updatedActivities = await engine.store.getActivities({ taskId: task.id });
507
- const updateActivity = updatedActivities.find((a) => a.action === 'task.updated');
508
- expect(updateActivity).toBeDefined();
509
- expect(updateActivity?.actorId).toBe('system');
510
- expect(updateActivity?.actorId).not.toBe('agent_worker_1');
511
- });
512
- });
513
-
514
-
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- export * from './types/index.js';
2
- export * from './utils/index.js';
3
- export * from './store/index.js';
4
- export * from './storage/index.js';
5
- export * from './plugins/index.js';
6
- export * from './engine/index.js';
7
- export * from './domain/index.js';
8
- export * from './webhooks/index.js';
9
-
@@ -1,103 +0,0 @@
1
- import type { CriticalPathPlugin, CustomFieldType, PluginMiddleware, PluginRoute, Task } from '../types/index.js';
2
-
3
- export const BUILT_IN_CUSTOM_FIELD_TYPES = [
4
- 'text',
5
- 'number',
6
- 'date',
7
- 'boolean',
8
- 'single_select',
9
- 'multi_select',
10
- 'user'
11
- ] as const;
12
-
13
- export class PluginRegistry {
14
- private plugins = new Map<string, CriticalPathPlugin>();
15
- private fieldTypes = new Map<string, CustomFieldType>();
16
-
17
- register(plugin: CriticalPathPlugin): void {
18
- if (this.plugins.has(plugin.id)) {
19
- throw new Error(`Plugin with ID "${plugin.id}" is already registered.`);
20
- }
21
- for (const fieldType of plugin.customFieldTypes ?? []) {
22
- if ((BUILT_IN_CUSTOM_FIELD_TYPES as readonly string[]).includes(fieldType.type) || this.fieldTypes.has(fieldType.type)) {
23
- throw new Error(`Plugin "${plugin.id}" registers custom field type "${fieldType.type}", which already exists.`);
24
- }
25
- }
26
- for (const fieldType of plugin.customFieldTypes ?? []) {
27
- this.fieldTypes.set(fieldType.type, fieldType);
28
- }
29
- this.plugins.set(plugin.id, plugin);
30
- }
31
-
32
- getPlugins(): CriticalPathPlugin[] {
33
- return Array.from(this.plugins.values());
34
- }
35
-
36
- /** Custom field types contributed by plugins, keyed by `type`. */
37
- getCustomFieldTypes(): ReadonlyMap<string, CustomFieldType> {
38
- return this.fieldTypes;
39
- }
40
-
41
- getRoutes(): Array<PluginRoute & { pluginId: string }> {
42
- return this.getPlugins().flatMap((p) => (p.routes ?? []).map((route) => ({ ...route, pluginId: p.id })));
43
- }
44
-
45
- getMiddleware(): PluginMiddleware[] {
46
- return this.getPlugins().flatMap((p) => (p.middleware ? [p.middleware] : []));
47
- }
48
-
49
- async runBeforeTaskCreate(task: Partial<Task>): Promise<Partial<Task>> {
50
- let currentTask = { ...task };
51
- for (const plugin of this.plugins.values()) {
52
- if (plugin.hooks?.beforeTaskCreate) {
53
- currentTask = await plugin.hooks.beforeTaskCreate(currentTask);
54
- }
55
- }
56
- return currentTask;
57
- }
58
-
59
- async runAfterTaskCreate(task: Task): Promise<void> {
60
- await this.runAfter('afterTaskCreate', (hooks) => hooks.afterTaskCreate?.(task));
61
- }
62
-
63
- async runBeforeTaskUpdate(id: string, updates: Partial<Task>): Promise<Partial<Task>> {
64
- let currentUpdates = { ...updates };
65
- for (const plugin of this.plugins.values()) {
66
- if (plugin.hooks?.beforeTaskUpdate) {
67
- currentUpdates = await plugin.hooks.beforeTaskUpdate(id, currentUpdates);
68
- }
69
- }
70
- return currentUpdates;
71
- }
72
-
73
- async runAfterTaskUpdate(task: Task, previousState: Task): Promise<void> {
74
- await this.runAfter('afterTaskUpdate', (hooks) => hooks.afterTaskUpdate?.(task, previousState));
75
- }
76
-
77
- async runBeforeTaskDelete(id: string, task: Task): Promise<void> {
78
- for (const plugin of this.plugins.values()) {
79
- if (plugin.hooks?.beforeTaskDelete) {
80
- await plugin.hooks.beforeTaskDelete(id, task);
81
- }
82
- }
83
- }
84
-
85
- async runAfterTaskDelete(id: string, task: Task): Promise<void> {
86
- await this.runAfter('afterTaskDelete', (hooks) => hooks.afterTaskDelete?.(id, task));
87
- }
88
-
89
- /** Runs an after-hook on every plugin; the change is already stored, so errors are only logged. */
90
- private async runAfter(
91
- name: string,
92
- call: (hooks: NonNullable<CriticalPathPlugin['hooks']>) => unknown
93
- ): Promise<void> {
94
- for (const plugin of this.plugins.values()) {
95
- if (!plugin.hooks) continue;
96
- try {
97
- await call(plugin.hooks);
98
- } catch (err) {
99
- console.error(`[CriticalPath] Plugin "${plugin.id}" ${name} hook failed:`, err);
100
- }
101
- }
102
- }
103
- }