@critical-path/core 0.16.0 → 0.18.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 +19 -1
- package/README.md +50 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +5 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +2 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/ladder.d.ts.map +1 -1
- package/dist/domain/ladder.js +14 -0
- package/dist/domain/ladder.js.map +1 -1
- package/dist/domain/ladder.test.js +3 -3
- package/dist/domain/ladder.test.js.map +1 -1
- package/dist/domain/metrics.d.ts +29 -0
- package/dist/domain/metrics.d.ts.map +1 -0
- package/dist/domain/metrics.js +403 -0
- package/dist/domain/metrics.js.map +1 -0
- package/dist/domain/metrics.test.d.ts +2 -0
- package/dist/domain/metrics.test.d.ts.map +1 -0
- package/dist/domain/metrics.test.js +251 -0
- package/dist/domain/metrics.test.js.map +1 -0
- package/dist/domain/workload.d.ts +10 -0
- package/dist/domain/workload.d.ts.map +1 -0
- package/dist/domain/workload.js +349 -0
- package/dist/domain/workload.js.map +1 -0
- package/dist/domain/workload.test.d.ts +2 -0
- package/dist/domain/workload.test.d.ts.map +1 -0
- package/dist/domain/workload.test.js +201 -0
- package/dist/domain/workload.test.js.map +1 -0
- package/dist/engine/index.d.ts +5 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +55 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/types/index.d.ts +91 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/entities.ts +6 -0
- package/src/domain/index.ts +2 -0
- package/src/domain/ladder.test.ts +3 -3
- package/src/domain/ladder.ts +16 -0
- package/src/domain/metrics.test.ts +286 -0
- package/src/domain/metrics.ts +484 -0
- package/src/domain/workload.test.ts +238 -0
- package/src/domain/workload.ts +403 -0
- package/src/engine/index.ts +86 -3
- package/src/types/index.ts +110 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/engine/index.ts
CHANGED
|
@@ -28,7 +28,11 @@ import type {
|
|
|
28
28
|
TaskLadderView,
|
|
29
29
|
MacroPhaseRollup,
|
|
30
30
|
StandardTaskTimelineItem,
|
|
31
|
-
ConcreteTaskEvidence
|
|
31
|
+
ConcreteTaskEvidence,
|
|
32
|
+
TaskMetrics,
|
|
33
|
+
TaskProgressHistory,
|
|
34
|
+
WorkloadDistribution,
|
|
35
|
+
WorkloadDistributionOptions
|
|
32
36
|
} from '../types/index.js';
|
|
33
37
|
import { StorageAdapter, InMemoryStore } from '../store/index.js';
|
|
34
38
|
import { PluginRegistry } from '../plugins/index.js';
|
|
@@ -77,6 +81,12 @@ import { validateCustomFieldValues } from '../domain/custom-fields.js';
|
|
|
77
81
|
import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
|
|
78
82
|
import { calculateCPM } from '../domain/cpm.js';
|
|
79
83
|
import { buildTimelineLadder, aggregateConcreteEvidenceForTask } from '../domain/ladder.js';
|
|
84
|
+
import {
|
|
85
|
+
calculateTaskMetrics,
|
|
86
|
+
reconstructTaskProgressHistory,
|
|
87
|
+
type MetricOptions
|
|
88
|
+
} from '../domain/metrics.js';
|
|
89
|
+
import { calculateWorkloadDistribution } from '../domain/workload.js';
|
|
80
90
|
|
|
81
91
|
export class CriticalPathEngine {
|
|
82
92
|
public readonly store: StorageAdapter;
|
|
@@ -451,6 +461,19 @@ export class CriticalPathEngine {
|
|
|
451
461
|
);
|
|
452
462
|
processedUpdates.semanticStatus = processedUpdates.semanticStatus ?? statusDef.category;
|
|
453
463
|
const now = new Date().toISOString();
|
|
464
|
+
|
|
465
|
+
const existingStatusDef = resolveStatusDefinition(
|
|
466
|
+
existing.status,
|
|
467
|
+
project?.statusDefinitions || workflow?.statuses
|
|
468
|
+
);
|
|
469
|
+
if (
|
|
470
|
+
(existingStatusDef.category === 'completed' || existingStatusDef.category === 'canceled') &&
|
|
471
|
+
(statusDef.category === 'in_progress' || statusDef.category === 'not_started') &&
|
|
472
|
+
processedUpdates.actualEndDate === undefined
|
|
473
|
+
) {
|
|
474
|
+
processedUpdates.actualEndDate = undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
454
477
|
if (statusDef.category === 'in_progress' && !existing.actualStartDate && !processedUpdates.actualStartDate) {
|
|
455
478
|
processedUpdates.actualStartDate = now;
|
|
456
479
|
}
|
|
@@ -1355,7 +1378,7 @@ export class CriticalPathEngine {
|
|
|
1355
1378
|
return this.store.deleteIteration(id);
|
|
1356
1379
|
}
|
|
1357
1380
|
|
|
1358
|
-
// ---
|
|
1381
|
+
// --- Ladder of Abstraction & Critical Path Method ---
|
|
1359
1382
|
async calculateCriticalPath(projectId: string): Promise<CriticalPathAnalysis> {
|
|
1360
1383
|
const tasks = await this.store.getTasks(projectId);
|
|
1361
1384
|
const allDepArrays = await Promise.all(tasks.map((t) => this.store.getDependencies(t.id)));
|
|
@@ -1441,15 +1464,75 @@ export class CriticalPathEngine {
|
|
|
1441
1464
|
|
|
1442
1465
|
const concrete = ladder.concrete?.[taskId] || aggregateConcreteEvidenceForTask(task);
|
|
1443
1466
|
const macroPhase = ladder.macro?.phases.find((p) => p.taskIds.includes(taskId));
|
|
1467
|
+
const metrics = await this.getTaskMetrics(taskId);
|
|
1444
1468
|
|
|
1445
1469
|
return {
|
|
1446
1470
|
taskId,
|
|
1447
1471
|
macroPhase,
|
|
1448
1472
|
standard,
|
|
1449
|
-
concrete
|
|
1473
|
+
concrete,
|
|
1474
|
+
metrics: metrics || undefined
|
|
1450
1475
|
};
|
|
1451
1476
|
}
|
|
1452
1477
|
|
|
1478
|
+
async getTaskMetrics(taskId: string, options?: MetricOptions): Promise<TaskMetrics | null> {
|
|
1479
|
+
const task = await this.store.getTask(taskId);
|
|
1480
|
+
if (!task) return null;
|
|
1481
|
+
|
|
1482
|
+
const [activities, timeEntries, project, workflow] = await Promise.all([
|
|
1483
|
+
this.store.getActivities({ taskId }),
|
|
1484
|
+
this.store.getTimeEntries(taskId),
|
|
1485
|
+
this.store.getProject(task.projectId),
|
|
1486
|
+
this.resolveProjectWorkflow(task.projectId)
|
|
1487
|
+
]);
|
|
1488
|
+
|
|
1489
|
+
const customStatusDefinitions = project?.statusDefinitions || workflow?.statuses;
|
|
1490
|
+
|
|
1491
|
+
return calculateTaskMetrics(task, activities, timeEntries, {
|
|
1492
|
+
customStatusDefinitions,
|
|
1493
|
+
...options
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async getTaskProgressHistory(taskId: string, options?: MetricOptions): Promise<TaskProgressHistory | null> {
|
|
1498
|
+
const task = await this.store.getTask(taskId);
|
|
1499
|
+
if (!task) return null;
|
|
1500
|
+
|
|
1501
|
+
const [activities, project, workflow] = await Promise.all([
|
|
1502
|
+
this.store.getActivities({ taskId }),
|
|
1503
|
+
this.store.getProject(task.projectId),
|
|
1504
|
+
this.resolveProjectWorkflow(task.projectId)
|
|
1505
|
+
]);
|
|
1506
|
+
|
|
1507
|
+
const customStatusDefinitions = project?.statusDefinitions || workflow?.statuses;
|
|
1508
|
+
|
|
1509
|
+
return reconstructTaskProgressHistory(task, activities, {
|
|
1510
|
+
customStatusDefinitions,
|
|
1511
|
+
...options
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async getWorkloadDistribution(
|
|
1516
|
+
projectId?: string,
|
|
1517
|
+
options: WorkloadDistributionOptions = {}
|
|
1518
|
+
): Promise<WorkloadDistribution> {
|
|
1519
|
+
const tasks = await this.store.getTasks(projectId);
|
|
1520
|
+
const teams = await this.store.getTeams();
|
|
1521
|
+
|
|
1522
|
+
const timeEntriesNested = await Promise.all(tasks.map((t) => this.store.getTimeEntries(t.id)));
|
|
1523
|
+
const timeEntries: TimeEntry[] = timeEntriesNested.flat();
|
|
1524
|
+
|
|
1525
|
+
return calculateWorkloadDistribution(
|
|
1526
|
+
{
|
|
1527
|
+
tasks,
|
|
1528
|
+
timeEntries,
|
|
1529
|
+
teams,
|
|
1530
|
+
projectId
|
|
1531
|
+
},
|
|
1532
|
+
options
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1453
1536
|
private async dispatchWebhook(event: WebhookEvent, payload: Record<string, unknown>): Promise<void> {
|
|
1454
1537
|
const webhooks = await this.store.getWebhooks();
|
|
1455
1538
|
const active = webhooks.filter((w) => w.active && w.events.includes(event));
|
package/src/types/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface User {
|
|
|
39
39
|
email: string;
|
|
40
40
|
avatarUrl?: string;
|
|
41
41
|
role: Role;
|
|
42
|
+
weeklyCapacityHours?: number;
|
|
42
43
|
createdAt: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -48,6 +49,7 @@ export interface Team {
|
|
|
48
49
|
description?: string;
|
|
49
50
|
leaderId?: string;
|
|
50
51
|
memberIds: string[];
|
|
52
|
+
weeklyCapacityHours?: number;
|
|
51
53
|
createdAt: string;
|
|
52
54
|
updatedAt: string;
|
|
53
55
|
}
|
|
@@ -426,7 +428,7 @@ export interface CriticalPathConfig {
|
|
|
426
428
|
}
|
|
427
429
|
|
|
428
430
|
// ==========================================
|
|
429
|
-
//
|
|
431
|
+
// Ladder of Abstraction Types
|
|
430
432
|
// ==========================================
|
|
431
433
|
|
|
432
434
|
export type AbstractionLevel = 'macro' | 'standard' | 'concrete' | 'all';
|
|
@@ -518,11 +520,78 @@ export interface RealityDelta {
|
|
|
518
520
|
estimatedHours: number;
|
|
519
521
|
loggedHours: number;
|
|
520
522
|
varianceHours: number; // loggedHours - estimatedHours
|
|
523
|
+
effortVarianceHours?: number; // loggedHours - estimatedHours (effort variance)
|
|
524
|
+
durationVarianceHours?: number; // actual duration hours - planned duration hours
|
|
521
525
|
scheduleVarianceDays?: number; // days difference between planned/due vs actual
|
|
526
|
+
accuracyRatio?: number; // loggedHours / estimatedHours (estimation accuracy ratio)
|
|
522
527
|
isOverdue: boolean;
|
|
523
528
|
isOverEstimate: boolean;
|
|
524
529
|
}
|
|
525
530
|
|
|
531
|
+
export type ProgressInferenceSource = 'explicit' | 'todos' | 'time_effort' | 'schedule_elapsed' | 'blended';
|
|
532
|
+
|
|
533
|
+
export interface TaskProgressInference {
|
|
534
|
+
progressPercentage: number;
|
|
535
|
+
source: ProgressInferenceSource;
|
|
536
|
+
isExplicit: boolean;
|
|
537
|
+
breakdown: {
|
|
538
|
+
explicitProgress?: number;
|
|
539
|
+
todoProgress?: number;
|
|
540
|
+
effortProgress?: number;
|
|
541
|
+
scheduleProgress?: number;
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export type ProgressCurveProfile =
|
|
546
|
+
| 'linear'
|
|
547
|
+
| 's_curve'
|
|
548
|
+
| 'early_surge'
|
|
549
|
+
| 'late_rush'
|
|
550
|
+
| 'stalled'
|
|
551
|
+
| 'insufficient_data';
|
|
552
|
+
|
|
553
|
+
export interface TaskProgressHistoryPoint {
|
|
554
|
+
timestamp: string;
|
|
555
|
+
progress: number;
|
|
556
|
+
inferredProgress?: number;
|
|
557
|
+
status: TaskStatus;
|
|
558
|
+
semanticStatus?: SemanticStatus;
|
|
559
|
+
action: string;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export interface TaskProgressHistory {
|
|
563
|
+
taskId: string;
|
|
564
|
+
points: TaskProgressHistoryPoint[];
|
|
565
|
+
curveProfile: ProgressCurveProfile;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export interface TaskEVM {
|
|
569
|
+
plannedValue: number; // PV (expected hours)
|
|
570
|
+
earnedValue: number; // EV (earned hours)
|
|
571
|
+
actualCost: number; // AC (logged hours)
|
|
572
|
+
costVariance: number; // CV = EV - AC
|
|
573
|
+
scheduleVariance: number; // SV = EV - PV
|
|
574
|
+
costPerformanceIndex: number; // CPI = EV / AC
|
|
575
|
+
schedulePerformanceIndex: number; // SPI = EV / PV
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export interface TaskInferredActuals {
|
|
579
|
+
actualStartDate?: string;
|
|
580
|
+
actualEndDate?: string;
|
|
581
|
+
isStartDateInferred: boolean;
|
|
582
|
+
isEndDateInferred: boolean;
|
|
583
|
+
activeWorkingHours?: number;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export interface TaskMetrics {
|
|
587
|
+
taskId: string;
|
|
588
|
+
inferredActuals: TaskInferredActuals;
|
|
589
|
+
realityDelta: RealityDelta;
|
|
590
|
+
progress: TaskProgressInference;
|
|
591
|
+
evm: TaskEVM;
|
|
592
|
+
progressHistory: TaskProgressHistory;
|
|
593
|
+
}
|
|
594
|
+
|
|
526
595
|
export interface ConcreteTaskEvidence {
|
|
527
596
|
taskId: string;
|
|
528
597
|
attachments: Attachment[];
|
|
@@ -560,5 +629,45 @@ export interface TaskLadderView {
|
|
|
560
629
|
macroPhase?: MacroPhaseRollup;
|
|
561
630
|
standard: StandardTaskTimelineItem;
|
|
562
631
|
concrete: ConcreteTaskEvidence;
|
|
632
|
+
metrics?: TaskMetrics;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export type WorkloadInterval = 'day' | 'week' | 'month';
|
|
636
|
+
export type WorkloadGroupBy = 'assignee' | 'team' | 'taskType' | 'priority' | 'status';
|
|
637
|
+
export type WorkloadMetric = 'scheduled' | 'logged' | 'remaining' | 'blended';
|
|
638
|
+
|
|
639
|
+
export interface WorkloadBucket {
|
|
640
|
+
date: string; // ISO date string e.g. "2026-09-14" (start of bucket)
|
|
641
|
+
timestamp: number; // Unix timestamp in ms
|
|
642
|
+
totalHours: number;
|
|
643
|
+
values: Record<string, number>; // Dimension key to hours: { [dimensionKey]: hours }
|
|
644
|
+
capacity?: Record<string, number>; // Dimension key to available capacity hours: { [dimensionKey]: capacityHours }
|
|
645
|
+
totalCapacity?: number;
|
|
646
|
+
utilizationRatio?: number; // totalHours / totalCapacity
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface WorkloadDistribution {
|
|
650
|
+
projectId?: string;
|
|
651
|
+
startDate: string;
|
|
652
|
+
endDate: string;
|
|
653
|
+
interval: WorkloadInterval;
|
|
654
|
+
groupBy: WorkloadGroupBy;
|
|
655
|
+
metric: WorkloadMetric;
|
|
656
|
+
seriesKeys: string[]; // Unique series keys sorted across all buckets
|
|
657
|
+
seriesLabels: Record<string, string>; // Human-readable labels e.g. { [key]: "Alice" }
|
|
658
|
+
buckets: WorkloadBucket[];
|
|
659
|
+
totalHours: number;
|
|
660
|
+
totalCapacity?: number;
|
|
661
|
+
averageUtilization?: number;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export interface WorkloadDistributionOptions {
|
|
665
|
+
startDate?: string;
|
|
666
|
+
endDate?: string;
|
|
667
|
+
interval?: WorkloadInterval; // default: 'week'
|
|
668
|
+
groupBy?: WorkloadGroupBy; // default: 'assignee'
|
|
669
|
+
metric?: WorkloadMetric; // default: 'blended'
|
|
670
|
+
defaultWeeklyCapacityHours?: number; // default: 40
|
|
671
|
+
capacityOverrides?: Record<string, number>; // e.g. { [assigneeOrTeamId]: weeklyCapacityHours }
|
|
563
672
|
}
|
|
564
673
|
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|