@critical-path/core 0.13.3 → 0.15.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.
- package/CHANGELOG.md +24 -0
- package/README.md +22 -0
- package/dist/deliverables.test.js +2 -2
- package/dist/deliverables.test.js.map +1 -1
- package/dist/domain/entities.d.ts +2 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +11 -4
- package/dist/domain/entities.js.map +1 -1
- package/dist/engine/index.d.ts +8 -3
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +56 -13
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +8 -8
- package/dist/index.test.js.map +1 -1
- package/dist/status.test.d.ts +2 -0
- package/dist/status.test.d.ts.map +1 -0
- package/dist/status.test.js +199 -0
- package/dist/status.test.js.map +1 -0
- package/dist/storage/file-storage.d.ts +1 -0
- package/dist/storage/file-storage.d.ts.map +1 -1
- package/dist/storage/file-storage.js +7 -0
- package/dist/storage/file-storage.js.map +1 -1
- package/dist/storage/firebase-storage.d.ts +2 -0
- package/dist/storage/firebase-storage.d.ts.map +1 -1
- package/dist/storage/firebase-storage.js +20 -0
- package/dist/storage/firebase-storage.js.map +1 -1
- package/dist/storage/storage.test.js +32 -0
- package/dist/storage/storage.test.js.map +1 -1
- package/dist/store/firebase.test.js +1 -1
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/sqlite.test.js +1 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +22 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/use-case-validation.test.js +1 -1
- package/dist/use-case-validation.test.js.map +1 -1
- package/dist/utils/status.d.ts +7 -10
- package/dist/utils/status.d.ts.map +1 -1
- package/dist/utils/status.js +104 -21
- package/dist/utils/status.js.map +1 -1
- package/dist/utils/workflow.js +15 -15
- package/dist/utils/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/deliverables.test.ts +2 -2
- package/src/domain/entities.ts +15 -5
- package/src/engine/index.ts +71 -16
- package/src/index.test.ts +8 -8
- package/src/status.test.ts +226 -0
- package/src/storage/file-storage.ts +8 -0
- package/src/storage/firebase-storage.ts +20 -0
- package/src/storage/storage.test.ts +40 -0
- package/src/store/firebase.test.ts +1 -1
- package/src/store/sqlite.test.ts +1 -1
- package/src/types/index.ts +24 -5
- package/src/use-case-validation.test.ts +1 -1
- package/src/utils/status.ts +119 -33
- package/src/utils/workflow.ts +15 -15
- package/tsconfig.tsbuildinfo +1 -1
package/dist/utils/workflow.js
CHANGED
|
@@ -79,13 +79,13 @@ export const DEFAULT_VFX_WORKFLOW = {
|
|
|
79
79
|
name: 'VFX Production Workflow',
|
|
80
80
|
description: 'Visual effects production pipeline from bidding and drafting to internal lead review and client supervisor approval.',
|
|
81
81
|
statuses: [
|
|
82
|
-
{ key: 'bidding', label: 'Bidding & Draft',
|
|
83
|
-
{ key: 'awarded', label: 'Awarded / Ready',
|
|
84
|
-
{ key: 'in_production', label: 'In Production',
|
|
85
|
-
{ key: 'internal_review', label: 'Internal Review',
|
|
86
|
-
{ key: 'client_review', label: 'Client Review',
|
|
87
|
-
{ key: 'revision_requested', label: 'Revision Requested',
|
|
88
|
-
{ key: 'approved', label: 'Approved (Final)',
|
|
82
|
+
{ key: 'bidding', label: 'Bidding & Draft', category: 'not_started' },
|
|
83
|
+
{ key: 'awarded', label: 'Awarded / Ready', category: 'not_started' },
|
|
84
|
+
{ key: 'in_production', label: 'In Production', category: 'in_progress' },
|
|
85
|
+
{ key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
|
|
86
|
+
{ key: 'client_review', label: 'Client Review', category: 'in_progress' },
|
|
87
|
+
{ key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
|
|
88
|
+
{ key: 'approved', label: 'Approved (Final)', category: 'completed' },
|
|
89
89
|
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
90
90
|
],
|
|
91
91
|
transitions: [
|
|
@@ -114,14 +114,14 @@ export const DEFAULT_CREATIVE_WORKFLOW = {
|
|
|
114
114
|
name: 'Creative Production Workflow',
|
|
115
115
|
description: 'Production pipeline for creative deliverables from briefing and concepts to internal review, client feedback, and final delivery.',
|
|
116
116
|
statuses: [
|
|
117
|
-
{ key: 'briefing', label: 'Briefing & Scoping',
|
|
118
|
-
{ key: 'concept', label: 'Concept & Storyboarding',
|
|
119
|
-
{ key: 'in_production', label: 'In Production',
|
|
120
|
-
{ key: 'internal_review', label: 'Internal Review',
|
|
121
|
-
{ key: 'client_review', label: 'Client Review',
|
|
122
|
-
{ key: 'revision_requested', label: 'Revision Requested',
|
|
123
|
-
{ key: 'approved', label: 'Approved',
|
|
124
|
-
{ key: 'delivered', label: 'Delivered',
|
|
117
|
+
{ key: 'briefing', label: 'Briefing & Scoping', category: 'not_started' },
|
|
118
|
+
{ key: 'concept', label: 'Concept & Storyboarding', category: 'in_progress' },
|
|
119
|
+
{ key: 'in_production', label: 'In Production', category: 'in_progress' },
|
|
120
|
+
{ key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
|
|
121
|
+
{ key: 'client_review', label: 'Client Review', category: 'in_progress' },
|
|
122
|
+
{ key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
|
|
123
|
+
{ key: 'approved', label: 'Approved', category: 'completed' },
|
|
124
|
+
{ key: 'delivered', label: 'Delivered', category: 'completed' },
|
|
125
125
|
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
126
126
|
],
|
|
127
127
|
transitions: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../../src/utils/workflow.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../../src/utils/workflow.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/deliverables.test.ts
CHANGED
|
@@ -238,8 +238,8 @@ describe('CriticalPathEngine Deliverables & Rollup Metrics', () => {
|
|
|
238
238
|
const summary = await engine.getDeliverableSummary(deliverable.id);
|
|
239
239
|
expect(summary).not.toBeNull();
|
|
240
240
|
expect(summary!.totalTasks).toBe(3);
|
|
241
|
-
expect(summary!.completedTasks).toBe(1); // 'approved' has
|
|
242
|
-
expect(summary!.activeTasks).toBe(1); // 'in_production' has
|
|
241
|
+
expect(summary!.completedTasks).toBe(1); // 'approved' has category 'completed'
|
|
242
|
+
expect(summary!.activeTasks).toBe(1); // 'in_production' has category 'in_progress'
|
|
243
243
|
expect(summary!.estimatedHours).toBe(26); // 8 + 12 + 6
|
|
244
244
|
expect(summary!.loggedHours).toBe(14); // 8 + 6 + 0
|
|
245
245
|
expect(summary!.progressPercentage).toBe(50); // (100 + 50 + 0) / 3 = 50%
|
package/src/domain/entities.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
CreateProjectInput,
|
|
12
12
|
Deliverable,
|
|
13
13
|
DeliverableStatus,
|
|
14
|
-
CreateDeliverableInput
|
|
14
|
+
CreateDeliverableInput,
|
|
15
|
+
SemanticStatus
|
|
15
16
|
} from '../types/index.js';
|
|
16
17
|
import type {
|
|
17
18
|
DomainEvent,
|
|
@@ -65,6 +66,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
65
66
|
public title: string;
|
|
66
67
|
public description?: string;
|
|
67
68
|
public status: TaskStatus;
|
|
69
|
+
public semanticStatus?: SemanticStatus;
|
|
68
70
|
public priority: Priority;
|
|
69
71
|
public taskType?: string;
|
|
70
72
|
public assigneeId?: string;
|
|
@@ -98,6 +100,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
98
100
|
this.title = data.title;
|
|
99
101
|
this.description = data.description;
|
|
100
102
|
this.status = data.status;
|
|
103
|
+
this.semanticStatus = data.semanticStatus;
|
|
101
104
|
this.priority = data.priority;
|
|
102
105
|
this.taskType = data.taskType;
|
|
103
106
|
this.assigneeId = data.assigneeId;
|
|
@@ -136,12 +139,16 @@ export class TaskEntity extends BaseEntity {
|
|
|
136
139
|
validateCustomFieldValues(options.customFieldDefs, input.customFields);
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
const status = input.status || options?.workflow?.defaultStatusKey || 'todo';
|
|
143
|
+
const statusDef = resolveStatusDefinition(status, options?.workflow?.statuses);
|
|
144
|
+
|
|
139
145
|
const task = new TaskEntity({
|
|
140
146
|
...input,
|
|
141
147
|
id,
|
|
142
148
|
createdAt: now,
|
|
143
149
|
updatedAt: now,
|
|
144
|
-
status
|
|
150
|
+
status,
|
|
151
|
+
semanticStatus: input.semanticStatus || statusDef.category,
|
|
145
152
|
priority: input.priority || 'medium',
|
|
146
153
|
loggedHours: input.loggedHours ?? 0,
|
|
147
154
|
progress: input.progress ?? 0,
|
|
@@ -180,12 +187,14 @@ export class TaskEntity extends BaseEntity {
|
|
|
180
187
|
const now = new Date().toISOString();
|
|
181
188
|
|
|
182
189
|
const statusDef = resolveStatusDefinition(newStatus, options?.statusDefs || workflow?.statuses);
|
|
183
|
-
|
|
190
|
+
this.semanticStatus = statusDef.category;
|
|
191
|
+
|
|
192
|
+
if (statusDef.category === 'in_progress' && !this.actualStartDate) {
|
|
184
193
|
this.actualStartDate = now;
|
|
185
194
|
}
|
|
186
|
-
if (statusDef.
|
|
195
|
+
if (statusDef.category === 'completed' || statusDef.category === 'canceled') {
|
|
187
196
|
this.actualEndDate = now;
|
|
188
|
-
if (this.progress !== undefined && this.progress < 100) {
|
|
197
|
+
if (statusDef.category === 'completed' && this.progress !== undefined && this.progress < 100) {
|
|
189
198
|
this.progress = 100;
|
|
190
199
|
}
|
|
191
200
|
}
|
|
@@ -365,6 +374,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
365
374
|
title: this.title,
|
|
366
375
|
description: this.description,
|
|
367
376
|
status: this.status,
|
|
377
|
+
semanticStatus: this.semanticStatus,
|
|
368
378
|
priority: this.priority,
|
|
369
379
|
taskType: this.taskType,
|
|
370
380
|
assigneeId: this.assigneeId,
|
package/src/engine/index.ts
CHANGED
|
@@ -20,11 +20,12 @@ import type {
|
|
|
20
20
|
CreateProjectInput,
|
|
21
21
|
Deliverable,
|
|
22
22
|
DeliverableSummary,
|
|
23
|
-
CreateDeliverableInput
|
|
23
|
+
CreateDeliverableInput,
|
|
24
|
+
TaskLifecycleState
|
|
24
25
|
} from '../types/index.js';
|
|
25
26
|
import { StorageAdapter, InMemoryStore } from '../store/index.js';
|
|
26
27
|
import { PluginRegistry } from '../plugins/index.js';
|
|
27
|
-
import { deriveTaskLifecycleState,
|
|
28
|
+
import { deriveTaskLifecycleState, resolveStatusDefinition } from '../utils/status.js';
|
|
28
29
|
import {
|
|
29
30
|
validateTransition,
|
|
30
31
|
getAllowedNextStatuses,
|
|
@@ -335,16 +336,18 @@ export class CriticalPathEngine {
|
|
|
335
336
|
const initialStatus = processedInput.status || taskInput.status || defaultStatus;
|
|
336
337
|
const now = new Date().toISOString();
|
|
337
338
|
|
|
338
|
-
// Derive initial lifecycle timestamps
|
|
339
|
+
// Derive initial lifecycle timestamps and semantic status
|
|
339
340
|
const statusDef = resolveStatusDefinition(initialStatus, project?.statusDefinitions || workflow?.statuses);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
341
|
+
const semanticStatus = processedInput.semanticStatus ?? taskInput.semanticStatus ?? statusDef.category;
|
|
342
|
+
const actualStartDate = processedInput.actualStartDate ?? taskInput.actualStartDate ?? (statusDef.category === 'in_progress' ? now : undefined);
|
|
343
|
+
const actualEndDate = processedInput.actualEndDate ?? taskInput.actualEndDate ?? ((statusDef.category === 'completed' || statusDef.category === 'canceled') ? now : undefined);
|
|
342
344
|
|
|
343
345
|
const created = await this.store.createTask({
|
|
344
346
|
projectId,
|
|
345
347
|
title: processedInput.title || taskInput.title,
|
|
346
348
|
description: processedInput.description ?? taskInput.description,
|
|
347
349
|
status: initialStatus,
|
|
350
|
+
semanticStatus,
|
|
348
351
|
priority: processedInput.priority || taskInput.priority || 'medium',
|
|
349
352
|
taskType: processedInput.taskType || taskInput.taskType || 'task',
|
|
350
353
|
assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
|
|
@@ -366,7 +369,7 @@ export class CriticalPathEngine {
|
|
|
366
369
|
estimatedDurationMinutes: processedInput.estimatedDurationMinutes ?? taskInput.estimatedDurationMinutes,
|
|
367
370
|
actualDurationMinutes: processedInput.actualDurationMinutes ?? taskInput.actualDurationMinutes,
|
|
368
371
|
billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
|
|
369
|
-
progress: processedInput.progress ?? taskInput.progress ?? (statusDef.
|
|
372
|
+
progress: processedInput.progress ?? taskInput.progress ?? (statusDef.category === 'completed' ? 100 : 0),
|
|
370
373
|
tags: processedInput.tags ?? taskInput.tags ?? [],
|
|
371
374
|
customFields: processedInput.customFields ?? taskInput.customFields ?? {},
|
|
372
375
|
parentId: processedInput.parentId ?? taskInput.parentId
|
|
@@ -428,13 +431,14 @@ export class CriticalPathEngine {
|
|
|
428
431
|
processedUpdates.status,
|
|
429
432
|
project?.statusDefinitions || workflow?.statuses
|
|
430
433
|
);
|
|
434
|
+
processedUpdates.semanticStatus = processedUpdates.semanticStatus ?? statusDef.category;
|
|
431
435
|
const now = new Date().toISOString();
|
|
432
|
-
if (statusDef.
|
|
436
|
+
if (statusDef.category === 'in_progress' && !existing.actualStartDate && !processedUpdates.actualStartDate) {
|
|
433
437
|
processedUpdates.actualStartDate = now;
|
|
434
438
|
}
|
|
435
|
-
if (statusDef.
|
|
439
|
+
if ((statusDef.category === 'completed' || statusDef.category === 'canceled') && !processedUpdates.actualEndDate) {
|
|
436
440
|
processedUpdates.actualEndDate = now;
|
|
437
|
-
if (processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
|
|
441
|
+
if (statusDef.category === 'completed' && processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
|
|
438
442
|
processedUpdates.progress = 100;
|
|
439
443
|
}
|
|
440
444
|
}
|
|
@@ -590,13 +594,25 @@ export class CriticalPathEngine {
|
|
|
590
594
|
};
|
|
591
595
|
}
|
|
592
596
|
|
|
593
|
-
async getTaskLifecycleState(
|
|
597
|
+
async getTaskLifecycleState(
|
|
598
|
+
taskId: string,
|
|
599
|
+
options?: { referenceDate?: Date; stalledThresholdDays?: number }
|
|
600
|
+
): Promise<TaskLifecycleState | null> {
|
|
594
601
|
const task = await this.getTask(taskId);
|
|
595
602
|
if (!task) return null;
|
|
596
603
|
const project = await this.getProject(task.projectId);
|
|
597
604
|
const workflow = await this.resolveProjectWorkflow(task.projectId);
|
|
598
605
|
const statusDefs = project?.statusDefinitions || workflow?.statuses;
|
|
599
|
-
|
|
606
|
+
|
|
607
|
+
const graph = await this.getTaskDependencyGraph(taskId);
|
|
608
|
+
const upstreamTasks = graph.upstreamTasks;
|
|
609
|
+
|
|
610
|
+
return deriveTaskLifecycleState(task, {
|
|
611
|
+
customDefinitions: statusDefs,
|
|
612
|
+
referenceDate: options?.referenceDate,
|
|
613
|
+
stalledThresholdDays: options?.stalledThresholdDays,
|
|
614
|
+
upstreamTasks
|
|
615
|
+
});
|
|
600
616
|
}
|
|
601
617
|
|
|
602
618
|
// --- Time Tracking ---
|
|
@@ -790,8 +806,12 @@ export class CriticalPathEngine {
|
|
|
790
806
|
}
|
|
791
807
|
|
|
792
808
|
// --- Attachments ---
|
|
793
|
-
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
|
|
794
|
-
|
|
809
|
+
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string; artifactType?: string }): Promise<Attachment[]> {
|
|
810
|
+
const attachments = await this.store.getAttachments(filter);
|
|
811
|
+
if (filter?.artifactType) {
|
|
812
|
+
return attachments.filter(a => a.artifactType === filter.artifactType);
|
|
813
|
+
}
|
|
814
|
+
return attachments;
|
|
795
815
|
}
|
|
796
816
|
|
|
797
817
|
async getAttachment(id: string): Promise<Attachment | null> {
|
|
@@ -854,6 +874,7 @@ export class CriticalPathEngine {
|
|
|
854
874
|
commentId?: string;
|
|
855
875
|
uploaderId: string;
|
|
856
876
|
uploaderType?: 'user' | 'agent' | 'system';
|
|
877
|
+
artifactType?: 'plan' | 'spec' | 'deliverable' | 'review' | 'general';
|
|
857
878
|
metadata?: Record<string, unknown>;
|
|
858
879
|
}
|
|
859
880
|
): Promise<Attachment> {
|
|
@@ -879,10 +900,44 @@ export class CriticalPathEngine {
|
|
|
879
900
|
sizeBytes: uploadResult.sizeBytes,
|
|
880
901
|
url: uploadResult.url,
|
|
881
902
|
storageKey: uploadResult.storageKey,
|
|
903
|
+
artifactType: input.artifactType,
|
|
882
904
|
metadata: input.metadata
|
|
883
905
|
});
|
|
884
906
|
}
|
|
885
907
|
|
|
908
|
+
async readAttachmentText(id: string): Promise<string> {
|
|
909
|
+
const attachment = await this.getAttachment(id);
|
|
910
|
+
if (!attachment) {
|
|
911
|
+
throw new Error(`Attachment with id "${id}" not found.`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (attachment.storageKey && this.fileStorage && typeof this.fileStorage.download === 'function') {
|
|
915
|
+
const bytes = await this.fileStorage.download(attachment.storageKey);
|
|
916
|
+
return new TextDecoder().decode(bytes);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (attachment.url) {
|
|
920
|
+
if (attachment.url.startsWith('data:')) {
|
|
921
|
+
const commaIndex = attachment.url.indexOf(',');
|
|
922
|
+
const encoded = commaIndex !== -1 ? attachment.url.substring(commaIndex + 1) : attachment.url;
|
|
923
|
+
if (typeof Buffer !== 'undefined') {
|
|
924
|
+
return Buffer.from(encoded, 'base64').toString('utf-8');
|
|
925
|
+
}
|
|
926
|
+
if (typeof atob === 'function') {
|
|
927
|
+
return atob(encoded);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const res = await fetch(attachment.url);
|
|
932
|
+
if (!res.ok) {
|
|
933
|
+
throw new Error(`Failed to fetch attachment from URL "${attachment.url}": ${res.statusText}`);
|
|
934
|
+
}
|
|
935
|
+
return await res.text();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
throw new Error(`Cannot read attachment "${id}": no valid storageKey or URL found.`);
|
|
939
|
+
}
|
|
940
|
+
|
|
886
941
|
async getPresignedAttachmentUploadUrl(options: PresignedUrlOptions): Promise<PresignedUploadResult> {
|
|
887
942
|
if (!this.fileStorage || !this.fileStorage.getPresignedUploadUrl) {
|
|
888
943
|
throw new Error('Presigned uploads are not supported by the configured FileStorageAdapter.');
|
|
@@ -1103,9 +1158,9 @@ export class CriticalPathEngine {
|
|
|
1103
1158
|
|
|
1104
1159
|
for (const task of tasks) {
|
|
1105
1160
|
const statusDef = resolveStatusDefinition(task.status, project?.statusDefinitions || workflow?.statuses);
|
|
1106
|
-
if (statusDef.
|
|
1161
|
+
if (statusDef.category === 'completed') {
|
|
1107
1162
|
completedTasks++;
|
|
1108
|
-
} else if (statusDef.
|
|
1163
|
+
} else if (statusDef.category === 'in_progress') {
|
|
1109
1164
|
activeTasks++;
|
|
1110
1165
|
}
|
|
1111
1166
|
|
|
@@ -1115,7 +1170,7 @@ export class CriticalPathEngine {
|
|
|
1115
1170
|
if (typeof task.progress === 'number') {
|
|
1116
1171
|
totalProgress += task.progress;
|
|
1117
1172
|
} else {
|
|
1118
|
-
totalProgress += statusDef.
|
|
1173
|
+
totalProgress += statusDef.category === 'completed' ? 100 : 0;
|
|
1119
1174
|
}
|
|
1120
1175
|
}
|
|
1121
1176
|
|
package/src/index.test.ts
CHANGED
|
@@ -128,9 +128,9 @@ describe('CriticalPathEngine Core Tests', () => {
|
|
|
128
128
|
key: 'CUST',
|
|
129
129
|
name: 'Custom Workflow Project',
|
|
130
130
|
statusDefinitions: [
|
|
131
|
-
{ key: 'draft', label: 'Draft',
|
|
132
|
-
{ key: 'active_work', label: 'Active Work',
|
|
133
|
-
{ key: 'finished', label: 'Finished',
|
|
131
|
+
{ key: 'draft', label: 'Draft', category: 'not_started' },
|
|
132
|
+
{ key: 'active_work', label: 'Active Work', category: 'in_progress' },
|
|
133
|
+
{ key: 'finished', label: 'Finished', category: 'completed' }
|
|
134
134
|
]
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -145,10 +145,10 @@ describe('CriticalPathEngine Core Tests', () => {
|
|
|
145
145
|
|
|
146
146
|
const state = await engine.getTaskLifecycleState(task.id);
|
|
147
147
|
expect(state).not.toBeNull();
|
|
148
|
-
expect(state?.
|
|
149
|
-
expect(state?.executionState).toBe('active');
|
|
148
|
+
expect(state?.semanticStatus).toBe('in_progress');
|
|
150
149
|
expect(state?.isActive).toBe(true);
|
|
151
150
|
expect(state?.isDone).toBe(false);
|
|
151
|
+
expect(state?.isBlocked).toBe(false);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it('builds task dependency graph with upstream and downstream tasks', async () => {
|
|
@@ -181,9 +181,9 @@ describe('CriticalPathEngine Core Tests', () => {
|
|
|
181
181
|
name: 'Strict Software Workflow',
|
|
182
182
|
defaultStatusKey: 'backlog',
|
|
183
183
|
statuses: [
|
|
184
|
-
{ key: 'backlog', label: 'Backlog',
|
|
185
|
-
{ key: 'in_progress', label: 'In Progress',
|
|
186
|
-
{ key: 'done', label: 'Done',
|
|
184
|
+
{ key: 'backlog', label: 'Backlog', category: 'not_started' },
|
|
185
|
+
{ key: 'in_progress', label: 'In Progress', category: 'in_progress' },
|
|
186
|
+
{ key: 'done', label: 'Done', category: 'completed' }
|
|
187
187
|
],
|
|
188
188
|
transitions: [
|
|
189
189
|
{ id: 't1', fromStatusKey: 'backlog', toStatusKey: 'in_progress', name: 'Start Work' },
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CriticalPathEngine } from './engine/index.js';
|
|
3
|
+
import { resolveStatusDefinition, deriveTaskLifecycleState } from './utils/status.js';
|
|
4
|
+
import type { Task, StatusDefinition } from './types/index.js';
|
|
5
|
+
|
|
6
|
+
describe('Universal Semantic Statuses & Implied Statuses', () => {
|
|
7
|
+
describe('resolveStatusDefinition', () => {
|
|
8
|
+
it('resolves core standard statuses to their semantic category', () => {
|
|
9
|
+
expect(resolveStatusDefinition('todo').category).toBe('not_started');
|
|
10
|
+
expect(resolveStatusDefinition('backlog').category).toBe('not_started');
|
|
11
|
+
expect(resolveStatusDefinition('in_progress').category).toBe('in_progress');
|
|
12
|
+
expect(resolveStatusDefinition('in_review').category).toBe('in_progress');
|
|
13
|
+
expect(resolveStatusDefinition('done').category).toBe('completed');
|
|
14
|
+
expect(resolveStatusDefinition('canceled').category).toBe('canceled');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('resolves custom status definitions when provided', () => {
|
|
18
|
+
const customDefs: StatusDefinition[] = [
|
|
19
|
+
{ key: 'concept', label: 'Concept & Brainstorming', category: 'not_started' },
|
|
20
|
+
{ key: 'client_review', label: 'Client Feedback Session', category: 'in_progress' },
|
|
21
|
+
{ key: 'shipped', label: 'Shipped to Customer', category: 'completed' },
|
|
22
|
+
{ key: 'abandoned', label: 'Abandoned Initiative', category: 'canceled' }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
expect(resolveStatusDefinition('concept', customDefs).category).toBe('not_started');
|
|
26
|
+
expect(resolveStatusDefinition('client_review', customDefs).category).toBe('in_progress');
|
|
27
|
+
expect(resolveStatusDefinition('shipped', customDefs).category).toBe('completed');
|
|
28
|
+
expect(resolveStatusDefinition('abandoned', customDefs).category).toBe('canceled');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('infers category heuristically when unknown status key is provided', () => {
|
|
32
|
+
expect(resolveStatusDefinition('draft_v2').category).toBe('not_started');
|
|
33
|
+
expect(resolveStatusDefinition('final_approved').category).toBe('completed');
|
|
34
|
+
expect(resolveStatusDefinition('active_development').category).toBe('in_progress');
|
|
35
|
+
expect(resolveStatusDefinition('rejected_proposal').category).toBe('canceled');
|
|
36
|
+
expect(resolveStatusDefinition('custom_arbitrary_key').category).toBe('not_started');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('deriveTaskLifecycleState', () => {
|
|
41
|
+
const baseTask: Task = {
|
|
42
|
+
id: 'task_1',
|
|
43
|
+
projectId: 'proj_1',
|
|
44
|
+
title: 'Design Wireframes',
|
|
45
|
+
status: 'todo',
|
|
46
|
+
priority: 'high',
|
|
47
|
+
createdAt: '2026-09-01T00:00:00.000Z',
|
|
48
|
+
updatedAt: '2026-09-01T00:00:00.000Z'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
it('derives basic state for not_started task', () => {
|
|
52
|
+
const derived = deriveTaskLifecycleState(baseTask);
|
|
53
|
+
expect(derived.semanticStatus).toBe('not_started');
|
|
54
|
+
expect(derived.isActive).toBe(false);
|
|
55
|
+
expect(derived.isDone).toBe(false);
|
|
56
|
+
expect(derived.isCancelled).toBe(false);
|
|
57
|
+
expect(derived.isReady).toBe(true);
|
|
58
|
+
expect(derived.isBlocked).toBe(false);
|
|
59
|
+
expect(derived.isUnassigned).toBe(true);
|
|
60
|
+
expect(derived.isUnplanned).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('derives blocked and ready states based on upstream tasks', () => {
|
|
64
|
+
const upstream1: Task = {
|
|
65
|
+
...baseTask,
|
|
66
|
+
id: 'task_up1',
|
|
67
|
+
status: 'in_progress',
|
|
68
|
+
semanticStatus: 'in_progress'
|
|
69
|
+
};
|
|
70
|
+
const upstream2: Task = {
|
|
71
|
+
...baseTask,
|
|
72
|
+
id: 'task_up2',
|
|
73
|
+
status: 'done',
|
|
74
|
+
semanticStatus: 'completed'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// When upstream1 is in progress, task_1 is blocked
|
|
78
|
+
const blockedState = deriveTaskLifecycleState(baseTask, {
|
|
79
|
+
upstreamTasks: [upstream1, upstream2]
|
|
80
|
+
});
|
|
81
|
+
expect(blockedState.isBlocked).toBe(true);
|
|
82
|
+
expect(blockedState.isReady).toBe(false);
|
|
83
|
+
expect(blockedState.blockingTaskIds).toEqual(['task_up1']);
|
|
84
|
+
|
|
85
|
+
// When all upstream tasks are completed, task_1 becomes ready
|
|
86
|
+
const allDoneState = deriveTaskLifecycleState(baseTask, {
|
|
87
|
+
upstreamTasks: [
|
|
88
|
+
{ ...upstream1, status: 'done', semanticStatus: 'completed' },
|
|
89
|
+
upstream2
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
expect(allDoneState.isBlocked).toBe(false);
|
|
93
|
+
expect(allDoneState.isReady).toBe(true);
|
|
94
|
+
expect(allDoneState.blockingTaskIds).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('derives schedule indicators: overdue, upcoming, unplanned', () => {
|
|
98
|
+
const refDate = new Date('2026-09-10T12:00:00.000Z');
|
|
99
|
+
|
|
100
|
+
// Overdue task
|
|
101
|
+
const overdueTask: Task = {
|
|
102
|
+
...baseTask,
|
|
103
|
+
dueDate: '2026-09-08T00:00:00.000Z'
|
|
104
|
+
};
|
|
105
|
+
const overdueDerived = deriveTaskLifecycleState(overdueTask, { referenceDate: refDate });
|
|
106
|
+
expect(overdueDerived.isOverdue).toBe(true);
|
|
107
|
+
expect(overdueDerived.isUnplanned).toBe(false);
|
|
108
|
+
|
|
109
|
+
// Completed task is not overdue even if dueDate is past
|
|
110
|
+
const completedTask: Task = {
|
|
111
|
+
...overdueTask,
|
|
112
|
+
status: 'done'
|
|
113
|
+
};
|
|
114
|
+
const completedDerived = deriveTaskLifecycleState(completedTask, { referenceDate: refDate });
|
|
115
|
+
expect(completedDerived.isOverdue).toBe(false);
|
|
116
|
+
expect(completedDerived.isDone).toBe(true);
|
|
117
|
+
|
|
118
|
+
// Upcoming task
|
|
119
|
+
const upcomingTask: Task = {
|
|
120
|
+
...baseTask,
|
|
121
|
+
plannedStartDate: '2026-09-12T00:00:00.000Z' // within 3 days of Sept 10
|
|
122
|
+
};
|
|
123
|
+
const upcomingDerived = deriveTaskLifecycleState(upcomingTask, { referenceDate: refDate });
|
|
124
|
+
expect(upcomingDerived.isUpcoming).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('derives resource indicators: unassigned and stalled', () => {
|
|
128
|
+
const refDate = new Date('2026-09-15T12:00:00.000Z');
|
|
129
|
+
|
|
130
|
+
// Assigned via assigneeId
|
|
131
|
+
const assignedTask1: Task = {
|
|
132
|
+
...baseTask,
|
|
133
|
+
assigneeId: 'user_alice'
|
|
134
|
+
};
|
|
135
|
+
expect(deriveTaskLifecycleState(assignedTask1).isUnassigned).toBe(false);
|
|
136
|
+
|
|
137
|
+
// Assigned via assignees array
|
|
138
|
+
const assignedTask2: Task = {
|
|
139
|
+
...baseTask,
|
|
140
|
+
assignees: [{ id: 'user_bob' }]
|
|
141
|
+
};
|
|
142
|
+
expect(deriveTaskLifecycleState(assignedTask2).isUnassigned).toBe(false);
|
|
143
|
+
|
|
144
|
+
// Stalled task: in_progress with no update in > 7 days
|
|
145
|
+
const stalledTask: Task = {
|
|
146
|
+
...baseTask,
|
|
147
|
+
status: 'in_progress',
|
|
148
|
+
updatedAt: '2026-09-01T00:00:00.000Z'
|
|
149
|
+
};
|
|
150
|
+
const stalledDerived = deriveTaskLifecycleState(stalledTask, { referenceDate: refDate, stalledThresholdDays: 7 });
|
|
151
|
+
expect(stalledDerived.isStalled).toBe(true);
|
|
152
|
+
|
|
153
|
+
// Not stalled if updated recently
|
|
154
|
+
const activeTask: Task = {
|
|
155
|
+
...stalledTask,
|
|
156
|
+
updatedAt: '2026-09-14T00:00:00.000Z'
|
|
157
|
+
};
|
|
158
|
+
expect(deriveTaskLifecycleState(activeTask, { referenceDate: refDate, stalledThresholdDays: 7 }).isStalled).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('derives estimate indicators: isOverEstimate and isPaceWarning', () => {
|
|
162
|
+
// Over estimate by logged hours
|
|
163
|
+
const overEstimateTask: Task = {
|
|
164
|
+
...baseTask,
|
|
165
|
+
status: 'in_progress',
|
|
166
|
+
estimatedHours: 10,
|
|
167
|
+
loggedHours: 12
|
|
168
|
+
};
|
|
169
|
+
const overEstDerived = deriveTaskLifecycleState(overEstimateTask);
|
|
170
|
+
expect(overEstDerived.isOverEstimate).toBe(true);
|
|
171
|
+
|
|
172
|
+
// Pace warning: in_progress task that has been active longer than estimated duration
|
|
173
|
+
const refDate = new Date('2026-09-10T00:00:00.000Z');
|
|
174
|
+
const paceWarningTask: Task = {
|
|
175
|
+
...baseTask,
|
|
176
|
+
status: 'in_progress',
|
|
177
|
+
actualStartDate: '2026-09-01T00:00:00.000Z', // 9 days = 216 hours elapsed
|
|
178
|
+
estimatedHours: 40 // only 40 hours estimated
|
|
179
|
+
};
|
|
180
|
+
const paceDerived = deriveTaskLifecycleState(paceWarningTask, { referenceDate: refDate });
|
|
181
|
+
expect(paceDerived.isPaceWarning).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Engine Integration', () => {
|
|
186
|
+
it('computes full lifecycle state with dependency graph integration', async () => {
|
|
187
|
+
const engine = new CriticalPathEngine();
|
|
188
|
+
const proj = await engine.createProject({ key: 'LIFECYCLE', name: 'Lifecycle Test' });
|
|
189
|
+
|
|
190
|
+
const taskA = await engine.createTask({
|
|
191
|
+
projectId: proj.id,
|
|
192
|
+
title: 'Task A (Dependency)',
|
|
193
|
+
status: 'todo',
|
|
194
|
+
priority: 'high'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const taskB = await engine.createTask({
|
|
198
|
+
projectId: proj.id,
|
|
199
|
+
title: 'Task B (Dependent)',
|
|
200
|
+
status: 'todo',
|
|
201
|
+
priority: 'medium'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await engine.store.addDependency({
|
|
205
|
+
taskId: taskB.id,
|
|
206
|
+
dependsOnTaskId: taskA.id,
|
|
207
|
+
type: 'blocking'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Task B should be blocked by Task A
|
|
211
|
+
let stateB = await engine.getTaskLifecycleState(taskB.id);
|
|
212
|
+
expect(stateB?.isBlocked).toBe(true);
|
|
213
|
+
expect(stateB?.isReady).toBe(false);
|
|
214
|
+
expect(stateB?.blockingTaskIds).toEqual([taskA.id]);
|
|
215
|
+
|
|
216
|
+
// Complete Task A
|
|
217
|
+
await engine.updateTask(taskA.id, { status: 'done' });
|
|
218
|
+
|
|
219
|
+
// Task B should now be ready
|
|
220
|
+
stateB = await engine.getTaskLifecycleState(taskB.id);
|
|
221
|
+
expect(stateB?.isBlocked).toBe(false);
|
|
222
|
+
expect(stateB?.isReady).toBe(true);
|
|
223
|
+
expect(stateB?.blockingTaskIds).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -123,6 +123,14 @@ export class InMemoryFileStore implements FileStorageAdapter {
|
|
|
123
123
|
return this.files.delete(storageKey);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
async download(storageKey: string): Promise<Buffer | Uint8Array> {
|
|
127
|
+
const file = this.files.get(storageKey);
|
|
128
|
+
if (!file) {
|
|
129
|
+
throw new Error(`File with storageKey "${storageKey}" not found`);
|
|
130
|
+
}
|
|
131
|
+
return file.data;
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
async getDownloadUrl(storageKey: string): Promise<string> {
|
|
127
135
|
const file = this.files.get(storageKey);
|
|
128
136
|
if (!file) {
|