dabke 0.81.1 → 0.83.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 +58 -0
- package/README.md +45 -27
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -1
- package/dist/client.js.map +1 -1
- package/dist/client.types.d.ts +9 -0
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js +1 -0
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +9 -0
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js +36 -34
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +13 -1
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js +4 -0
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rules/cost-utils.d.ts +11 -0
- package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
- package/dist/cpsat/rules/cost-utils.js +24 -0
- package/dist/cpsat/rules/cost-utils.js.map +1 -0
- package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
- package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
- package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/index.d.ts +3 -0
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +3 -0
- package/dist/cpsat/rules/index.js.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +16 -2
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-days-week.d.ts +44 -0
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-week.js +95 -0
- package/dist/cpsat/rules/max-days-week.js.map +1 -0
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +15 -2
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +16 -2
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +15 -2
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +15 -2
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-days-week.d.ts +34 -0
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-week.js +84 -0
- package/dist/cpsat/rules/min-days-week.js.map +1 -0
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +15 -2
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +16 -2
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
- package/dist/cpsat/rules/minimize-cost.js +2 -23
- package/dist/cpsat/rules/minimize-cost.js.map +1 -1
- package/dist/cpsat/rules/must-assign.d.ts +49 -0
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
- package/dist/cpsat/rules/must-assign.js +86 -0
- package/dist/cpsat/rules/must-assign.js.map +1 -0
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
- package/dist/cpsat/rules/registry.d.ts +28 -2
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +4 -1
- package/dist/cpsat/rules/registry.js.map +1 -1
- package/dist/cpsat/rules/resolver.js +2 -2
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/rules.types.d.ts +3 -0
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +18 -1
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.js +59 -16
- package/dist/cpsat/rules/scope.types.js.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
- package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +6 -3
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +44 -42
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +64 -46
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +37 -27
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/utils.d.ts.map +1 -1
- package/dist/cpsat/utils.js +7 -12
- package/dist/cpsat/utils.js.map +1 -1
- package/dist/cpsat/validation-reporter.d.ts +10 -7
- package/dist/cpsat/validation-reporter.d.ts.map +1 -1
- package/dist/cpsat/validation-reporter.js +44 -72
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/cpsat/validation.types.d.ts +54 -44
- package/dist/cpsat/validation.types.d.ts.map +1 -1
- package/dist/cpsat/validation.types.js +15 -10
- package/dist/cpsat/validation.types.js.map +1 -1
- package/dist/datetime.utils.d.ts +3 -203
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +1 -288
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +14 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -83
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +204 -0
- package/dist/schedule/cost.d.ts.map +1 -0
- package/dist/schedule/cost.js +187 -0
- package/dist/schedule/cost.js.map +1 -0
- package/dist/schedule/coverage.d.ts +85 -0
- package/dist/schedule/coverage.d.ts.map +1 -0
- package/dist/schedule/coverage.js +33 -0
- package/dist/schedule/coverage.js.map +1 -0
- package/dist/schedule/definition.d.ts +227 -0
- package/dist/schedule/definition.d.ts.map +1 -0
- package/dist/schedule/definition.js +659 -0
- package/dist/schedule/definition.js.map +1 -0
- package/dist/schedule/index.d.ts +67 -0
- package/dist/schedule/index.d.ts.map +1 -0
- package/dist/schedule/index.js +69 -0
- package/dist/schedule/index.js.map +1 -0
- package/dist/schedule/rules.d.ts +353 -0
- package/dist/schedule/rules.d.ts.map +1 -0
- package/dist/schedule/rules.js +352 -0
- package/dist/schedule/rules.js.map +1 -0
- package/dist/schedule/shift-patterns.d.ts +34 -0
- package/dist/schedule/shift-patterns.d.ts.map +1 -0
- package/dist/schedule/shift-patterns.js +41 -0
- package/dist/schedule/shift-patterns.js.map +1 -0
- package/dist/schedule/time-periods.d.ts +69 -0
- package/dist/schedule/time-periods.d.ts.map +1 -0
- package/dist/schedule/time-periods.js +91 -0
- package/dist/schedule/time-periods.js.map +1 -0
- package/dist/types.d.ts +14 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -9
- package/solver/src/solver/app.py +1 -1
- package/solver/src/solver/solver.py +7 -4
- package/src/client.ts +6 -8
- package/src/client.types.ts +9 -0
- package/src/cpsat/model-builder.ts +44 -35
- package/src/cpsat/response.ts +13 -1
- package/src/cpsat/rules/cost-utils.ts +25 -0
- package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
- package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
- package/src/cpsat/rules/index.ts +3 -0
- package/src/cpsat/rules/max-consecutive-days.ts +17 -0
- package/src/cpsat/rules/max-days-week.ts +143 -0
- package/src/cpsat/rules/max-hours-day.ts +21 -1
- package/src/cpsat/rules/max-hours-week.ts +22 -1
- package/src/cpsat/rules/max-shifts-day.ts +21 -1
- package/src/cpsat/rules/min-consecutive-days.ts +16 -1
- package/src/cpsat/rules/min-days-week.ts +120 -0
- package/src/cpsat/rules/min-hours-day.ts +16 -1
- package/src/cpsat/rules/min-hours-week.ts +17 -1
- package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
- package/src/cpsat/rules/minimize-cost.ts +2 -29
- package/src/cpsat/rules/must-assign.ts +108 -0
- package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
- package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
- package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
- package/src/cpsat/rules/registry.ts +8 -2
- package/src/cpsat/rules/resolver.ts +2 -2
- package/src/cpsat/rules/rules.types.ts +3 -0
- package/src/cpsat/rules/scope.types.ts +73 -20
- package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
- package/src/cpsat/rules/time-off.ts +6 -2
- package/src/cpsat/semantic-time.ts +115 -91
- package/src/cpsat/types.ts +37 -27
- package/src/cpsat/utils.ts +8 -12
- package/src/cpsat/validation-reporter.ts +51 -82
- package/src/cpsat/validation.types.ts +72 -47
- package/src/datetime.utils.ts +3 -334
- package/src/index.ts +35 -107
- package/src/schedule/cost.ts +242 -0
- package/src/schedule/coverage.ts +135 -0
- package/src/schedule/definition.ts +958 -0
- package/src/schedule/index.ts +112 -0
- package/src/schedule/rules.ts +529 -0
- package/src/schedule/shift-patterns.ts +46 -0
- package/src/schedule/time-periods.ts +110 -0
- package/src/types.ts +14 -88
- package/dist/errors.d.ts +0 -12
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -17
- package/dist/errors.js.map +0 -1
- package/dist/llms.d.ts +0 -2
- package/dist/llms.d.ts.map +0 -1
- package/dist/llms.js +0 -3
- package/dist/llms.js.map +0 -1
- package/dist/schedule.d.ts +0 -724
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -899
- package/dist/schedule.js.map +0 -1
- package/dist/validation.d.ts +0 -105
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -130
- package/dist/validation.js.map +0 -1
- package/llms.txt +0 -925
- package/src/errors.ts +0 -17
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1419
- package/src/validation.ts +0 -188
package/dist/schedule.js
DELETED
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* High-level schedule definition API.
|
|
3
|
-
*
|
|
4
|
-
* Small, composable factory functions that produce a complete scheduling
|
|
5
|
-
* configuration. Designed for LLM code generation: each concept is a single
|
|
6
|
-
* function call with per-call type safety.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```typescript
|
|
10
|
-
* import {
|
|
11
|
-
* defineSchedule, t, time, cover, shift,
|
|
12
|
-
* maxHoursPerDay, maxHoursPerWeek, minRestBetweenShifts,
|
|
13
|
-
* weekdays, weekend,
|
|
14
|
-
* } from "dabke";
|
|
15
|
-
*
|
|
16
|
-
* export default defineSchedule({
|
|
17
|
-
* roles: ["cashier", "floor_lead", "stocker"],
|
|
18
|
-
* skills: ["keyholder"],
|
|
19
|
-
*
|
|
20
|
-
* times: {
|
|
21
|
-
* opening: time({ startTime: t(8), endTime: t(10) }),
|
|
22
|
-
* peak_hours: time(
|
|
23
|
-
* { startTime: t(11), endTime: t(14) },
|
|
24
|
-
* { startTime: t(10), endTime: t(15), dayOfWeek: weekend },
|
|
25
|
-
* ),
|
|
26
|
-
* closing: time({ startTime: t(20), endTime: t(22) }),
|
|
27
|
-
* },
|
|
28
|
-
*
|
|
29
|
-
* coverage: [
|
|
30
|
-
* cover("opening", "keyholder", 1),
|
|
31
|
-
* cover("peak_hours", "cashier", 3, { dayOfWeek: weekdays }),
|
|
32
|
-
* cover("peak_hours", "cashier", 5, { dayOfWeek: weekend }),
|
|
33
|
-
* cover("closing", "floor_lead", 1),
|
|
34
|
-
* ],
|
|
35
|
-
*
|
|
36
|
-
* shiftPatterns: [
|
|
37
|
-
* shift("morning", t(8), t(14)),
|
|
38
|
-
* shift("afternoon", t(14), t(22)),
|
|
39
|
-
* ],
|
|
40
|
-
*
|
|
41
|
-
* rules: [
|
|
42
|
-
* maxHoursPerDay(10),
|
|
43
|
-
* maxHoursPerWeek(48),
|
|
44
|
-
* minRestBetweenShifts(10),
|
|
45
|
-
* ],
|
|
46
|
-
* });
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @module
|
|
50
|
-
*/
|
|
51
|
-
import { defineSemanticTimes } from "./cpsat/semantic-time.js";
|
|
52
|
-
import { resolveDaysFromPeriod } from "./datetime.utils.js";
|
|
53
|
-
// ============================================================================
|
|
54
|
-
// Primitives
|
|
55
|
-
// ============================================================================
|
|
56
|
-
/**
|
|
57
|
-
* Creates a {@link TimeOfDay} value.
|
|
58
|
-
*
|
|
59
|
-
* @param hours - Hour component (0-23)
|
|
60
|
-
* @param minutes - Minute component (0-59)
|
|
61
|
-
*
|
|
62
|
-
* @example Hours only
|
|
63
|
-
* ```ts
|
|
64
|
-
* t(9) // { hours: 9, minutes: 0 }
|
|
65
|
-
* ```
|
|
66
|
-
*
|
|
67
|
-
* @example Hours and minutes
|
|
68
|
-
* ```ts
|
|
69
|
-
* t(17, 30) // { hours: 17, minutes: 30 }
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export function t(hours, minutes = 0) {
|
|
73
|
-
return { hours, minutes };
|
|
74
|
-
}
|
|
75
|
-
/** Monday through Friday. */
|
|
76
|
-
export const weekdays = [
|
|
77
|
-
"monday",
|
|
78
|
-
"tuesday",
|
|
79
|
-
"wednesday",
|
|
80
|
-
"thursday",
|
|
81
|
-
"friday",
|
|
82
|
-
];
|
|
83
|
-
/** Saturday and Sunday. */
|
|
84
|
-
export const weekend = ["saturday", "sunday"];
|
|
85
|
-
// ============================================================================
|
|
86
|
-
// Semantic Times
|
|
87
|
-
// ============================================================================
|
|
88
|
-
/**
|
|
89
|
-
* Defines a named time window.
|
|
90
|
-
*
|
|
91
|
-
* @remarks
|
|
92
|
-
* A semantic time is any recurring period you need to reference:
|
|
93
|
-
* service hours, delivery windows, peak periods, weekly events. Times
|
|
94
|
-
* may overlap (e.g., "dinner" 18:00-22:00 and "happy_hour"
|
|
95
|
-
* 17:30-18:30, or "lunch" 12:00-14:00 with "peak_lunch"
|
|
96
|
-
* 13:00-13:30). Coverage and rules reference these names; each
|
|
97
|
-
* generates independent constraints.
|
|
98
|
-
*
|
|
99
|
-
* Every argument is a {@link SemanticTimeVariant} with `startTime`/`endTime`
|
|
100
|
-
* and optional `dayOfWeek`/`dates` scoping. An entry without scoping is the
|
|
101
|
-
* default (applies when no scoped entry matches). At most one default is
|
|
102
|
-
* allowed. If no default, the time only exists on the scoped days.
|
|
103
|
-
*
|
|
104
|
-
* Resolution precedence: `dates` > `dayOfWeek` > default.
|
|
105
|
-
*
|
|
106
|
-
* @example Every day
|
|
107
|
-
* ```typescript
|
|
108
|
-
* day_shift: time({ startTime: t(7), endTime: t(15) }),
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* @example Default with weekend variant
|
|
112
|
-
* ```typescript
|
|
113
|
-
* peak_hours: time(
|
|
114
|
-
* { startTime: t(9), endTime: t(17) },
|
|
115
|
-
* { startTime: t(10), endTime: t(15), dayOfWeek: weekend },
|
|
116
|
-
* ),
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
|
-
* @example No default (specific days only)
|
|
120
|
-
* ```typescript
|
|
121
|
-
* happy_hour: time(
|
|
122
|
-
* { startTime: t(16), endTime: t(18), dayOfWeek: ["monday", "tuesday"] },
|
|
123
|
-
* { startTime: t(17), endTime: t(19), dayOfWeek: ["friday"] },
|
|
124
|
-
* ),
|
|
125
|
-
* ```
|
|
126
|
-
*/
|
|
127
|
-
export function time(...entries) {
|
|
128
|
-
// Validate: at most one default (no dayOfWeek and no dates)
|
|
129
|
-
const defaults = entries.filter((e) => !e.dayOfWeek && !e.dates);
|
|
130
|
-
if (defaults.length > 1) {
|
|
131
|
-
throw new Error("time() accepts at most one default entry (without dayOfWeek or dates). " +
|
|
132
|
-
`Found ${defaults.length} default entries.`);
|
|
133
|
-
}
|
|
134
|
-
// Single entry without scoping: simple SemanticTimeDef
|
|
135
|
-
if (entries.length === 1 && !entries[0].dayOfWeek && !entries[0].dates) {
|
|
136
|
-
return {
|
|
137
|
-
startTime: entries[0].startTime,
|
|
138
|
-
endTime: entries[0].endTime,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// Multiple entries or scoped entries: pass through directly
|
|
142
|
-
return entries.map((entry) => {
|
|
143
|
-
const variant = {
|
|
144
|
-
startTime: entry.startTime,
|
|
145
|
-
endTime: entry.endTime,
|
|
146
|
-
};
|
|
147
|
-
if (entry.dayOfWeek)
|
|
148
|
-
variant.dayOfWeek = entry.dayOfWeek;
|
|
149
|
-
if (entry.dates)
|
|
150
|
-
variant.dates = entry.dates;
|
|
151
|
-
return variant;
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
export function cover(timeName, target, countOrFirstVariant, ...rest) {
|
|
155
|
-
if (typeof countOrFirstVariant === "number") {
|
|
156
|
-
// Simple form: cover(time, target, count, opts?)
|
|
157
|
-
return {
|
|
158
|
-
_type: "coverage",
|
|
159
|
-
timeName,
|
|
160
|
-
target,
|
|
161
|
-
count: countOrFirstVariant,
|
|
162
|
-
options: rest[0] ?? {},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// Variant form: cover(time, target, ...variants)
|
|
166
|
-
const variants = [countOrFirstVariant, ...rest];
|
|
167
|
-
const defaults = variants.filter((v) => !v.dayOfWeek && !v.dates);
|
|
168
|
-
if (defaults.length > 1) {
|
|
169
|
-
throw new Error("cover() accepts at most one default variant (without dayOfWeek or dates). " +
|
|
170
|
-
`Found ${defaults.length} default variants.`);
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
_type: "coverage",
|
|
174
|
-
timeName,
|
|
175
|
-
target,
|
|
176
|
-
count: 0,
|
|
177
|
-
options: {},
|
|
178
|
-
variants,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
// ============================================================================
|
|
182
|
-
// Shift Patterns
|
|
183
|
-
// ============================================================================
|
|
184
|
-
/**
|
|
185
|
-
* Creates a {@link ShiftPattern} (time slot template).
|
|
186
|
-
*
|
|
187
|
-
* @remarks
|
|
188
|
-
* Shift patterns define when people can work: the concrete time slots
|
|
189
|
-
* the solver may assign members to. Each pattern repeats across all
|
|
190
|
-
* scheduling days unless filtered by `dayOfWeek` or `roles`.
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* ```typescript
|
|
194
|
-
* shift("early", t(6), t(14)),
|
|
195
|
-
* shift("day", t(9), t(17)),
|
|
196
|
-
* shift("night", t(22), t(6), { roles: ["nurse", "doctor"] }),
|
|
197
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export function shift(id, startTime, endTime, opts) {
|
|
200
|
-
const pattern = { id, startTime, endTime };
|
|
201
|
-
if (opts?.roles)
|
|
202
|
-
pattern.roles = opts.roles;
|
|
203
|
-
if (opts?.dayOfWeek)
|
|
204
|
-
pattern.dayOfWeek = opts.dayOfWeek;
|
|
205
|
-
if (opts?.locationId)
|
|
206
|
-
pattern.locationId = opts.locationId;
|
|
207
|
-
return pattern;
|
|
208
|
-
}
|
|
209
|
-
function makeRule(rule, fields) {
|
|
210
|
-
return { _type: "rule", _rule: rule, ...fields };
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Limits how many hours a person can work in a single day.
|
|
214
|
-
*
|
|
215
|
-
* @example Global limit
|
|
216
|
-
* ```ts
|
|
217
|
-
* maxHoursPerDay(10)
|
|
218
|
-
* ```
|
|
219
|
-
*
|
|
220
|
-
* @example Scoped to a role
|
|
221
|
-
* ```ts
|
|
222
|
-
* maxHoursPerDay(6, { appliesTo: "student" })
|
|
223
|
-
* ```
|
|
224
|
-
*/
|
|
225
|
-
export function maxHoursPerDay(hours, opts) {
|
|
226
|
-
return makeRule("max-hours-day", { hours, ...opts });
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Caps total hours a person can work within each scheduling week.
|
|
230
|
-
*
|
|
231
|
-
* @example Global cap
|
|
232
|
-
* ```ts
|
|
233
|
-
* maxHoursPerWeek(48)
|
|
234
|
-
* ```
|
|
235
|
-
*
|
|
236
|
-
* @example Part-time cap for a skill group
|
|
237
|
-
* ```ts
|
|
238
|
-
* maxHoursPerWeek(20, { appliesTo: "part_time" })
|
|
239
|
-
* ```
|
|
240
|
-
*/
|
|
241
|
-
export function maxHoursPerWeek(hours, opts) {
|
|
242
|
-
return makeRule("max-hours-week", { hours, ...opts });
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Ensures a person works at least a minimum number of hours per day when assigned.
|
|
246
|
-
*
|
|
247
|
-
* @example
|
|
248
|
-
* ```ts
|
|
249
|
-
* minHoursPerDay(4)
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
export function minHoursPerDay(hours, opts) {
|
|
253
|
-
return makeRule("min-hours-day", { hours, ...opts });
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Enforces a minimum total number of hours per scheduling week.
|
|
257
|
-
*
|
|
258
|
-
* @example Guaranteed minimum for full-time members
|
|
259
|
-
* ```ts
|
|
260
|
-
* minHoursPerWeek(30, { appliesTo: "full_time" })
|
|
261
|
-
* ```
|
|
262
|
-
*/
|
|
263
|
-
export function minHoursPerWeek(hours, opts) {
|
|
264
|
-
return makeRule("min-hours-week", { hours, ...opts });
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Limits how many shifts a person can work in a single day.
|
|
268
|
-
*
|
|
269
|
-
* @example One shift per day
|
|
270
|
-
* ```ts
|
|
271
|
-
* maxShiftsPerDay(1)
|
|
272
|
-
* ```
|
|
273
|
-
*/
|
|
274
|
-
export function maxShiftsPerDay(shifts, opts) {
|
|
275
|
-
return makeRule("max-shifts-day", { shifts, ...opts });
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Limits how many consecutive days a person can be assigned.
|
|
279
|
-
*
|
|
280
|
-
* @example Five-day work week limit
|
|
281
|
-
* ```ts
|
|
282
|
-
* maxConsecutiveDays(5)
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
export function maxConsecutiveDays(days, opts) {
|
|
286
|
-
return makeRule("max-consecutive-days", { days, ...opts });
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Requires a minimum stretch of consecutive working days once assigned.
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* ```ts
|
|
293
|
-
* minConsecutiveDays(2)
|
|
294
|
-
* ```
|
|
295
|
-
*/
|
|
296
|
-
export function minConsecutiveDays(days, opts) {
|
|
297
|
-
return makeRule("min-consecutive-days", { days, ...opts });
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Enforces a minimum rest period between any two shifts a person works.
|
|
301
|
-
*
|
|
302
|
-
* @example EU Working Time Directive (11 hours)
|
|
303
|
-
* ```ts
|
|
304
|
-
* minRestBetweenShifts(11)
|
|
305
|
-
* ```
|
|
306
|
-
*/
|
|
307
|
-
export function minRestBetweenShifts(hours, opts) {
|
|
308
|
-
return makeRule("min-rest-between-shifts", { hours, ...opts });
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Adds objective weight to prefer or avoid assigning team members.
|
|
312
|
-
*
|
|
313
|
-
* @param level - `"high"` to prefer assigning, `"low"` to avoid
|
|
314
|
-
* @param opts - Entity and time scoping (no priority; preference is the priority mechanism)
|
|
315
|
-
*
|
|
316
|
-
* @example Prefer assigning full-time staff
|
|
317
|
-
* ```ts
|
|
318
|
-
* preference("high", { appliesTo: "full_time" })
|
|
319
|
-
* ```
|
|
320
|
-
*
|
|
321
|
-
* @example Avoid assigning a specific member on weekends
|
|
322
|
-
* ```ts
|
|
323
|
-
* preference("low", { appliesTo: "alice", dayOfWeek: weekend })
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
export function preference(level, opts) {
|
|
327
|
-
return makeRule("assignment-priority", { preference: level, ...opts });
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Prefers assigning a person to shift patterns at a specific location.
|
|
331
|
-
*
|
|
332
|
-
* @example
|
|
333
|
-
* ```ts
|
|
334
|
-
* preferLocation("north_wing", { appliesTo: "alice" })
|
|
335
|
-
* ```
|
|
336
|
-
*/
|
|
337
|
-
export function preferLocation(locationId, opts) {
|
|
338
|
-
return makeRule("location-preference", { locationId, ...opts });
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Tells the solver to minimize total labor cost.
|
|
342
|
-
*
|
|
343
|
-
* @remarks
|
|
344
|
-
* Without this rule, cost modifiers only affect post-solve calculation.
|
|
345
|
-
* When present, the solver actively prefers cheaper assignments.
|
|
346
|
-
*
|
|
347
|
-
* For hourly members, penalizes each assignment proportionally to cost.
|
|
348
|
-
* For salaried members, adds a fixed weekly salary cost when they have
|
|
349
|
-
* any assignment that week (zero marginal cost up to contracted hours).
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* ```ts
|
|
353
|
-
* minimizeCost()
|
|
354
|
-
* ```
|
|
355
|
-
*/
|
|
356
|
-
export function minimizeCost(opts) {
|
|
357
|
-
return makeRule("minimize-cost", { ...opts });
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Multiplies the base rate for assignments on specified days.
|
|
361
|
-
*
|
|
362
|
-
* @remarks
|
|
363
|
-
* The base cost (1x) is already counted by {@link minimizeCost};
|
|
364
|
-
* this rule adds only the extra portion above 1x.
|
|
365
|
-
*
|
|
366
|
-
* @example Weekend multiplier
|
|
367
|
-
* ```typescript
|
|
368
|
-
* dayMultiplier(1.5, { dayOfWeek: weekend })
|
|
369
|
-
* ```
|
|
370
|
-
*/
|
|
371
|
-
export function dayMultiplier(factor, opts) {
|
|
372
|
-
return makeRule("day-cost-multiplier", { factor, ...opts });
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Adds a flat extra amount per hour for assignments on specified days.
|
|
376
|
-
*
|
|
377
|
-
* @remarks
|
|
378
|
-
* The surcharge is independent of the member's base rate.
|
|
379
|
-
*
|
|
380
|
-
* @example Weekend surcharge
|
|
381
|
-
* ```typescript
|
|
382
|
-
* daySurcharge(500, { dayOfWeek: weekend })
|
|
383
|
-
* ```
|
|
384
|
-
*/
|
|
385
|
-
export function daySurcharge(amountPerHour, opts) {
|
|
386
|
-
return makeRule("day-cost-surcharge", { amountPerHour, ...opts });
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Adds a flat surcharge per hour for the portion of a shift that overlaps a time-of-day window.
|
|
390
|
-
*
|
|
391
|
-
* @remarks
|
|
392
|
-
* The window supports overnight spans (e.g., 22:00-06:00). The surcharge
|
|
393
|
-
* is independent of the member's base rate.
|
|
394
|
-
*
|
|
395
|
-
* @param amountPerHour - Flat surcharge per hour in smallest currency unit
|
|
396
|
-
* @param window - Time-of-day window
|
|
397
|
-
* @param opts - Entity and time scoping
|
|
398
|
-
*
|
|
399
|
-
* @example Night differential
|
|
400
|
-
* ```typescript
|
|
401
|
-
* timeSurcharge(200, { from: t(22), until: t(6) })
|
|
402
|
-
* ```
|
|
403
|
-
*/
|
|
404
|
-
export function timeSurcharge(amountPerHour, window, opts) {
|
|
405
|
-
return makeRule("time-cost-surcharge", { amountPerHour, window, ...opts });
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Applies a multiplier to hours beyond a weekly threshold.
|
|
409
|
-
*
|
|
410
|
-
* @remarks
|
|
411
|
-
* Only the extra portion above 1x is added (the base cost is already
|
|
412
|
-
* counted by {@link minimizeCost}).
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```typescript
|
|
416
|
-
* overtimeMultiplier({ after: 40, factor: 1.5 })
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
export function overtimeMultiplier(opts) {
|
|
420
|
-
return makeRule("overtime-weekly-multiplier", { ...opts });
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Adds a flat surcharge per hour beyond a weekly threshold.
|
|
424
|
-
*
|
|
425
|
-
* @remarks
|
|
426
|
-
* The surcharge is independent of the member's base rate.
|
|
427
|
-
*
|
|
428
|
-
* @example
|
|
429
|
-
* ```typescript
|
|
430
|
-
* overtimeSurcharge({ after: 40, amount: 1000 })
|
|
431
|
-
* ```
|
|
432
|
-
*/
|
|
433
|
-
export function overtimeSurcharge(opts) {
|
|
434
|
-
return makeRule("overtime-weekly-surcharge", { ...opts });
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Applies a multiplier to hours beyond a daily threshold.
|
|
438
|
-
*
|
|
439
|
-
* @remarks
|
|
440
|
-
* Only the extra portion above 1x is added (the base cost is already
|
|
441
|
-
* counted by {@link minimizeCost}).
|
|
442
|
-
*
|
|
443
|
-
* @example
|
|
444
|
-
* ```typescript
|
|
445
|
-
* dailyOvertimeMultiplier({ after: 8, factor: 1.5 })
|
|
446
|
-
* ```
|
|
447
|
-
*/
|
|
448
|
-
export function dailyOvertimeMultiplier(opts) {
|
|
449
|
-
return makeRule("overtime-daily-multiplier", { ...opts });
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Adds a flat surcharge per hour beyond a daily threshold.
|
|
453
|
-
*
|
|
454
|
-
* @remarks
|
|
455
|
-
* The surcharge is independent of the member's base rate.
|
|
456
|
-
*
|
|
457
|
-
* @example
|
|
458
|
-
* ```typescript
|
|
459
|
-
* dailyOvertimeSurcharge({ after: 8, amount: 500 })
|
|
460
|
-
* ```
|
|
461
|
-
*/
|
|
462
|
-
export function dailyOvertimeSurcharge(opts) {
|
|
463
|
-
return makeRule("overtime-daily-surcharge", { ...opts });
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Applies multiple overtime thresholds with increasing multipliers.
|
|
467
|
-
*
|
|
468
|
-
* @remarks
|
|
469
|
-
* Each tier applies only to the hours between its threshold and the next.
|
|
470
|
-
* Tiers must be sorted by threshold ascending.
|
|
471
|
-
*
|
|
472
|
-
* @example
|
|
473
|
-
* ```typescript
|
|
474
|
-
* // Hours 0-40: base rate
|
|
475
|
-
* // Hours 40-48: 1.5x
|
|
476
|
-
* // Hours 48+: 2.0x
|
|
477
|
-
* tieredOvertimeMultiplier([
|
|
478
|
-
* { after: 40, factor: 1.5 },
|
|
479
|
-
* { after: 48, factor: 2.0 },
|
|
480
|
-
* ])
|
|
481
|
-
* ```
|
|
482
|
-
*/
|
|
483
|
-
export function tieredOvertimeMultiplier(tiers, opts) {
|
|
484
|
-
return makeRule("overtime-tiered-multiplier", { tiers, ...opts });
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Blocks or penalizes assignments during specified time periods.
|
|
488
|
-
*
|
|
489
|
-
* @remarks
|
|
490
|
-
* At least one time scoping field is required (`dayOfWeek`, `dateRange`,
|
|
491
|
-
* `dates`, or `recurringPeriods`).
|
|
492
|
-
*
|
|
493
|
-
* Use `from` for "off from this time until end of day" and `until` for
|
|
494
|
-
* "off from start of day until this time."
|
|
495
|
-
*
|
|
496
|
-
* @example
|
|
497
|
-
* ```typescript
|
|
498
|
-
* timeOff({ appliesTo: "mauro", dayOfWeek: weekend }),
|
|
499
|
-
* timeOff({ appliesTo: "student", dayOfWeek: ["wednesday"], from: t(14) }),
|
|
500
|
-
* timeOff({ appliesTo: "alice", dateRange: { start: "2024-02-01", end: "2024-02-05" } }),
|
|
501
|
-
* ```
|
|
502
|
-
*/
|
|
503
|
-
export function timeOff(opts) {
|
|
504
|
-
return makeRule("time-off", { ...opts });
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Encourages or enforces that team members work the same shifts on a day.
|
|
508
|
-
*
|
|
509
|
-
* @example
|
|
510
|
-
* ```typescript
|
|
511
|
-
* assignTogether(["alice", "bob"], { priority: "HIGH" }),
|
|
512
|
-
* ```
|
|
513
|
-
*/
|
|
514
|
-
export function assignTogether(members, opts) {
|
|
515
|
-
return makeRule("assign-together", { members, ...opts });
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Defines a complete schedule configuration.
|
|
519
|
-
*
|
|
520
|
-
* @remarks
|
|
521
|
-
* Validates the static config at call time (role/skill disjointness, coverage
|
|
522
|
-
* targets, shift pattern roles). Returns a {@link ScheduleDefinition} whose
|
|
523
|
-
* `createSchedulerConfig` method validates runtime data (member IDs,
|
|
524
|
-
* `appliesTo` resolution) and produces a {@link ModelBuilderConfig}.
|
|
525
|
-
*
|
|
526
|
-
* @example
|
|
527
|
-
* ```typescript
|
|
528
|
-
* import { defineSchedule, t, time, cover, shift, maxHoursPerDay } from "dabke";
|
|
529
|
-
*
|
|
530
|
-
* export default defineSchedule({
|
|
531
|
-
* roles: ["agent", "supervisor"],
|
|
532
|
-
* times: { peak: time({ startTime: t(9), endTime: t(17) }) },
|
|
533
|
-
* coverage: [cover("peak", "agent", 4)],
|
|
534
|
-
* shiftPatterns: [shift("day", t(9), t(17))],
|
|
535
|
-
* rules: [maxHoursPerDay(8)],
|
|
536
|
-
* });
|
|
537
|
-
* ```
|
|
538
|
-
*/
|
|
539
|
-
export function defineSchedule(config) {
|
|
540
|
-
const roles = new Set(config.roles);
|
|
541
|
-
const skills = new Set(config.skills ?? []);
|
|
542
|
-
// Validate role/skill disjointness
|
|
543
|
-
for (const skill of skills) {
|
|
544
|
-
if (roles.has(skill)) {
|
|
545
|
-
throw new Error(`"${skill}" is declared as both a role and a skill. Roles and skills must be disjoint.`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
// Validate shift pattern role references
|
|
549
|
-
for (const sp of config.shiftPatterns) {
|
|
550
|
-
if (sp.roles) {
|
|
551
|
-
for (const role of sp.roles) {
|
|
552
|
-
if (!roles.has(role)) {
|
|
553
|
-
throw new Error(`Shift pattern "${sp.id}" references unknown role "${role}". ` +
|
|
554
|
-
`Declared roles: ${[...roles].join(", ")}`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
// Validate coverage entries
|
|
560
|
-
for (const entry of config.coverage) {
|
|
561
|
-
const targets = Array.isArray(entry.target) ? entry.target : [entry.target];
|
|
562
|
-
if (Array.isArray(entry.target)) {
|
|
563
|
-
// Array target: all must be roles (OR logic)
|
|
564
|
-
for (const target of targets) {
|
|
565
|
-
if (!roles.has(target)) {
|
|
566
|
-
throw new Error(`Coverage for "${entry.timeName}" references "${target}" in a role OR group, ` +
|
|
567
|
-
`but it is not a declared role. Declared roles: ${[...roles].join(", ")}`);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
// Single target: must be role or skill
|
|
573
|
-
if (!roles.has(entry.target) && !skills.has(entry.target)) {
|
|
574
|
-
throw new Error(`Coverage for "${entry.timeName}" references unknown target "${entry.target}". ` +
|
|
575
|
-
`Declared roles: ${[...roles].join(", ")}. ` +
|
|
576
|
-
`Declared skills: ${[...skills].join(", ")}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Validate skills option
|
|
580
|
-
if (entry.options.skills) {
|
|
581
|
-
for (const s of entry.options.skills) {
|
|
582
|
-
if (!skills.has(s)) {
|
|
583
|
-
throw new Error(`Coverage for "${entry.timeName}" uses skill filter "${s}" ` +
|
|
584
|
-
`which is not a declared skill. Declared skills: ${[...skills].join(", ")}`);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// Build semantic time context
|
|
590
|
-
const semanticTimes = defineSemanticTimes(config.times);
|
|
591
|
-
// Convert coverage entries to semantic coverage requirements
|
|
592
|
-
const coverageReqs = buildCoverageRequirements(config.coverage, roles, skills);
|
|
593
|
-
const shiftPatterns = config.shiftPatterns;
|
|
594
|
-
return {
|
|
595
|
-
roles: config.roles,
|
|
596
|
-
skills: (config.skills ?? []),
|
|
597
|
-
timeNames: Object.keys(config.times),
|
|
598
|
-
shiftPatternIds: config.shiftPatterns.map((sp) => sp.id),
|
|
599
|
-
ruleNames: (config.rules ?? []).map((r) => r._rule),
|
|
600
|
-
createSchedulerConfig(args) {
|
|
601
|
-
// Detect duplicate member IDs
|
|
602
|
-
const memberIds = new Set();
|
|
603
|
-
for (const member of args.members) {
|
|
604
|
-
if (memberIds.has(member.id)) {
|
|
605
|
-
throw new Error(`Duplicate member ID "${member.id}".`);
|
|
606
|
-
}
|
|
607
|
-
memberIds.add(member.id);
|
|
608
|
-
}
|
|
609
|
-
// Validate member IDs don't collide with roles/skills
|
|
610
|
-
for (const member of args.members) {
|
|
611
|
-
if (roles.has(member.id)) {
|
|
612
|
-
throw new Error(`Member ID "${member.id}" collides with a declared role name.`);
|
|
613
|
-
}
|
|
614
|
-
if (skills.has(member.id)) {
|
|
615
|
-
throw new Error(`Member ID "${member.id}" collides with a declared skill name.`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
// Validate member roles/skills reference declared roles/skills
|
|
619
|
-
for (const member of args.members) {
|
|
620
|
-
for (const role of member.roles) {
|
|
621
|
-
if (!roles.has(role)) {
|
|
622
|
-
throw new Error(`Member "${member.id}" references unknown role "${role}". ` +
|
|
623
|
-
`Declared roles: ${[...roles].join(", ")}`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if (member.skills) {
|
|
627
|
-
for (const skill of member.skills) {
|
|
628
|
-
if (!skills.has(skill)) {
|
|
629
|
-
throw new Error(`Member "${member.id}" references unknown skill "${skill}". ` +
|
|
630
|
-
`Declared skills: ${[...skills].join(", ")}`);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
// Resolve rules
|
|
636
|
-
const specRules = config.rules ?? [];
|
|
637
|
-
const runtimeRules = args.runtimeRules ?? [];
|
|
638
|
-
const allRules = [...specRules, ...runtimeRules];
|
|
639
|
-
// Validate pay data when cost rules are present
|
|
640
|
-
const costRuleNames = new Set([
|
|
641
|
-
"minimize-cost",
|
|
642
|
-
"day-cost-multiplier",
|
|
643
|
-
"day-cost-surcharge",
|
|
644
|
-
"time-cost-surcharge",
|
|
645
|
-
"overtime-weekly-multiplier",
|
|
646
|
-
"overtime-weekly-surcharge",
|
|
647
|
-
"overtime-daily-multiplier",
|
|
648
|
-
"overtime-daily-surcharge",
|
|
649
|
-
"overtime-tiered-multiplier",
|
|
650
|
-
]);
|
|
651
|
-
const hasCostRules = allRules.some((r) => costRuleNames.has(r._rule));
|
|
652
|
-
if (hasCostRules) {
|
|
653
|
-
const missingPay = args.members.filter((m) => !m.pay).map((m) => m.id);
|
|
654
|
-
if (missingPay.length > 0) {
|
|
655
|
-
throw new Error(`Cost rules require pay data on all members. Missing pay: ${missingPay.join(", ")}`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
// Sort rules so minimize-cost compiles before modifier rules
|
|
659
|
-
const sortedRules = sortCostRulesFirst(allRules);
|
|
660
|
-
const ruleConfigs = resolveRules(sortedRules, roles, skills, memberIds);
|
|
661
|
-
// Resolve scheduling period with dayOfWeek filter
|
|
662
|
-
const resolvedPeriod = applyDaysFilter(args.schedulingPeriod, config.dayOfWeek);
|
|
663
|
-
const days = resolveDaysFromPeriod(resolvedPeriod);
|
|
664
|
-
// Resolve coverage
|
|
665
|
-
const resolvedCoverage = semanticTimes.resolve(coverageReqs, days);
|
|
666
|
-
return {
|
|
667
|
-
members: args.members,
|
|
668
|
-
shiftPatterns,
|
|
669
|
-
schedulingPeriod: resolvedPeriod,
|
|
670
|
-
coverage: resolvedCoverage,
|
|
671
|
-
ruleConfigs,
|
|
672
|
-
weekStartsOn: config.weekStartsOn,
|
|
673
|
-
};
|
|
674
|
-
},
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
// ============================================================================
|
|
678
|
-
// Internal: Coverage Translation
|
|
679
|
-
// ============================================================================
|
|
680
|
-
function buildCoverageRequirements(entries, roles, skills) {
|
|
681
|
-
return entries.map((entry) => {
|
|
682
|
-
// Variant form: produce a VariantCoverageRequirement
|
|
683
|
-
if (entry.variants) {
|
|
684
|
-
return buildVariantCoverageRequirement(entry, roles, skills);
|
|
685
|
-
}
|
|
686
|
-
// Simple form: produce a SemanticCoverageRequirement
|
|
687
|
-
const base = {
|
|
688
|
-
semanticTime: entry.timeName,
|
|
689
|
-
targetCount: entry.count,
|
|
690
|
-
};
|
|
691
|
-
if (entry.options.priority)
|
|
692
|
-
base.priority = entry.options.priority;
|
|
693
|
-
if (entry.options.dayOfWeek)
|
|
694
|
-
base.dayOfWeek = entry.options.dayOfWeek;
|
|
695
|
-
if (entry.options.dates)
|
|
696
|
-
base.dates = entry.options.dates;
|
|
697
|
-
return buildSimpleCoverageTarget(entry, base, roles, skills);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* Resolve the target (role/skill) for a simple coverage entry.
|
|
702
|
-
*/
|
|
703
|
-
function buildSimpleCoverageTarget(entry, base, roles, skills) {
|
|
704
|
-
if (Array.isArray(entry.target)) {
|
|
705
|
-
return {
|
|
706
|
-
...base,
|
|
707
|
-
roles: entry.target,
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
const singleTarget = entry.target;
|
|
711
|
-
if (roles.has(singleTarget)) {
|
|
712
|
-
if (entry.options.skills) {
|
|
713
|
-
return {
|
|
714
|
-
...base,
|
|
715
|
-
roles: [singleTarget],
|
|
716
|
-
skills: entry.options.skills,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
return {
|
|
720
|
-
...base,
|
|
721
|
-
roles: [singleTarget],
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
if (skills.has(singleTarget)) {
|
|
725
|
-
return {
|
|
726
|
-
...base,
|
|
727
|
-
skills: [singleTarget],
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
throw new Error(`Coverage target "${singleTarget}" is not a declared role or skill.`);
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Build a VariantCoverageRequirement from a variant-form CoverageEntry.
|
|
734
|
-
*/
|
|
735
|
-
function buildVariantCoverageRequirement(entry, roles, skills) {
|
|
736
|
-
const variants = entry.variants;
|
|
737
|
-
const resolveTarget = () => {
|
|
738
|
-
if (Array.isArray(entry.target)) {
|
|
739
|
-
return { roles: entry.target };
|
|
740
|
-
}
|
|
741
|
-
const singleTarget = entry.target;
|
|
742
|
-
if (roles.has(singleTarget)) {
|
|
743
|
-
return { roles: [singleTarget] };
|
|
744
|
-
}
|
|
745
|
-
if (skills.has(singleTarget)) {
|
|
746
|
-
return { skills: [singleTarget] };
|
|
747
|
-
}
|
|
748
|
-
throw new Error(`Coverage target "${singleTarget}" is not a declared role or skill.`);
|
|
749
|
-
};
|
|
750
|
-
return {
|
|
751
|
-
semanticTime: entry.timeName,
|
|
752
|
-
variants,
|
|
753
|
-
...resolveTarget(),
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
// ============================================================================
|
|
757
|
-
// Internal: Rule Translation
|
|
758
|
-
// ============================================================================
|
|
759
|
-
/**
|
|
760
|
-
* Resolves an `appliesTo` value into entity scope fields.
|
|
761
|
-
*
|
|
762
|
-
* @remarks
|
|
763
|
-
* Each target string is checked against roles, skills, then member IDs.
|
|
764
|
-
* If all targets resolve to the same namespace, they are combined into one
|
|
765
|
-
* scope field. If they span namespaces, an error is thrown; the caller
|
|
766
|
-
* should use separate rule entries instead.
|
|
767
|
-
*/
|
|
768
|
-
function resolveAppliesTo(appliesTo, roles, skills, memberIds) {
|
|
769
|
-
if (!appliesTo)
|
|
770
|
-
return {};
|
|
771
|
-
const targets = Array.isArray(appliesTo) ? appliesTo : [appliesTo];
|
|
772
|
-
if (targets.length === 0)
|
|
773
|
-
return {};
|
|
774
|
-
const resolvedRoles = [];
|
|
775
|
-
const resolvedSkills = [];
|
|
776
|
-
const resolvedMembers = [];
|
|
777
|
-
for (const target of targets) {
|
|
778
|
-
if (roles.has(target)) {
|
|
779
|
-
resolvedRoles.push(target);
|
|
780
|
-
}
|
|
781
|
-
else if (skills.has(target)) {
|
|
782
|
-
resolvedSkills.push(target);
|
|
783
|
-
}
|
|
784
|
-
else if (memberIds.has(target)) {
|
|
785
|
-
resolvedMembers.push(target);
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
throw new Error(`appliesTo target "${target}" is not a declared role, skill, or member ID.`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
// Count how many namespaces were used
|
|
792
|
-
const namespacesUsed = [resolvedRoles, resolvedSkills, resolvedMembers].filter((arr) => arr.length > 0).length;
|
|
793
|
-
if (namespacesUsed > 1) {
|
|
794
|
-
// Mixed namespaces not supported in a single rule config.
|
|
795
|
-
throw new Error(`appliesTo targets span multiple namespaces (roles: [${resolvedRoles.join(", ")}], ` +
|
|
796
|
-
`skills: [${resolvedSkills.join(", ")}], members: [${resolvedMembers.join(", ")}]). ` +
|
|
797
|
-
`Use separate rule entries for each namespace.`);
|
|
798
|
-
}
|
|
799
|
-
if (resolvedRoles.length > 0) {
|
|
800
|
-
return { roleIds: resolvedRoles };
|
|
801
|
-
}
|
|
802
|
-
if (resolvedSkills.length > 0) {
|
|
803
|
-
return { skillIds: resolvedSkills };
|
|
804
|
-
}
|
|
805
|
-
if (resolvedMembers.length > 0) {
|
|
806
|
-
return { memberIds: resolvedMembers };
|
|
807
|
-
}
|
|
808
|
-
return {};
|
|
809
|
-
}
|
|
810
|
-
function resolveRules(rules, roles, skills, memberIds) {
|
|
811
|
-
return rules.map((rule) => {
|
|
812
|
-
const { _type, _rule, appliesTo, dates, ...passthrough } = rule;
|
|
813
|
-
const entityScope = _rule === "assign-together" ? {} : resolveAppliesTo(appliesTo, roles, skills, memberIds);
|
|
814
|
-
// Rename dates → specificDates (internal field name)
|
|
815
|
-
const resolvedDates = dates ? { specificDates: dates } : {};
|
|
816
|
-
switch (_rule) {
|
|
817
|
-
case "time-off": {
|
|
818
|
-
const { from, until, ...timeOffRest } = passthrough;
|
|
819
|
-
if (!timeOffRest.dayOfWeek &&
|
|
820
|
-
!timeOffRest.dateRange &&
|
|
821
|
-
!dates &&
|
|
822
|
-
!timeOffRest.recurringPeriods) {
|
|
823
|
-
throw new Error("timeOff() requires at least one time scope (dayOfWeek, dateRange, dates, or recurringPeriods).");
|
|
824
|
-
}
|
|
825
|
-
const partialDay = {};
|
|
826
|
-
if (from && until) {
|
|
827
|
-
partialDay.startTime = from;
|
|
828
|
-
partialDay.endTime = until;
|
|
829
|
-
}
|
|
830
|
-
else if (from) {
|
|
831
|
-
partialDay.startTime = from;
|
|
832
|
-
partialDay.endTime = { hours: 23, minutes: 59 };
|
|
833
|
-
}
|
|
834
|
-
else if (until) {
|
|
835
|
-
partialDay.startTime = { hours: 0, minutes: 0 };
|
|
836
|
-
partialDay.endTime = until;
|
|
837
|
-
}
|
|
838
|
-
return {
|
|
839
|
-
name: _rule,
|
|
840
|
-
...timeOffRest,
|
|
841
|
-
...entityScope,
|
|
842
|
-
...resolvedDates,
|
|
843
|
-
...partialDay,
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
case "assign-together": {
|
|
847
|
-
const { members, ...atRest } = passthrough;
|
|
848
|
-
for (const member of members) {
|
|
849
|
-
if (!memberIds.has(member)) {
|
|
850
|
-
throw new Error(`assignTogether references unknown member "${member}". ` +
|
|
851
|
-
`Known member IDs: ${[...memberIds].join(", ")}`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
return { name: _rule, groupMemberIds: members, ...atRest };
|
|
855
|
-
}
|
|
856
|
-
default:
|
|
857
|
-
return {
|
|
858
|
-
name: _rule,
|
|
859
|
-
...passthrough,
|
|
860
|
-
...entityScope,
|
|
861
|
-
...resolvedDates,
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
// ============================================================================
|
|
867
|
-
// Internal: Cost Rule Ordering
|
|
868
|
-
// ============================================================================
|
|
869
|
-
/**
|
|
870
|
-
* Sorts rules so that `minimize-cost` compiles before cost modifier rules.
|
|
871
|
-
*
|
|
872
|
-
* @remarks
|
|
873
|
-
* The `minimize-cost` rule must be compiled first because modifier rules
|
|
874
|
-
* (multipliers, surcharges) reference cost variables it creates.
|
|
875
|
-
* Non-cost rules retain their original relative order.
|
|
876
|
-
*/
|
|
877
|
-
function sortCostRulesFirst(rules) {
|
|
878
|
-
return rules.toSorted((a, b) => {
|
|
879
|
-
const aIsCostBase = a._rule === "minimize-cost" ? 0 : 1;
|
|
880
|
-
const bIsCostBase = b._rule === "minimize-cost" ? 0 : 1;
|
|
881
|
-
return aIsCostBase - bIsCostBase;
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
// ============================================================================
|
|
885
|
-
// Internal: Scheduling Period Helpers
|
|
886
|
-
// ============================================================================
|
|
887
|
-
function applyDaysFilter(schedulingPeriod, dayOfWeek) {
|
|
888
|
-
if (!dayOfWeek || dayOfWeek.length === 0) {
|
|
889
|
-
return schedulingPeriod;
|
|
890
|
-
}
|
|
891
|
-
const existingDays = schedulingPeriod.dayOfWeek;
|
|
892
|
-
if (!existingDays || existingDays.length === 0) {
|
|
893
|
-
return { ...schedulingPeriod, dayOfWeek: dayOfWeek };
|
|
894
|
-
}
|
|
895
|
-
const existingSet = new Set(existingDays);
|
|
896
|
-
const intersected = dayOfWeek.filter((day) => existingSet.has(day));
|
|
897
|
-
return { ...schedulingPeriod, dayOfWeek: intersected };
|
|
898
|
-
}
|
|
899
|
-
//# sourceMappingURL=schedule.js.map
|