@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +50 -1
  3. package/dist/domain/entities.d.ts.map +1 -1
  4. package/dist/domain/entities.js +5 -0
  5. package/dist/domain/entities.js.map +1 -1
  6. package/dist/domain/index.d.ts +2 -0
  7. package/dist/domain/index.d.ts.map +1 -1
  8. package/dist/domain/index.js +2 -0
  9. package/dist/domain/index.js.map +1 -1
  10. package/dist/domain/ladder.d.ts.map +1 -1
  11. package/dist/domain/ladder.js +14 -0
  12. package/dist/domain/ladder.js.map +1 -1
  13. package/dist/domain/ladder.test.js +3 -3
  14. package/dist/domain/ladder.test.js.map +1 -1
  15. package/dist/domain/metrics.d.ts +29 -0
  16. package/dist/domain/metrics.d.ts.map +1 -0
  17. package/dist/domain/metrics.js +403 -0
  18. package/dist/domain/metrics.js.map +1 -0
  19. package/dist/domain/metrics.test.d.ts +2 -0
  20. package/dist/domain/metrics.test.d.ts.map +1 -0
  21. package/dist/domain/metrics.test.js +251 -0
  22. package/dist/domain/metrics.test.js.map +1 -0
  23. package/dist/domain/workload.d.ts +10 -0
  24. package/dist/domain/workload.d.ts.map +1 -0
  25. package/dist/domain/workload.js +349 -0
  26. package/dist/domain/workload.js.map +1 -0
  27. package/dist/domain/workload.test.d.ts +2 -0
  28. package/dist/domain/workload.test.d.ts.map +1 -0
  29. package/dist/domain/workload.test.js +201 -0
  30. package/dist/domain/workload.test.js.map +1 -0
  31. package/dist/engine/index.d.ts +5 -1
  32. package/dist/engine/index.d.ts.map +1 -1
  33. package/dist/engine/index.js +55 -2
  34. package/dist/engine/index.js.map +1 -1
  35. package/dist/types/index.d.ts +91 -0
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/domain/entities.ts +6 -0
  39. package/src/domain/index.ts +2 -0
  40. package/src/domain/ladder.test.ts +3 -3
  41. package/src/domain/ladder.ts +16 -0
  42. package/src/domain/metrics.test.ts +286 -0
  43. package/src/domain/metrics.ts +484 -0
  44. package/src/domain/workload.test.ts +238 -0
  45. package/src/domain/workload.ts +403 -0
  46. package/src/engine/index.ts +86 -3
  47. package/src/types/index.ts +110 -1
  48. package/tsconfig.tsbuildinfo +1 -1
@@ -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
- // --- Bret Victor's Ladder of Abstraction & Critical Path Method ---
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));
@@ -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
- // Bret Victor's Ladder of Abstraction Types
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
 
@@ -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"}