@critical-path/core 0.20.1 → 0.20.2
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.
- package/CHANGELOG.md +10 -0
- package/dist/domain/entities.d.ts +4 -0
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +56 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/metrics.d.ts.map +1 -1
- package/dist/domain/metrics.js +2 -1
- package/dist/domain/metrics.js.map +1 -1
- package/dist/domain/task-duration.test.js +129 -0
- package/dist/domain/task-duration.test.js.map +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +76 -15
- package/dist/engine/index.js.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +11 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/entities.ts +56 -0
- package/src/domain/metrics.ts +2 -1
- package/src/domain/task-duration.test.ts +148 -0
- package/src/engine/index.ts +85 -22
- package/src/store/sqlite.ts +15 -5
- package/src/types/index.ts +6 -0
package/src/engine/index.ts
CHANGED
|
@@ -374,6 +374,10 @@ export class CriticalPathEngine {
|
|
|
374
374
|
const inProgressSince = processedInput.inProgressSince ?? taskInput.inProgressSince ?? (statusDef.category === 'in_progress' ? now : undefined);
|
|
375
375
|
const actualEndDate = processedInput.actualEndDate ?? taskInput.actualEndDate ?? ((statusDef.category === 'completed' || statusDef.category === 'canceled') ? now : undefined);
|
|
376
376
|
|
|
377
|
+
const isInitialBlocked = processedInput.isBlocked ?? taskInput.isBlocked ?? false;
|
|
378
|
+
const blockedSince = processedInput.blockedSince ?? taskInput.blockedSince ?? (isInitialBlocked && statusDef.category === 'in_progress' ? now : undefined);
|
|
379
|
+
const blockedDurationSeconds = processedInput.blockedDurationSeconds ?? taskInput.blockedDurationSeconds;
|
|
380
|
+
|
|
377
381
|
const created = await this.store.createTask({
|
|
378
382
|
projectId,
|
|
379
383
|
title: processedInput.title || taskInput.title,
|
|
@@ -403,8 +407,10 @@ export class CriticalPathEngine {
|
|
|
403
407
|
billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
|
|
404
408
|
actualDurationSeconds: processedInput.actualDurationSeconds ?? taskInput.actualDurationSeconds,
|
|
405
409
|
inProgressSince,
|
|
410
|
+
blockedDurationSeconds,
|
|
411
|
+
blockedSince,
|
|
406
412
|
progress: processedInput.progress ?? taskInput.progress ?? (statusDef.category === 'completed' ? 100 : 0),
|
|
407
|
-
isBlocked:
|
|
413
|
+
isBlocked: isInitialBlocked,
|
|
408
414
|
blockedReason: processedInput.blockedReason ?? taskInput.blockedReason ?? null,
|
|
409
415
|
tags: processedInput.tags ?? taskInput.tags ?? [],
|
|
410
416
|
customFields: processedInput.customFields ?? taskInput.customFields ?? {},
|
|
@@ -507,22 +513,25 @@ export class CriticalPathEngine {
|
|
|
507
513
|
|
|
508
514
|
const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, taskUpdates);
|
|
509
515
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
516
|
+
const now = new Date().toISOString();
|
|
517
|
+
const existingStatusDef = resolveStatusDefinition(
|
|
518
|
+
existing.status,
|
|
519
|
+
project?.statusDefinitions || workflow?.statuses
|
|
520
|
+
);
|
|
521
|
+
const newStatusDef = processedUpdates.status
|
|
522
|
+
? resolveStatusDefinition(
|
|
523
|
+
processedUpdates.status,
|
|
524
|
+
project?.statusDefinitions || workflow?.statuses
|
|
525
|
+
)
|
|
526
|
+
: undefined;
|
|
527
|
+
const effectiveCategory = newStatusDef?.category ?? existingStatusDef.category;
|
|
518
528
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
);
|
|
529
|
+
// Auto-update execution/completion timestamps if status changes
|
|
530
|
+
if (processedUpdates.status && processedUpdates.status !== existing.status && newStatusDef) {
|
|
531
|
+
processedUpdates.semanticStatus = processedUpdates.semanticStatus ?? newStatusDef.category;
|
|
523
532
|
|
|
524
|
-
// 1. Leaving in_progress: finalize active session duration
|
|
525
|
-
if (existingStatusDef.category === 'in_progress' &&
|
|
533
|
+
// 1. Leaving in_progress: finalize active session duration and blocked duration
|
|
534
|
+
if (existingStatusDef.category === 'in_progress' && newStatusDef.category !== 'in_progress') {
|
|
526
535
|
const inProgressSince = existing.inProgressSince;
|
|
527
536
|
if (inProgressSince) {
|
|
528
537
|
const startMs = new Date(inProgressSince).getTime();
|
|
@@ -540,16 +549,34 @@ export class CriticalPathEngine {
|
|
|
540
549
|
}
|
|
541
550
|
processedUpdates.inProgressSince = null;
|
|
542
551
|
}
|
|
552
|
+
|
|
553
|
+
const blockedSince = processedUpdates.blockedSince ?? existing.blockedSince;
|
|
554
|
+
if (blockedSince) {
|
|
555
|
+
const startMs = new Date(blockedSince).getTime();
|
|
556
|
+
const endMs = new Date(now).getTime();
|
|
557
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
558
|
+
const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
559
|
+
processedUpdates.blockedDurationSeconds =
|
|
560
|
+
(processedUpdates.blockedDurationSeconds ?? existing.blockedDurationSeconds ?? 0) + sessionSeconds;
|
|
561
|
+
}
|
|
562
|
+
processedUpdates.blockedSince = null;
|
|
563
|
+
}
|
|
543
564
|
}
|
|
544
565
|
|
|
545
|
-
// 2. Entering in_progress: retain earliest start date & start session timer
|
|
546
|
-
if (
|
|
566
|
+
// 2. Entering in_progress: retain earliest start date & start session timer (and blocked timer if blocked)
|
|
567
|
+
if (newStatusDef.category === 'in_progress') {
|
|
547
568
|
if (!existing.actualStartDate && !processedUpdates.actualStartDate) {
|
|
548
569
|
processedUpdates.actualStartDate = now;
|
|
549
570
|
}
|
|
550
571
|
if (!processedUpdates.inProgressSince) {
|
|
551
572
|
processedUpdates.inProgressSince = now;
|
|
552
573
|
}
|
|
574
|
+
const isTaskBlocked = processedUpdates.isBlocked !== undefined
|
|
575
|
+
? Boolean(processedUpdates.isBlocked)
|
|
576
|
+
: Boolean(existing.isBlocked);
|
|
577
|
+
if (isTaskBlocked && !processedUpdates.blockedSince) {
|
|
578
|
+
processedUpdates.blockedSince = now;
|
|
579
|
+
}
|
|
553
580
|
if (
|
|
554
581
|
(existingStatusDef.category === 'completed' || existingStatusDef.category === 'canceled') &&
|
|
555
582
|
processedUpdates.actualEndDate === undefined
|
|
@@ -558,21 +585,49 @@ export class CriticalPathEngine {
|
|
|
558
585
|
}
|
|
559
586
|
} else if (
|
|
560
587
|
(existingStatusDef.category === 'completed' || existingStatusDef.category === 'canceled') &&
|
|
561
|
-
|
|
588
|
+
newStatusDef.category === 'not_started' &&
|
|
562
589
|
processedUpdates.actualEndDate === undefined
|
|
563
590
|
) {
|
|
564
591
|
processedUpdates.actualEndDate = undefined;
|
|
565
592
|
}
|
|
566
593
|
|
|
567
594
|
// 3. Entering completed or canceled
|
|
568
|
-
if ((
|
|
595
|
+
if ((newStatusDef.category === 'completed' || newStatusDef.category === 'canceled') && !processedUpdates.actualEndDate) {
|
|
569
596
|
processedUpdates.actualEndDate = now;
|
|
570
|
-
if (
|
|
597
|
+
if (newStatusDef.category === 'completed' && processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
|
|
571
598
|
processedUpdates.progress = 100;
|
|
572
599
|
}
|
|
573
600
|
}
|
|
574
601
|
}
|
|
575
602
|
|
|
603
|
+
// Handle isBlocked transitions while in_progress (when in_progress, whether status changed or not)
|
|
604
|
+
if (effectiveCategory === 'in_progress') {
|
|
605
|
+
const wasBlocked = Boolean(existing.isBlocked);
|
|
606
|
+
const isNowBlocked = processedUpdates.isBlocked !== undefined
|
|
607
|
+
? Boolean(processedUpdates.isBlocked)
|
|
608
|
+
: wasBlocked;
|
|
609
|
+
|
|
610
|
+
if (!wasBlocked && isNowBlocked) {
|
|
611
|
+
if (!processedUpdates.blockedSince) {
|
|
612
|
+
processedUpdates.blockedSince = now;
|
|
613
|
+
}
|
|
614
|
+
} else if (wasBlocked && !isNowBlocked) {
|
|
615
|
+
const blockedSince = existing.blockedSince ?? processedUpdates.blockedSince;
|
|
616
|
+
if (blockedSince) {
|
|
617
|
+
const startMs = new Date(blockedSince).getTime();
|
|
618
|
+
const endMs = new Date(now).getTime();
|
|
619
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
620
|
+
const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
621
|
+
processedUpdates.blockedDurationSeconds =
|
|
622
|
+
(processedUpdates.blockedDurationSeconds ?? existing.blockedDurationSeconds ?? 0) + sessionSeconds;
|
|
623
|
+
}
|
|
624
|
+
processedUpdates.blockedSince = null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (processedUpdates.isBlocked === false && (existing.blockedSince || processedUpdates.blockedSince)) {
|
|
628
|
+
processedUpdates.blockedSince = null;
|
|
629
|
+
}
|
|
630
|
+
|
|
576
631
|
// Mutual sync for manual duration/effort edits
|
|
577
632
|
if (processedUpdates.actualDurationSeconds !== undefined && processedUpdates.actualHours === undefined) {
|
|
578
633
|
processedUpdates.actualHours = Math.round((processedUpdates.actualDurationSeconds / 3600) * 100) / 100;
|
|
@@ -591,7 +646,6 @@ export class CriticalPathEngine {
|
|
|
591
646
|
|
|
592
647
|
await this.plugins.runAfterTaskUpdate(updated, existing);
|
|
593
648
|
|
|
594
|
-
const now = new Date().toISOString();
|
|
595
649
|
const isStatusChange = existing.status !== updated.status;
|
|
596
650
|
|
|
597
651
|
// Publish typed Domain Events
|
|
@@ -741,9 +795,18 @@ export class CriticalPathEngine {
|
|
|
741
795
|
let updatedDownstream = downstream;
|
|
742
796
|
const updates: Partial<Task> = {};
|
|
743
797
|
|
|
744
|
-
if (downstream.isBlocked || downstream.blockedReason) {
|
|
798
|
+
if (downstream.isBlocked || downstream.blockedReason || downstream.blockedSince) {
|
|
745
799
|
updates.isBlocked = false;
|
|
746
800
|
updates.blockedReason = null;
|
|
801
|
+
if (downstream.blockedSince) {
|
|
802
|
+
const startMs = new Date(downstream.blockedSince).getTime();
|
|
803
|
+
const endMs = new Date().getTime();
|
|
804
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
805
|
+
const blockedSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
806
|
+
updates.blockedDurationSeconds = (downstream.blockedDurationSeconds ?? 0) + blockedSeconds;
|
|
807
|
+
}
|
|
808
|
+
updates.blockedSince = null;
|
|
809
|
+
}
|
|
747
810
|
}
|
|
748
811
|
|
|
749
812
|
if (Object.keys(updates).length > 0) {
|
package/src/store/sqlite.ts
CHANGED
|
@@ -133,6 +133,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
133
133
|
billableDurationMinutes REAL,
|
|
134
134
|
actualDurationSeconds REAL,
|
|
135
135
|
inProgressSince TEXT,
|
|
136
|
+
blockedDurationSeconds REAL,
|
|
137
|
+
blockedSince TEXT,
|
|
136
138
|
progress REAL,
|
|
137
139
|
isBlocked INTEGER,
|
|
138
140
|
blockedReason TEXT,
|
|
@@ -663,18 +665,18 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
663
665
|
reviewerId, iterationId, teamId, containerId, deliverableId, plannedStartDate, actualStartDate, actualEndDate,
|
|
664
666
|
dueDate, estimatedHours, loggedHours, actualHours, billableHours,
|
|
665
667
|
estimatedDurationMinutes, actualDurationMinutes, billableDurationMinutes,
|
|
666
|
-
actualDurationSeconds, inProgressSince, progress,
|
|
668
|
+
actualDurationSeconds, inProgressSince, blockedDurationSeconds, blockedSince, progress,
|
|
667
669
|
isBlocked, blockedReason,
|
|
668
670
|
tags, customFields, parentId, createdAt, updatedAt
|
|
669
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
671
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
670
672
|
`);
|
|
671
673
|
stmt.run(
|
|
672
674
|
newTask.id,
|
|
673
675
|
newTask.projectId,
|
|
674
676
|
newTask.title,
|
|
675
677
|
newTask.description || null,
|
|
676
|
-
newTask.status,
|
|
677
|
-
newTask.priority,
|
|
678
|
+
newTask.status || 'todo',
|
|
679
|
+
newTask.priority || 'medium',
|
|
678
680
|
newTask.taskType || null,
|
|
679
681
|
newTask.assigneeId || null,
|
|
680
682
|
newTask.assignees ? JSON.stringify(newTask.assignees) : null,
|
|
@@ -697,6 +699,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
697
699
|
newTask.billableDurationMinutes ?? null,
|
|
698
700
|
newTask.actualDurationSeconds ?? null,
|
|
699
701
|
newTask.inProgressSince ?? null,
|
|
702
|
+
newTask.blockedDurationSeconds ?? null,
|
|
703
|
+
newTask.blockedSince ?? null,
|
|
700
704
|
newTask.progress ?? null,
|
|
701
705
|
newTask.isBlocked !== undefined ? (newTask.isBlocked ? 1 : 0) : null,
|
|
702
706
|
newTask.blockedReason ?? null,
|
|
@@ -726,7 +730,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
726
730
|
plannedStartDate = ?, actualStartDate = ?, actualEndDate = ?, dueDate = ?,
|
|
727
731
|
estimatedHours = ?, loggedHours = ?, actualHours = ?, billableHours = ?,
|
|
728
732
|
estimatedDurationMinutes = ?, actualDurationMinutes = ?, billableDurationMinutes = ?,
|
|
729
|
-
actualDurationSeconds = ?, inProgressSince = ?, progress = ?,
|
|
733
|
+
actualDurationSeconds = ?, inProgressSince = ?, blockedDurationSeconds = ?, blockedSince = ?, progress = ?,
|
|
730
734
|
isBlocked = ?, blockedReason = ?,
|
|
731
735
|
tags = ?, customFields = ?, parentId = ?, updatedAt = ?
|
|
732
736
|
WHERE id = ?
|
|
@@ -759,6 +763,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
759
763
|
updated.billableDurationMinutes ?? null,
|
|
760
764
|
updated.actualDurationSeconds ?? null,
|
|
761
765
|
updated.inProgressSince ?? null,
|
|
766
|
+
updated.blockedDurationSeconds ?? null,
|
|
767
|
+
updated.blockedSince ?? null,
|
|
762
768
|
updated.progress ?? null,
|
|
763
769
|
updated.isBlocked !== undefined ? (updated.isBlocked ? 1 : 0) : null,
|
|
764
770
|
updated.blockedReason ?? null,
|
|
@@ -1204,6 +1210,10 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1204
1210
|
private mapTask(row: any): Task {
|
|
1205
1211
|
return {
|
|
1206
1212
|
...row,
|
|
1213
|
+
actualDurationSeconds: row.actualDurationSeconds ?? undefined,
|
|
1214
|
+
inProgressSince: row.inProgressSince || undefined,
|
|
1215
|
+
blockedDurationSeconds: row.blockedDurationSeconds ?? undefined,
|
|
1216
|
+
blockedSince: row.blockedSince || undefined,
|
|
1207
1217
|
isBlocked: row.isBlocked !== null && row.isBlocked !== undefined ? Boolean(row.isBlocked) : undefined,
|
|
1208
1218
|
blockedReason: row.blockedReason || undefined,
|
|
1209
1219
|
taskType: row.taskType || undefined,
|
package/src/types/index.ts
CHANGED
|
@@ -218,6 +218,10 @@ export interface Task {
|
|
|
218
218
|
actualDurationSeconds?: number;
|
|
219
219
|
/** Transient ISO timestamp marking the start of current 'in_progress' session (null when not in_progress) */
|
|
220
220
|
inProgressSince?: string | null;
|
|
221
|
+
/** Cumulative duration spent in blocked state while in progress (in seconds) */
|
|
222
|
+
blockedDurationSeconds?: number;
|
|
223
|
+
/** Transient ISO timestamp marking when the task became blocked while in progress (null when not blocked or not in_progress) */
|
|
224
|
+
blockedSince?: string | null;
|
|
221
225
|
// Progress (0 to 100 percentage)
|
|
222
226
|
progress?: number;
|
|
223
227
|
isBlocked?: boolean;
|
|
@@ -634,6 +638,8 @@ export interface TaskInferredActuals {
|
|
|
634
638
|
calendarDurationHours?: number;
|
|
635
639
|
/** Cumulative active execution duration in seconds */
|
|
636
640
|
actualDurationSeconds?: number;
|
|
641
|
+
/** Cumulative duration spent in blocked state while in progress (in seconds) */
|
|
642
|
+
blockedDurationSeconds?: number;
|
|
637
643
|
}
|
|
638
644
|
|
|
639
645
|
export interface TaskMetrics {
|