@critical-path/core 0.33.0 → 0.35.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 (38) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -1
  3. package/dist/domain/entities.js +17 -17
  4. package/dist/domain/entities.js.map +1 -1
  5. package/dist/domain/events.d.ts +53 -2
  6. package/dist/domain/events.d.ts.map +1 -1
  7. package/dist/domain/events.js +7 -0
  8. package/dist/domain/events.js.map +1 -1
  9. package/dist/engine/entity-events.test.d.ts +2 -0
  10. package/dist/engine/entity-events.test.d.ts.map +1 -0
  11. package/dist/engine/entity-events.test.js +61 -0
  12. package/dist/engine/entity-events.test.js.map +1 -0
  13. package/dist/engine/index.d.ts +2 -0
  14. package/dist/engine/index.d.ts.map +1 -1
  15. package/dist/engine/index.js +106 -39
  16. package/dist/engine/index.js.map +1 -1
  17. package/dist/schemas/index.d.ts +6 -6
  18. package/dist/store/firebase.js +5 -5
  19. package/dist/store/firebase.js.map +1 -1
  20. package/dist/store/index.d.ts +5 -0
  21. package/dist/store/index.d.ts.map +1 -1
  22. package/dist/store/index.js +30 -13
  23. package/dist/store/index.js.map +1 -1
  24. package/dist/store/sqlite.js +13 -13
  25. package/dist/store/sqlite.js.map +1 -1
  26. package/dist/testing/conformance.d.ts.map +1 -1
  27. package/dist/testing/conformance.js +15 -0
  28. package/dist/testing/conformance.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/domain/entities.ts +17 -17
  31. package/src/domain/events.ts +49 -0
  32. package/src/engine/entity-events.test.ts +67 -0
  33. package/src/engine/index.ts +115 -39
  34. package/src/store/firebase.ts +5 -5
  35. package/src/store/index.ts +30 -13
  36. package/src/store/sqlite.ts +13 -13
  37. package/src/testing/conformance.ts +18 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -64,6 +64,14 @@ export class CriticalPathEngine {
64
64
  Object.defineProperty(scoped, 'actor', { value: Object.freeze({ ...actor }), enumerable: true });
65
65
  return scoped;
66
66
  }
67
+ /** Publishes a domain event, filling in its id and timestamp. */
68
+ async publishEvent(event) {
69
+ await this.events.publish({
70
+ ...event,
71
+ id: `evt_${crypto.randomUUID()}`,
72
+ occurredAt: new Date().toISOString()
73
+ });
74
+ }
67
75
  actorIdOr(fallback) {
68
76
  return this.actor?.userId ?? fallback;
69
77
  }
@@ -222,7 +230,7 @@ export class CriticalPathEngine {
222
230
  const created = await this.store.createWorkflow({ ...workflow, tenantId: this.actor ? this.actor.tenantId : workflow.tenantId });
223
231
  const now = new Date().toISOString();
224
232
  const event = {
225
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
233
+ id: `evt_${crypto.randomUUID()}`,
226
234
  name: 'workflow.created',
227
235
  aggregateId: created.id,
228
236
  aggregateType: 'Workflow',
@@ -247,7 +255,7 @@ export class CriticalPathEngine {
247
255
  if (updated) {
248
256
  const now = new Date().toISOString();
249
257
  const event = {
250
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
258
+ id: `evt_${crypto.randomUUID()}`,
251
259
  name: 'workflow.updated',
252
260
  aggregateId: updated.id,
253
261
  aggregateType: 'Workflow',
@@ -272,7 +280,7 @@ export class CriticalPathEngine {
272
280
  if (deleted) {
273
281
  const now = new Date().toISOString();
274
282
  const event = {
275
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
283
+ id: `evt_${crypto.randomUUID()}`,
276
284
  name: 'workflow.deleted',
277
285
  aggregateId: id,
278
286
  aggregateType: 'Workflow',
@@ -349,7 +357,7 @@ export class CriticalPathEngine {
349
357
  const created = await this.store.createProject(input);
350
358
  const now = new Date().toISOString();
351
359
  const event = {
352
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
360
+ id: `evt_${crypto.randomUUID()}`,
353
361
  name: 'project.created',
354
362
  aggregateId: created.id,
355
363
  aggregateType: 'Project',
@@ -378,7 +386,7 @@ export class CriticalPathEngine {
378
386
  if (updated) {
379
387
  const now = new Date().toISOString();
380
388
  const event = {
381
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
389
+ id: `evt_${crypto.randomUUID()}`,
382
390
  name: 'project.updated',
383
391
  aggregateId: updated.id,
384
392
  aggregateType: 'Project',
@@ -424,7 +432,7 @@ export class CriticalPathEngine {
424
432
  if (deleted) {
425
433
  const now = new Date().toISOString();
426
434
  const event = {
427
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
435
+ id: `evt_${crypto.randomUUID()}`,
428
436
  name: 'project.deleted',
429
437
  aggregateId: id,
430
438
  aggregateType: 'Project',
@@ -551,7 +559,7 @@ export class CriticalPathEngine {
551
559
  await this.plugins.runAfterTaskCreate(created);
552
560
  // Publish typed Domain Event
553
561
  const event = {
554
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
562
+ id: `evt_${crypto.randomUUID()}`,
555
563
  name: 'task.created',
556
564
  aggregateId: created.id,
557
565
  aggregateType: 'Task',
@@ -746,7 +754,7 @@ export class CriticalPathEngine {
746
754
  // Publish typed Domain Events
747
755
  if (isStatusChange) {
748
756
  const statusEvent = {
749
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
757
+ id: `evt_${crypto.randomUUID()}`,
750
758
  name: 'task.status_changed',
751
759
  aggregateId: updated.id,
752
760
  aggregateType: 'Task',
@@ -768,7 +776,7 @@ export class CriticalPathEngine {
768
776
  }
769
777
  else {
770
778
  const updateEvent = {
771
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
779
+ id: `evt_${crypto.randomUUID()}`,
772
780
  name: 'task.updated',
773
781
  aggregateId: updated.id,
774
782
  aggregateType: 'Task',
@@ -788,7 +796,7 @@ export class CriticalPathEngine {
788
796
  if (!wasBlocked && isNowBlocked) {
789
797
  const blockedReason = updated.blockedReason ?? null;
790
798
  const blockedEvent = {
791
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
799
+ id: `evt_${crypto.randomUUID()}`,
792
800
  name: 'task.blocked',
793
801
  aggregateId: updated.id,
794
802
  aggregateType: 'Task',
@@ -812,7 +820,7 @@ export class CriticalPathEngine {
812
820
  }
813
821
  else if (wasBlocked && !isNowBlocked) {
814
822
  const unblockedEvent = {
815
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
823
+ id: `evt_${crypto.randomUUID()}`,
816
824
  name: 'task.unblocked',
817
825
  aggregateId: updated.id,
818
826
  aggregateType: 'Task',
@@ -887,7 +895,7 @@ export class CriticalPathEngine {
887
895
  }
888
896
  const now = new Date().toISOString();
889
897
  const unblockedEvent = {
890
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
898
+ id: `evt_${crypto.randomUUID()}`,
891
899
  name: 'task.unblocked',
892
900
  aggregateId: downstream.id,
893
901
  aggregateType: 'Task',
@@ -929,7 +937,7 @@ export class CriticalPathEngine {
929
937
  await this.plugins.runAfterTaskDelete(id, existing);
930
938
  const now = new Date().toISOString();
931
939
  const event = {
932
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
940
+ id: `evt_${crypto.randomUUID()}`,
933
941
  name: 'task.deleted',
934
942
  aggregateId: id,
935
943
  aggregateType: 'Task',
@@ -995,7 +1003,7 @@ export class CriticalPathEngine {
995
1003
  const removed = await this.store.removeDependency(id);
996
1004
  if (removed) {
997
1005
  const event = {
998
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1006
+ id: `evt_${crypto.randomUUID()}`,
999
1007
  name: 'dependency.removed',
1000
1008
  aggregateId: id,
1001
1009
  aggregateType: 'Dependency',
@@ -1025,7 +1033,7 @@ export class CriticalPathEngine {
1025
1033
  const created = await this.store.addDependency(dep);
1026
1034
  const now = new Date().toISOString();
1027
1035
  const event = {
1028
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1036
+ id: `evt_${crypto.randomUUID()}`,
1029
1037
  name: 'dependency.added',
1030
1038
  aggregateId: created.id,
1031
1039
  aggregateType: 'Dependency',
@@ -1132,7 +1140,7 @@ export class CriticalPathEngine {
1132
1140
  const created = await this.store.logTime(fullEntry);
1133
1141
  const now = new Date().toISOString();
1134
1142
  const event = {
1135
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1143
+ id: `evt_${crypto.randomUUID()}`,
1136
1144
  name: 'time.logged',
1137
1145
  aggregateId: entry.taskId,
1138
1146
  aggregateType: 'Task',
@@ -1177,7 +1185,7 @@ export class CriticalPathEngine {
1177
1185
  const created = await this.store.addComment({ ...comment, mentions });
1178
1186
  const now = new Date().toISOString();
1179
1187
  const event = {
1180
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1188
+ id: `evt_${crypto.randomUUID()}`,
1181
1189
  name: 'comment.created',
1182
1190
  aggregateId: created.id,
1183
1191
  aggregateType: 'Comment',
@@ -1202,7 +1210,7 @@ export class CriticalPathEngine {
1202
1210
  return null;
1203
1211
  const now = new Date().toISOString();
1204
1212
  const event = {
1205
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1213
+ id: `evt_${crypto.randomUUID()}`,
1206
1214
  name: 'comment.updated',
1207
1215
  aggregateId: updated.id,
1208
1216
  aggregateType: 'Comment',
@@ -1224,7 +1232,7 @@ export class CriticalPathEngine {
1224
1232
  if (deleted) {
1225
1233
  const now = new Date().toISOString();
1226
1234
  const event = {
1227
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1235
+ id: `evt_${crypto.randomUUID()}`,
1228
1236
  name: 'comment.deleted',
1229
1237
  aggregateId: id,
1230
1238
  aggregateType: 'Comment',
@@ -1261,7 +1269,7 @@ export class CriticalPathEngine {
1261
1269
  return null;
1262
1270
  const now = new Date().toISOString();
1263
1271
  const event = {
1264
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1272
+ id: `evt_${crypto.randomUUID()}`,
1265
1273
  name: 'comment.reaction.added',
1266
1274
  aggregateId: commentId,
1267
1275
  aggregateType: 'Comment',
@@ -1297,7 +1305,7 @@ export class CriticalPathEngine {
1297
1305
  return null;
1298
1306
  const now = new Date().toISOString();
1299
1307
  const event = {
1300
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1308
+ id: `evt_${crypto.randomUUID()}`,
1301
1309
  name: 'comment.reaction.removed',
1302
1310
  aggregateId: commentId,
1303
1311
  aggregateType: 'Comment',
@@ -1340,7 +1348,7 @@ export class CriticalPathEngine {
1340
1348
  const created = await this.store.createAttachment(attachment);
1341
1349
  const now = new Date().toISOString();
1342
1350
  const event = {
1343
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1351
+ id: `evt_${crypto.randomUUID()}`,
1344
1352
  name: 'attachment.created',
1345
1353
  aggregateId: created.id,
1346
1354
  aggregateType: 'Attachment',
@@ -1370,7 +1378,7 @@ export class CriticalPathEngine {
1370
1378
  if (deleted) {
1371
1379
  const now = new Date().toISOString();
1372
1380
  const event = {
1373
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1381
+ id: `evt_${crypto.randomUUID()}`,
1374
1382
  name: 'attachment.deleted',
1375
1383
  aggregateId: id,
1376
1384
  aggregateType: 'Attachment',
@@ -1498,7 +1506,7 @@ export class CriticalPathEngine {
1498
1506
  const created = await this.store.createTeam({ ...team, tenantId: this.actor ? this.actor.tenantId : team.tenantId });
1499
1507
  const now = new Date().toISOString();
1500
1508
  const event = {
1501
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1509
+ id: `evt_${crypto.randomUUID()}`,
1502
1510
  name: 'team.created',
1503
1511
  aggregateId: created.id,
1504
1512
  aggregateType: 'Team',
@@ -1509,16 +1517,36 @@ export class CriticalPathEngine {
1509
1517
  return created;
1510
1518
  }
1511
1519
  async updateTeam(id, updates) {
1512
- if (!(await this.getTeam(id)))
1520
+ const existing = await this.getTeam(id);
1521
+ if (!existing)
1513
1522
  return null;
1514
1523
  await this.requireWorkspaceAccess('workspace.manage');
1515
- return this.store.updateTeam(id, this.withoutTenant(updates));
1524
+ const updated = await this.store.updateTeam(id, this.withoutTenant(updates));
1525
+ if (updated) {
1526
+ await this.publishEvent({
1527
+ name: 'team.updated',
1528
+ aggregateId: id,
1529
+ aggregateType: 'Team',
1530
+ payload: { team: updated, previous: existing }
1531
+ });
1532
+ }
1533
+ return updated;
1516
1534
  }
1517
1535
  async deleteTeam(id) {
1518
- if (!(await this.getTeam(id)))
1536
+ const existing = await this.getTeam(id);
1537
+ if (!existing)
1519
1538
  return false;
1520
1539
  await this.requireWorkspaceAccess('workspace.manage');
1521
- return this.store.deleteTeam(id);
1540
+ const deleted = await this.store.deleteTeam(id);
1541
+ if (deleted) {
1542
+ await this.publishEvent({
1543
+ name: 'team.deleted',
1544
+ aggregateId: id,
1545
+ aggregateType: 'Team',
1546
+ payload: { teamId: id, name: existing.name, tenantId: existing.tenantId }
1547
+ });
1548
+ }
1549
+ return deleted;
1522
1550
  }
1523
1551
  // --- Containers ---
1524
1552
  async getContainers(projectId) {
@@ -1537,7 +1565,7 @@ export class CriticalPathEngine {
1537
1565
  const created = await this.store.createContainer(container);
1538
1566
  const now = new Date().toISOString();
1539
1567
  const event = {
1540
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1568
+ id: `evt_${crypto.randomUUID()}`,
1541
1569
  name: 'container.created',
1542
1570
  aggregateId: created.id,
1543
1571
  aggregateType: 'Container',
@@ -1552,7 +1580,16 @@ export class CriticalPathEngine {
1552
1580
  if (!existing)
1553
1581
  return null;
1554
1582
  await this.requireProjectAccess('plan.manage', existing.projectId);
1555
- return this.store.updateContainer(id, updates);
1583
+ const updated = await this.store.updateContainer(id, updates);
1584
+ if (updated) {
1585
+ await this.publishEvent({
1586
+ name: 'container.updated',
1587
+ aggregateId: id,
1588
+ aggregateType: 'Container',
1589
+ payload: { container: updated, previous: existing }
1590
+ });
1591
+ }
1592
+ return updated;
1556
1593
  }
1557
1594
  /** Deletes a container. Its tasks and nested containers are kept but no longer reference it. */
1558
1595
  async deleteContainer(id) {
@@ -1564,7 +1601,16 @@ export class CriticalPathEngine {
1564
1601
  for (const child of (await this.store.getContainers(existing.projectId)).filter((c) => c.parentId === id)) {
1565
1602
  await this.store.updateContainer(child.id, { parentId: undefined });
1566
1603
  }
1567
- return this.store.deleteContainer(id);
1604
+ const deleted = await this.store.deleteContainer(id);
1605
+ if (deleted) {
1606
+ await this.publishEvent({
1607
+ name: 'container.deleted',
1608
+ aggregateId: id,
1609
+ aggregateType: 'Container',
1610
+ payload: { containerId: id, projectId: existing.projectId, name: existing.name }
1611
+ });
1612
+ }
1613
+ return deleted;
1568
1614
  }
1569
1615
  /** Unsets a reference (e.g. `iterationId`) on every task in the project that points at `id`. */
1570
1616
  async clearTaskReferences(projectId, field, id) {
@@ -1599,7 +1645,7 @@ export class CriticalPathEngine {
1599
1645
  });
1600
1646
  const now = new Date().toISOString();
1601
1647
  const event = {
1602
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1648
+ id: `evt_${crypto.randomUUID()}`,
1603
1649
  name: 'deliverable.created',
1604
1650
  aggregateId: created.id,
1605
1651
  aggregateType: 'Deliverable',
@@ -1628,7 +1674,7 @@ export class CriticalPathEngine {
1628
1674
  const now = new Date().toISOString();
1629
1675
  if (updates.status && updates.status !== existing.status) {
1630
1676
  const statusEvent = {
1631
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1677
+ id: `evt_${crypto.randomUUID()}`,
1632
1678
  name: 'deliverable.status_changed',
1633
1679
  aggregateId: updated.id,
1634
1680
  aggregateType: 'Deliverable',
@@ -1642,7 +1688,7 @@ export class CriticalPathEngine {
1642
1688
  await this.events.publish(statusEvent);
1643
1689
  }
1644
1690
  const updateEvent = {
1645
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1691
+ id: `evt_${crypto.randomUUID()}`,
1646
1692
  name: 'deliverable.updated',
1647
1693
  aggregateId: updated.id,
1648
1694
  aggregateType: 'Deliverable',
@@ -1669,7 +1715,7 @@ export class CriticalPathEngine {
1669
1715
  if (deleted) {
1670
1716
  const now = new Date().toISOString();
1671
1717
  const event = {
1672
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1718
+ id: `evt_${crypto.randomUUID()}`,
1673
1719
  name: 'deliverable.deleted',
1674
1720
  aggregateId: id,
1675
1721
  aggregateType: 'Deliverable',
@@ -1745,10 +1791,16 @@ export class CriticalPathEngine {
1745
1791
  async createIteration(iteration) {
1746
1792
  await this.requireProjectAccess('plan.manage', iteration.projectId);
1747
1793
  const created = await this.store.createIteration(iteration);
1794
+ await this.publishEvent({
1795
+ name: 'iteration.created',
1796
+ aggregateId: created.id,
1797
+ aggregateType: 'Iteration',
1798
+ payload: { iteration: created }
1799
+ });
1748
1800
  if (created.status === 'active') {
1749
1801
  const now = new Date().toISOString();
1750
1802
  const event = {
1751
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1803
+ id: `evt_${crypto.randomUUID()}`,
1752
1804
  name: 'iteration.started',
1753
1805
  aggregateId: created.id,
1754
1806
  aggregateType: 'Iteration',
@@ -1766,10 +1818,16 @@ export class CriticalPathEngine {
1766
1818
  await this.requireProjectAccess('plan.manage', existing.projectId);
1767
1819
  const updated = await this.store.updateIteration(id, updates);
1768
1820
  if (updated) {
1821
+ await this.publishEvent({
1822
+ name: 'iteration.updated',
1823
+ aggregateId: id,
1824
+ aggregateType: 'Iteration',
1825
+ payload: { iteration: updated, previous: existing }
1826
+ });
1769
1827
  const now = new Date().toISOString();
1770
1828
  if (existing.status !== 'active' && updated.status === 'active') {
1771
1829
  const event = {
1772
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1830
+ id: `evt_${crypto.randomUUID()}`,
1773
1831
  name: 'iteration.started',
1774
1832
  aggregateId: updated.id,
1775
1833
  aggregateType: 'Iteration',
@@ -1780,7 +1838,7 @@ export class CriticalPathEngine {
1780
1838
  }
1781
1839
  else if (existing.status !== 'completed' && updated.status === 'completed') {
1782
1840
  const event = {
1783
- id: `evt_${Math.random().toString(36).substring(2, 9)}`,
1841
+ id: `evt_${crypto.randomUUID()}`,
1784
1842
  name: 'iteration.completed',
1785
1843
  aggregateId: updated.id,
1786
1844
  aggregateType: 'Iteration',
@@ -1799,7 +1857,16 @@ export class CriticalPathEngine {
1799
1857
  return false;
1800
1858
  await this.requireProjectAccess('plan.manage', existing.projectId);
1801
1859
  await this.clearTaskReferences(existing.projectId, 'iterationId', id);
1802
- return this.store.deleteIteration(id);
1860
+ const deleted = await this.store.deleteIteration(id);
1861
+ if (deleted) {
1862
+ await this.publishEvent({
1863
+ name: 'iteration.deleted',
1864
+ aggregateId: id,
1865
+ aggregateType: 'Iteration',
1866
+ payload: { iterationId: id, projectId: existing.projectId, name: existing.name }
1867
+ });
1868
+ }
1869
+ return deleted;
1803
1870
  }
1804
1871
  // --- Ladder of Abstraction & Critical Path Method ---
1805
1872
  async calculateCriticalPath(projectId, options = {}) {