@critical-path/core 0.18.0 → 0.19.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 (68) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +43 -0
  3. package/dist/domain/calendar.d.ts +53 -0
  4. package/dist/domain/calendar.d.ts.map +1 -0
  5. package/dist/domain/calendar.js +333 -0
  6. package/dist/domain/calendar.js.map +1 -0
  7. package/dist/domain/calendar.test.d.ts +2 -0
  8. package/dist/domain/calendar.test.d.ts.map +1 -0
  9. package/dist/domain/calendar.test.js +136 -0
  10. package/dist/domain/calendar.test.js.map +1 -0
  11. package/dist/domain/cpm.d.ts +8 -3
  12. package/dist/domain/cpm.d.ts.map +1 -1
  13. package/dist/domain/cpm.js +36 -3
  14. package/dist/domain/cpm.js.map +1 -1
  15. package/dist/domain/cpm.test.js +53 -0
  16. package/dist/domain/cpm.test.js.map +1 -1
  17. package/dist/domain/events.d.ts +10 -2
  18. package/dist/domain/events.d.ts.map +1 -1
  19. package/dist/domain/events.js.map +1 -1
  20. package/dist/domain/index.d.ts +1 -0
  21. package/dist/domain/index.d.ts.map +1 -1
  22. package/dist/domain/index.js +1 -0
  23. package/dist/domain/index.js.map +1 -1
  24. package/dist/domain/ladder.d.ts +3 -2
  25. package/dist/domain/ladder.d.ts.map +1 -1
  26. package/dist/domain/ladder.js +21 -3
  27. package/dist/domain/ladder.js.map +1 -1
  28. package/dist/domain/metrics.test.js +5 -4
  29. package/dist/domain/metrics.test.js.map +1 -1
  30. package/dist/domain/workload.d.ts.map +1 -1
  31. package/dist/domain/workload.js +60 -17
  32. package/dist/domain/workload.js.map +1 -1
  33. package/dist/domain/workload.test.js +60 -0
  34. package/dist/domain/workload.test.js.map +1 -1
  35. package/dist/engine/index.d.ts +3 -1
  36. package/dist/engine/index.d.ts.map +1 -1
  37. package/dist/engine/index.js +71 -4
  38. package/dist/engine/index.js.map +1 -1
  39. package/dist/store/sqlite.d.ts.map +1 -1
  40. package/dist/store/sqlite.js +9 -3
  41. package/dist/store/sqlite.js.map +1 -1
  42. package/dist/store/sqlite.test.js +15 -1
  43. package/dist/store/sqlite.test.js.map +1 -1
  44. package/dist/task-state.test.js +49 -0
  45. package/dist/task-state.test.js.map +1 -1
  46. package/dist/types/index.d.ts +44 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/status.d.ts.map +1 -1
  49. package/dist/utils/status.js +5 -2
  50. package/dist/utils/status.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/domain/calendar.test.ts +164 -0
  53. package/src/domain/calendar.ts +457 -0
  54. package/src/domain/cpm.test.ts +61 -0
  55. package/src/domain/cpm.ts +47 -4
  56. package/src/domain/events.ts +11 -1
  57. package/src/domain/index.ts +1 -0
  58. package/src/domain/ladder.ts +25 -4
  59. package/src/domain/metrics.test.ts +6 -4
  60. package/src/domain/workload.test.ts +73 -0
  61. package/src/domain/workload.ts +73 -19
  62. package/src/engine/index.ts +79 -5
  63. package/src/store/sqlite.test.ts +17 -1
  64. package/src/store/sqlite.ts +11 -1
  65. package/src/task-state.test.ts +60 -1
  66. package/src/types/index.ts +48 -0
  67. package/src/utils/status.ts +7 -2
  68. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @critical-path/core
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 31a6dde: Promote task blocking state (`isBlocked: boolean` and `blockedReason: string | null`) to first-class fields on `Task`, `CreateTaskInput`, and `UpdateTaskInput`.
8
+
9
+ - **Domain Events**: Introduced `TaskBlockedEvent` (`task.blocked`) and updated `TaskUnblockedEvent` (`task.unblocked`) to support explicit unblocking.
10
+ - **Lifecycle Derivation**: Updated `deriveTaskStatus` and `deriveTaskLifecycleState` to reflect `task.isBlocked` directly alongside upstream dependency checks.
11
+ - **Engines & Storage**: Added `isBlocked` and `blockedReason` persistence to `SQLiteStore`, `FirebaseStore`, and `InMemoryStore`.
12
+ - **MCP Server**: Added `isBlocked` and `blockedReason` parameter options to `create_task` and `update_task` tool schemas.
13
+ - 31a6dde: Add comprehensive Work Schedule, Working Hours, Working Days & Holidays subsystem:
14
+ - Added `WorkSchedule`, `DaySchedule`, `WorkingHoursRange`, and `Holiday` types supporting custom shifts, weekend configurations, and organization/regional holidays.
15
+ - Integrated hierarchical calendar resolution: Task Assignee/Team -> Project -> Engine/Default (`DEFAULT_WORK_SCHEDULE`).
16
+ - Enhanced Critical Path Method (CPM) with calendar working-hours math (`earlyStartDate`, `earlyFinishDate`, `lateStartDate`, `lateFinishDate`, `totalWorkingHours`, `projectEndDate`) skipping weekends and holidays.
17
+ - Enhanced Workload & Capacity Engine with working-day effort distribution and holiday capacity deductions.
18
+ - Added working-days schedule variance (`scheduleVarianceWorkingDays`) to Reality Deltas in the Ladder of Abstraction.
19
+
3
20
  ## 0.18.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -36,6 +36,12 @@
36
36
  - Multi-scale timeline synthesis: Macro bird's-eye phase rollups (`getTimelineLadder({ level: 'macro' })`), Standard Gantt view with topological CPM forward/backward passes and total float/slack, and Concrete grounding (attachments, deliverables, checklist items, daily effort histograms, and reality deltas).
37
37
  - Single-task contextual drilldown via `getTaskLadder(taskId)`.
38
38
  - Comprehensive CPM analysis via `calculateCriticalPath(projectId)` identifying project bottleneck tasks and critical path duration.
39
+ - **Work Schedules, Working Hours, Working Days & Holidays Subsystem**:
40
+ - Configurable `WorkSchedule` data model defining day-by-day active working hours (e.g. 09:00–17:00), non-working days/weekends, and organization or regional `Holiday` exemptions.
41
+ - Multi-level schedule inheritance hierarchy: Task Assignee / Team -> Project -> Global Engine Default (`DEFAULT_WORK_SCHEDULE`).
42
+ - Calendar math domain operations (`addWorkingHours`, `subtractWorkingHours`, `getWorkingHoursBetween`, `getWorkingDaysBetween`, `getNetAvailableCapacity`).
43
+ - Automatic exclusion of weekends and holidays during Critical Path Method (CPM) forward and backward schedule passes (`earlyStartDate`, `earlyFinishDate`, `lateStartDate`, `lateFinishDate`, `totalWorkingHours`, `projectEndDate`).
44
+ - Calendar-aware capacity reductions in Workload Distribution and working-day schedule variance metrics (`scheduleVarianceWorkingDays`).
39
45
  - **Headless Workload & Capacity Distribution (Streamgraphs & Capacity Planning)**:
40
46
  - Time-series aggregations across customizable intervals (`day`, `week`, `month`) and dimensions (`assignee`, `team`, `taskType`, `priority`, `status`).
41
47
  - Pluggable effort distribution metrics (`scheduled`, `logged`, `remaining`, `blended`) with contiguous, gap-free calendar buckets and zero-filled tabular series matrices ready for D3 (`d3.stack().offset(d3.stackOffsetWiggle)`).
@@ -202,6 +208,43 @@ console.log('Contiguous Weekly Buckets:', workload.buckets);
202
208
  // }
203
209
  ```
204
210
 
211
+ ### 8. Configuring Work Schedules, Working Hours & Holidays
212
+
213
+ ```ts
214
+ import { CriticalPathEngine, type WorkSchedule, addWorkingHours } from '@critical-path/core';
215
+
216
+ // Define a project schedule with 4-day workweeks and holidays
217
+ const engineeringSchedule: WorkSchedule = {
218
+ timezone: 'UTC',
219
+ days: {
220
+ monday: { isWorking: true, hours: [{ start: '09:00', end: '17:00' }] },
221
+ tuesday: { isWorking: true, hours: [{ start: '09:00', end: '17:00' }] },
222
+ wednesday: { isWorking: true, hours: [{ start: '09:00', end: '17:00' }] },
223
+ thursday: { isWorking: true, hours: [{ start: '09:00', end: '17:00' }] },
224
+ friday: { isWorking: false },
225
+ saturday: { isWorking: false },
226
+ sunday: { isWorking: false }
227
+ },
228
+ holidays: [
229
+ { date: '2026-12-25', name: 'Christmas Day' },
230
+ { date: '2026-12-26', name: 'Boxing Day' }
231
+ ]
232
+ };
233
+
234
+ const engine = new CriticalPathEngine({
235
+ defaultSchedule: engineeringSchedule
236
+ });
237
+
238
+ // CPM automatically rolls over non-working days and holidays
239
+ const cpm = await engine.calculateCriticalPath('proj_123', {
240
+ projectStartDate: '2026-09-01T09:00:00Z',
241
+ schedule: engineeringSchedule
242
+ });
243
+
244
+ console.log('Project End Date (skipping weekends & holidays):', cpm.projectEndDate);
245
+ console.log('Total Working Hours on Critical Path:', cpm.totalWorkingHours);
246
+ ```
247
+
205
248
  ---
206
249
 
207
250
  ## 🔌 Creating a Custom Plugin
@@ -0,0 +1,53 @@
1
+ import type { Project, Team, User, WorkSchedule } from '../types/index.js';
2
+ export declare const DEFAULT_WORK_SCHEDULE: WorkSchedule;
3
+ export interface ScheduleResolutionContext {
4
+ user?: User | null;
5
+ team?: Team | null;
6
+ project?: Project | null;
7
+ defaultSchedule?: WorkSchedule | null;
8
+ }
9
+ /**
10
+ * Resolves the effective work schedule following the hierarchy:
11
+ * 1. User schedule (if present)
12
+ * 2. Team schedule (if present)
13
+ * 3. Project schedule (if present)
14
+ * 4. Engine / default schedule (fallback to DEFAULT_WORK_SCHEDULE)
15
+ */
16
+ export declare function resolveEffectiveSchedule(context?: ScheduleResolutionContext): WorkSchedule;
17
+ export declare function toDateString(d: Date): string;
18
+ export declare function parseDate(input: Date | string): Date;
19
+ /**
20
+ * Checks whether a given calendar date is an active working day under the provided schedule.
21
+ */
22
+ export declare function isWorkingDay(input: Date | string, schedule?: WorkSchedule): boolean;
23
+ /**
24
+ * Returns the effective working hours available on a specific calendar day.
25
+ */
26
+ export declare function getWorkingHoursInDay(input: Date | string, schedule?: WorkSchedule): number;
27
+ /**
28
+ * Advances a start date forward by a given number of working hours,
29
+ * skipping non-working days, holidays, and non-working hours.
30
+ */
31
+ export declare function addWorkingHours(startInput: Date | string, hoursToAdd: number, schedule?: WorkSchedule): Date;
32
+ /**
33
+ * Moves backward from an end date by a given number of working hours,
34
+ * skipping non-working days, holidays, and non-working hours.
35
+ */
36
+ export declare function subtractWorkingHours(endInput: Date | string, hoursToSubtract: number, schedule?: WorkSchedule): Date;
37
+ /**
38
+ * Calculates the exact number of active working hours between two dates under the schedule.
39
+ */
40
+ export declare function getWorkingHoursBetween(startInput: Date | string, endInput: Date | string, schedule?: WorkSchedule): number;
41
+ /**
42
+ * Returns the count of working days between two dates (inclusive of start and end).
43
+ */
44
+ export declare function getWorkingDaysBetween(startInput: Date | string, endInput: Date | string, schedule?: WorkSchedule): number;
45
+ /**
46
+ * Returns an array of YYYY-MM-DD date strings for all active working days between start and end.
47
+ */
48
+ export declare function getWorkingDaysList(startInput: Date | string, endInput: Date | string, schedule?: WorkSchedule): string[];
49
+ /**
50
+ * Computes net available capacity (in working hours) over a contiguous date interval.
51
+ */
52
+ export declare function getNetAvailableCapacity(startInput: Date | string, endInput: Date | string, schedule?: WorkSchedule): number;
53
+ //# sourceMappingURL=calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../src/domain/calendar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,OAAO,EACP,IAAI,EACJ,IAAI,EAEJ,YAAY,EACb,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,qBAAqB,EAAE,YAanC,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,YAAY,CAM1F;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAE5C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAUpD;AAkBD;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,QAAQ,GAAE,YAAoC,GAAG,OAAO,CAa1G;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,QAAQ,GAAE,YAAoC,GAAG,MAAM,CA4BjH;AA4CD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,IAAI,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,YAAoC,GAC7C,IAAI,CAqEN;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,IAAI,GAAG,MAAM,EACvB,eAAe,EAAE,MAAM,EACvB,QAAQ,GAAE,YAAoC,GAC7C,IAAI,CAkEN;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,IAAI,GAAG,MAAM,EACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,EACvB,QAAQ,GAAE,YAAoC,GAC7C,MAAM,CAkCR;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,IAAI,GAAG,MAAM,EACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,EACvB,QAAQ,GAAE,YAAoC,GAC7C,MAAM,CAuBR;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,IAAI,GAAG,MAAM,EACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,EACvB,QAAQ,GAAE,YAAoC,GAC7C,MAAM,EAAE,CAsBV;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,IAAI,GAAG,MAAM,EACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,EACvB,QAAQ,GAAE,YAAoC,GAC7C,MAAM,CAgBR"}
@@ -0,0 +1,333 @@
1
+ export const DEFAULT_WORK_SCHEDULE = {
2
+ name: 'Standard 40-Hour Work Week',
3
+ defaultHoursPerDay: 8,
4
+ days: [
5
+ { dayOfWeek: 0, isWorkingDay: false }, // Sunday
6
+ { dayOfWeek: 1, isWorkingDay: true, hours: [{ start: '09:00', end: '17:00' }] }, // Monday
7
+ { dayOfWeek: 2, isWorkingDay: true, hours: [{ start: '09:00', end: '17:00' }] }, // Tuesday
8
+ { dayOfWeek: 3, isWorkingDay: true, hours: [{ start: '09:00', end: '17:00' }] }, // Wednesday
9
+ { dayOfWeek: 4, isWorkingDay: true, hours: [{ start: '09:00', end: '17:00' }] }, // Thursday
10
+ { dayOfWeek: 5, isWorkingDay: true, hours: [{ start: '09:00', end: '17:00' }] }, // Friday
11
+ { dayOfWeek: 6, isWorkingDay: false } // Saturday
12
+ ],
13
+ holidays: []
14
+ };
15
+ /**
16
+ * Resolves the effective work schedule following the hierarchy:
17
+ * 1. User schedule (if present)
18
+ * 2. Team schedule (if present)
19
+ * 3. Project schedule (if present)
20
+ * 4. Engine / default schedule (fallback to DEFAULT_WORK_SCHEDULE)
21
+ */
22
+ export function resolveEffectiveSchedule(context) {
23
+ if (context?.user?.schedule)
24
+ return context.user.schedule;
25
+ if (context?.team?.schedule)
26
+ return context.team.schedule;
27
+ if (context?.project?.schedule)
28
+ return context.project.schedule;
29
+ if (context?.defaultSchedule)
30
+ return context.defaultSchedule;
31
+ return DEFAULT_WORK_SCHEDULE;
32
+ }
33
+ export function toDateString(d) {
34
+ return d.toISOString().split('T')[0];
35
+ }
36
+ export function parseDate(input) {
37
+ if (input instanceof Date) {
38
+ return new Date(input.getTime());
39
+ }
40
+ if (!input.includes('T')) {
41
+ // Treat date-only string as UTC midnight
42
+ const parts = input.split('-');
43
+ return new Date(Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)));
44
+ }
45
+ return new Date(input);
46
+ }
47
+ function parseTimeToMinutes(timeStr) {
48
+ const parts = timeStr.split(':');
49
+ const h = parseInt(parts[0], 10) || 0;
50
+ const m = parseInt(parts[1], 10) || 0;
51
+ return h * 60 + m;
52
+ }
53
+ function getDayConfig(dayOfWeek, schedule) {
54
+ return schedule.days.find((d) => d.dayOfWeek === dayOfWeek);
55
+ }
56
+ function getHoliday(dateStr, schedule) {
57
+ if (!schedule.holidays)
58
+ return undefined;
59
+ return schedule.holidays.find((h) => h.date === dateStr);
60
+ }
61
+ /**
62
+ * Checks whether a given calendar date is an active working day under the provided schedule.
63
+ */
64
+ export function isWorkingDay(input, schedule = DEFAULT_WORK_SCHEDULE) {
65
+ const d = parseDate(input);
66
+ const dateStr = toDateString(d);
67
+ const holiday = getHoliday(dateStr, schedule);
68
+ if (holiday && !holiday.halfDay) {
69
+ return false;
70
+ }
71
+ const dayOfWeek = d.getUTCDay();
72
+ const dayConfig = getDayConfig(dayOfWeek, schedule);
73
+ if (!dayConfig || !dayConfig.isWorkingDay) {
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ /**
79
+ * Returns the effective working hours available on a specific calendar day.
80
+ */
81
+ export function getWorkingHoursInDay(input, schedule = DEFAULT_WORK_SCHEDULE) {
82
+ const d = parseDate(input);
83
+ const dateStr = toDateString(d);
84
+ const holiday = getHoliday(dateStr, schedule);
85
+ if (holiday && !holiday.halfDay) {
86
+ return 0;
87
+ }
88
+ const dayOfWeek = d.getUTCDay();
89
+ const dayConfig = getDayConfig(dayOfWeek, schedule);
90
+ if (!dayConfig || !dayConfig.isWorkingDay) {
91
+ return 0;
92
+ }
93
+ let baseHours = schedule.defaultHoursPerDay ?? 8;
94
+ if (dayConfig.hours && dayConfig.hours.length > 0) {
95
+ baseHours = dayConfig.hours.reduce((sum, range) => {
96
+ const startMin = parseTimeToMinutes(range.start);
97
+ const endMin = parseTimeToMinutes(range.end);
98
+ return sum + Math.max(0, (endMin - startMin) / 60);
99
+ }, 0);
100
+ }
101
+ if (holiday && holiday.halfDay) {
102
+ return Math.round((baseHours / 2) * 100) / 100;
103
+ }
104
+ return baseHours;
105
+ }
106
+ /**
107
+ * Returns the normalized working time windows (in minutes from UTC midnight) for a given date.
108
+ */
109
+ function getDayWorkingWindows(d, schedule) {
110
+ const dateStr = toDateString(d);
111
+ const holiday = getHoliday(dateStr, schedule);
112
+ if (holiday && !holiday.halfDay) {
113
+ return [];
114
+ }
115
+ const dayOfWeek = d.getUTCDay();
116
+ const dayConfig = getDayConfig(dayOfWeek, schedule);
117
+ if (!dayConfig || !dayConfig.isWorkingDay) {
118
+ return [];
119
+ }
120
+ let ranges = dayConfig.hours && dayConfig.hours.length > 0
121
+ ? dayConfig.hours.map((r) => ({ start: parseTimeToMinutes(r.start), end: parseTimeToMinutes(r.end) }))
122
+ : [{ start: 9 * 60, end: (9 + (schedule.defaultHoursPerDay ?? 8)) * 60 }];
123
+ if (holiday && holiday.halfDay && ranges.length > 0) {
124
+ // Take the first half of the working hours
125
+ const totalMinutes = ranges.reduce((acc, r) => acc + (r.end - r.start), 0);
126
+ let targetMinutes = totalMinutes / 2;
127
+ const adjustedRanges = [];
128
+ for (const r of ranges) {
129
+ const dur = r.end - r.start;
130
+ if (targetMinutes <= 0)
131
+ break;
132
+ if (dur <= targetMinutes) {
133
+ adjustedRanges.push(r);
134
+ targetMinutes -= dur;
135
+ }
136
+ else {
137
+ adjustedRanges.push({ start: r.start, end: r.start + targetMinutes });
138
+ targetMinutes = 0;
139
+ }
140
+ }
141
+ return adjustedRanges;
142
+ }
143
+ return ranges;
144
+ }
145
+ /**
146
+ * Advances a start date forward by a given number of working hours,
147
+ * skipping non-working days, holidays, and non-working hours.
148
+ */
149
+ export function addWorkingHours(startInput, hoursToAdd, schedule = DEFAULT_WORK_SCHEDULE) {
150
+ if (hoursToAdd <= 0) {
151
+ return parseDate(startInput);
152
+ }
153
+ let current = parseDate(startInput);
154
+ let remainingMinutes = Math.round(hoursToAdd * 60);
155
+ // Safety guard against infinite loops
156
+ let daysScanned = 0;
157
+ const maxDays = 3650; // 10 years
158
+ while (remainingMinutes > 0 && daysScanned < maxDays) {
159
+ const windows = getDayWorkingWindows(current, schedule);
160
+ if (windows.length === 0) {
161
+ // Non-working day or holiday: move to start of next UTC day
162
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
163
+ daysScanned++;
164
+ continue;
165
+ }
166
+ const currentMinuteOfDay = current.getUTCHours() * 60 + current.getUTCMinutes() + current.getUTCSeconds() / 60;
167
+ for (let i = 0; i < windows.length && remainingMinutes > 0; i++) {
168
+ const w = windows[i];
169
+ if (currentMinuteOfDay >= w.end) {
170
+ // Already past this window
171
+ continue;
172
+ }
173
+ // If current time is before the window starts, jump to window start
174
+ const windowStartMin = Math.max(w.start, currentMinuteOfDay);
175
+ const availableMinutes = w.end - windowStartMin;
176
+ if (remainingMinutes <= availableMinutes) {
177
+ const finalMinuteOfDay = windowStartMin + remainingMinutes;
178
+ const finalHour = Math.floor(finalMinuteOfDay / 60);
179
+ const finalMin = Math.round(finalMinuteOfDay % 60);
180
+ return new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate(), finalHour, finalMin, 0, 0));
181
+ }
182
+ else {
183
+ remainingMinutes -= availableMinutes;
184
+ // Advance current time to end of this window
185
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate(), Math.floor(w.end / 60), Math.round(w.end % 60), 0, 0));
186
+ }
187
+ }
188
+ // Finished processing windows for today; advance to tomorrow at 00:00
189
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
190
+ daysScanned++;
191
+ }
192
+ return current;
193
+ }
194
+ /**
195
+ * Moves backward from an end date by a given number of working hours,
196
+ * skipping non-working days, holidays, and non-working hours.
197
+ */
198
+ export function subtractWorkingHours(endInput, hoursToSubtract, schedule = DEFAULT_WORK_SCHEDULE) {
199
+ if (hoursToSubtract <= 0) {
200
+ return parseDate(endInput);
201
+ }
202
+ let current = parseDate(endInput);
203
+ let remainingMinutes = Math.round(hoursToSubtract * 60);
204
+ let daysScanned = 0;
205
+ const maxDays = 3650;
206
+ while (remainingMinutes > 0 && daysScanned < maxDays) {
207
+ const windows = getDayWorkingWindows(current, schedule);
208
+ if (windows.length === 0) {
209
+ // Non-working day: move to end of previous UTC day (23:59:59)
210
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() - 1, 23, 59, 59, 999));
211
+ daysScanned++;
212
+ continue;
213
+ }
214
+ const currentMinuteOfDay = current.getUTCHours() * 60 + current.getUTCMinutes() + current.getUTCSeconds() / 60;
215
+ // Traverse windows backwards
216
+ for (let i = windows.length - 1; i >= 0 && remainingMinutes > 0; i--) {
217
+ const w = windows[i];
218
+ if (currentMinuteOfDay <= w.start) {
219
+ continue;
220
+ }
221
+ const windowEndMin = Math.min(w.end, currentMinuteOfDay);
222
+ const availableMinutes = windowEndMin - w.start;
223
+ if (remainingMinutes <= availableMinutes) {
224
+ const finalMinuteOfDay = windowEndMin - remainingMinutes;
225
+ const finalHour = Math.floor(finalMinuteOfDay / 60);
226
+ const finalMin = Math.round(finalMinuteOfDay % 60);
227
+ return new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate(), finalHour, finalMin, 0, 0));
228
+ }
229
+ else {
230
+ remainingMinutes -= availableMinutes;
231
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate(), Math.floor(w.start / 60), Math.round(w.start % 60), 0, 0));
232
+ }
233
+ }
234
+ // Move to end of previous day
235
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() - 1, 23, 59, 59, 999));
236
+ daysScanned++;
237
+ }
238
+ return current;
239
+ }
240
+ /**
241
+ * Calculates the exact number of active working hours between two dates under the schedule.
242
+ */
243
+ export function getWorkingHoursBetween(startInput, endInput, schedule = DEFAULT_WORK_SCHEDULE) {
244
+ const start = parseDate(startInput);
245
+ const end = parseDate(endInput);
246
+ if (end <= start) {
247
+ return 0;
248
+ }
249
+ let totalMinutes = 0;
250
+ let current = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0));
251
+ const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), 0, 0, 0, 0));
252
+ let days = 0;
253
+ while (current <= endDay && days < 3650) {
254
+ const windows = getDayWorkingWindows(current, schedule);
255
+ const isFirstDay = current.getTime() === Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0);
256
+ const isLastDay = current.getTime() === endDay.getTime();
257
+ const startMin = isFirstDay ? start.getUTCHours() * 60 + start.getUTCMinutes() : 0;
258
+ const endMin = isLastDay ? end.getUTCHours() * 60 + end.getUTCMinutes() : 24 * 60;
259
+ for (const w of windows) {
260
+ const activeStart = Math.max(w.start, startMin);
261
+ const activeEnd = Math.min(w.end, endMin);
262
+ if (activeEnd > activeStart) {
263
+ totalMinutes += (activeEnd - activeStart);
264
+ }
265
+ }
266
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
267
+ days++;
268
+ }
269
+ return Math.round((totalMinutes / 60) * 100) / 100;
270
+ }
271
+ /**
272
+ * Returns the count of working days between two dates (inclusive of start and end).
273
+ */
274
+ export function getWorkingDaysBetween(startInput, endInput, schedule = DEFAULT_WORK_SCHEDULE) {
275
+ const start = parseDate(startInput);
276
+ const end = parseDate(endInput);
277
+ if (end < start) {
278
+ return 0;
279
+ }
280
+ let current = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0));
281
+ const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), 0, 0, 0, 0));
282
+ let count = 0;
283
+ let days = 0;
284
+ while (current <= endDay && days < 3650) {
285
+ if (isWorkingDay(current, schedule)) {
286
+ const holiday = getHoliday(toDateString(current), schedule);
287
+ count += (holiday && holiday.halfDay) ? 0.5 : 1;
288
+ }
289
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
290
+ days++;
291
+ }
292
+ return count;
293
+ }
294
+ /**
295
+ * Returns an array of YYYY-MM-DD date strings for all active working days between start and end.
296
+ */
297
+ export function getWorkingDaysList(startInput, endInput, schedule = DEFAULT_WORK_SCHEDULE) {
298
+ const start = parseDate(startInput);
299
+ const end = parseDate(endInput);
300
+ if (end < start) {
301
+ return [];
302
+ }
303
+ let current = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0));
304
+ const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), 0, 0, 0, 0));
305
+ const list = [];
306
+ let days = 0;
307
+ while (current <= endDay && days < 3650) {
308
+ if (isWorkingDay(current, schedule)) {
309
+ list.push(toDateString(current));
310
+ }
311
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
312
+ days++;
313
+ }
314
+ return list;
315
+ }
316
+ /**
317
+ * Computes net available capacity (in working hours) over a contiguous date interval.
318
+ */
319
+ export function getNetAvailableCapacity(startInput, endInput, schedule = DEFAULT_WORK_SCHEDULE) {
320
+ const start = parseDate(startInput);
321
+ const end = parseDate(endInput);
322
+ let current = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0));
323
+ const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), 0, 0, 0, 0));
324
+ let totalCapacity = 0;
325
+ let days = 0;
326
+ while (current <= endDay && days < 3650) {
327
+ totalCapacity += getWorkingHoursInDay(current, schedule);
328
+ current = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate() + 1, 0, 0, 0, 0));
329
+ days++;
330
+ }
331
+ return Math.round(totalCapacity * 100) / 100;
332
+ }
333
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/domain/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=calendar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.test.d.ts","sourceRoot":"","sources":["../../src/domain/calendar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { DEFAULT_WORK_SCHEDULE, resolveEffectiveSchedule, isWorkingDay, getWorkingHoursInDay, addWorkingHours, subtractWorkingHours, getWorkingHoursBetween, getWorkingDaysBetween, getWorkingDaysList, getNetAvailableCapacity } from './calendar.js';
3
+ describe('Calendar and Work Schedule Domain', () => {
4
+ describe('resolveEffectiveSchedule', () => {
5
+ const customUserSchedule = {
6
+ name: 'User 30h',
7
+ defaultHoursPerDay: 6,
8
+ days: [{ dayOfWeek: 1, isWorkingDay: true }]
9
+ };
10
+ const customTeamSchedule = {
11
+ name: 'Team 35h',
12
+ defaultHoursPerDay: 7,
13
+ days: [{ dayOfWeek: 1, isWorkingDay: true }]
14
+ };
15
+ const customProjectSchedule = {
16
+ name: 'Project 4-Day',
17
+ defaultHoursPerDay: 10,
18
+ days: [{ dayOfWeek: 1, isWorkingDay: true }]
19
+ };
20
+ it('prioritizes user schedule when present', () => {
21
+ const resolved = resolveEffectiveSchedule({
22
+ user: { id: 'u1', name: 'Alice', email: 'alice@test.com', role: 'contributor', schedule: customUserSchedule, createdAt: '' },
23
+ team: { id: 't1', name: 'Dev', memberIds: [], schedule: customTeamSchedule, createdAt: '', updatedAt: '' },
24
+ project: { id: 'p1', name: 'Proj', schedule: customProjectSchedule, createdAt: '', updatedAt: '' }
25
+ });
26
+ expect(resolved.name).toBe('User 30h');
27
+ });
28
+ it('falls back to team schedule when user has no custom schedule', () => {
29
+ const resolved = resolveEffectiveSchedule({
30
+ user: { id: 'u1', name: 'Alice', email: 'alice@test.com', role: 'contributor', createdAt: '' },
31
+ team: { id: 't1', name: 'Dev', memberIds: [], schedule: customTeamSchedule, createdAt: '', updatedAt: '' },
32
+ project: { id: 'p1', name: 'Proj', schedule: customProjectSchedule, createdAt: '', updatedAt: '' }
33
+ });
34
+ expect(resolved.name).toBe('Team 35h');
35
+ });
36
+ it('falls back to project schedule when user and team have no schedule', () => {
37
+ const resolved = resolveEffectiveSchedule({
38
+ project: { id: 'p1', name: 'Proj', schedule: customProjectSchedule, createdAt: '', updatedAt: '' }
39
+ });
40
+ expect(resolved.name).toBe('Project 4-Day');
41
+ });
42
+ it('falls back to engine default or DEFAULT_WORK_SCHEDULE', () => {
43
+ const resolved = resolveEffectiveSchedule();
44
+ expect(resolved.name).toBe(DEFAULT_WORK_SCHEDULE.name);
45
+ expect(resolved.defaultHoursPerDay).toBe(8);
46
+ });
47
+ });
48
+ describe('isWorkingDay and getWorkingHoursInDay', () => {
49
+ // 2026-09-18 is Friday, 2026-09-19 is Saturday, 2026-09-20 is Sunday, 2026-09-21 is Monday
50
+ it('identifies weekdays as working days and weekends as non-working days by default', () => {
51
+ expect(isWorkingDay('2026-09-18')).toBe(true); // Friday
52
+ expect(isWorkingDay('2026-09-19')).toBe(false); // Saturday
53
+ expect(isWorkingDay('2026-09-20')).toBe(false); // Sunday
54
+ expect(isWorkingDay('2026-09-21')).toBe(true); // Monday
55
+ expect(getWorkingHoursInDay('2026-09-18')).toBe(8);
56
+ expect(getWorkingHoursInDay('2026-09-19')).toBe(0);
57
+ expect(getWorkingHoursInDay('2026-09-20')).toBe(0);
58
+ });
59
+ it('accounts for full and half-day holidays', () => {
60
+ const holidaySchedule = {
61
+ ...DEFAULT_WORK_SCHEDULE,
62
+ holidays: [
63
+ { date: '2026-09-21', name: 'Labor Day' },
64
+ { date: '2026-09-22', name: 'Eve', halfDay: true }
65
+ ]
66
+ };
67
+ expect(isWorkingDay('2026-09-21', holidaySchedule)).toBe(false);
68
+ expect(getWorkingHoursInDay('2026-09-21', holidaySchedule)).toBe(0);
69
+ expect(isWorkingDay('2026-09-22', holidaySchedule)).toBe(true);
70
+ expect(getWorkingHoursInDay('2026-09-22', holidaySchedule)).toBe(4);
71
+ });
72
+ });
73
+ describe('addWorkingHours', () => {
74
+ it('advances hours within the same working day', () => {
75
+ // Monday 2026-09-21 at 09:00 + 4 hours -> 13:00
76
+ const start = '2026-09-21T09:00:00Z';
77
+ const finish = addWorkingHours(start, 4);
78
+ expect(finish.toISOString()).toBe('2026-09-21T13:00:00.000Z');
79
+ });
80
+ it('rolls over weekends seamlessly', () => {
81
+ // Friday 2026-09-18 at 15:00 + 4 hours
82
+ // 15:00-17:00 is 2h on Friday. Sat & Sun skipped.
83
+ // Remaining 2h starts Monday 2026-09-21 at 09:00 -> finishes 11:00!
84
+ const start = '2026-09-18T15:00:00Z';
85
+ const finish = addWorkingHours(start, 4);
86
+ expect(finish.toISOString()).toBe('2026-09-21T11:00:00.000Z');
87
+ });
88
+ it('skips holidays when rolling over', () => {
89
+ const scheduleWithMondayHoliday = {
90
+ ...DEFAULT_WORK_SCHEDULE,
91
+ holidays: [{ date: '2026-09-21', name: 'Bank Holiday' }]
92
+ };
93
+ // Friday 2026-09-18 at 15:00 + 4 hours -> 2h Friday, Sat/Sun skipped, Mon skipped -> finishes Tuesday 11:00!
94
+ const start = '2026-09-18T15:00:00Z';
95
+ const finish = addWorkingHours(start, 4, scheduleWithMondayHoliday);
96
+ expect(finish.toISOString()).toBe('2026-09-22T11:00:00.000Z');
97
+ });
98
+ });
99
+ describe('subtractWorkingHours', () => {
100
+ it('moves backward within the same working day', () => {
101
+ // Monday 2026-09-21 at 15:00 - 4 hours -> 11:00
102
+ const end = '2026-09-21T15:00:00Z';
103
+ const start = subtractWorkingHours(end, 4);
104
+ expect(start.toISOString()).toBe('2026-09-21T11:00:00.000Z');
105
+ });
106
+ it('moves backward across weekends', () => {
107
+ // Monday 2026-09-21 at 11:00 - 4 hours
108
+ // 2h backwards to 09:00 on Monday. Sunday & Saturday skipped.
109
+ // Remaining 2h backwards from Friday 17:00 -> 15:00!
110
+ const end = '2026-09-21T11:00:00Z';
111
+ const start = subtractWorkingHours(end, 4);
112
+ expect(start.toISOString()).toBe('2026-09-18T15:00:00.000Z');
113
+ });
114
+ });
115
+ describe('getWorkingHoursBetween and getWorkingDaysBetween', () => {
116
+ it('calculates working hours between two dates excluding weekends', () => {
117
+ // Friday 09:00 to Monday 17:00 = 8h (Friday) + 0h (weekend) + 8h (Monday) = 16h
118
+ const start = '2026-09-18T09:00:00Z';
119
+ const end = '2026-09-21T17:00:00Z';
120
+ expect(getWorkingHoursBetween(start, end)).toBe(16);
121
+ expect(getWorkingDaysBetween('2026-09-18', '2026-09-21')).toBe(2);
122
+ expect(getWorkingDaysList('2026-09-18', '2026-09-21')).toEqual(['2026-09-18', '2026-09-21']);
123
+ });
124
+ it('calculates net capacity deducting holidays', () => {
125
+ const scheduleWithHoliday = {
126
+ ...DEFAULT_WORK_SCHEDULE,
127
+ holidays: [{ date: '2026-09-21', name: 'Holiday' }]
128
+ };
129
+ // Friday 2026-09-18 to Tuesday 2026-09-22:
130
+ // Friday (8) + Sat (0) + Sun (0) + Mon (0, holiday) + Tue (8) = 16h
131
+ const cap = getNetAvailableCapacity('2026-09-18', '2026-09-22', scheduleWithHoliday);
132
+ expect(cap).toBe(16);
133
+ });
134
+ });
135
+ });
136
+ //# sourceMappingURL=calendar.test.js.map