dabke 0.82.0 → 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 +23 -0
- package/README.md +6 -3
- 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-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/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/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/registry.d.ts +4 -1
- 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/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 +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- 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.js → schedule/definition.js} +9 -673
- 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/package.json +4 -9
- package/src/cpsat/rules/index.ts +3 -0
- package/src/cpsat/rules/max-days-week.ts +143 -0
- package/src/cpsat/rules/min-days-week.ts +120 -0
- package/src/cpsat/rules/must-assign.ts +108 -0
- package/src/cpsat/rules/registry.ts +6 -0
- package/src/cpsat/rules/rules.types.ts +3 -0
- package/src/cpsat/rules/scope.types.ts +1 -1
- package/src/index.ts +8 -3
- 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/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 -917
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js.map +0 -1
- package/llms.txt +0 -758
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1960
|
@@ -1,630 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
* schedule, t, time, cover, shift,
|
|
12
|
-
* maxHoursPerDay, maxHoursPerWeek, minRestBetweenShifts,
|
|
13
|
-
* weekdays, weekend,
|
|
14
|
-
* } from "dabke";
|
|
15
|
-
*
|
|
16
|
-
* const venue = schedule({
|
|
17
|
-
* roleIds: ["cashier", "floor_lead", "stocker"],
|
|
18
|
-
* skillIds: ["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
|
-
* const result = await venue
|
|
49
|
-
* .with([
|
|
50
|
-
* { id: "alice", roleIds: ["cashier"], skillIds: ["keyholder"] },
|
|
51
|
-
* ])
|
|
52
|
-
* .solve(client, { dateRange: { start: "2025-03-03", end: "2025-03-09" } });
|
|
53
|
-
* ```
|
|
2
|
+
* Schedule definition, compilation, and solving.
|
|
54
3
|
*
|
|
55
4
|
* @module
|
|
56
5
|
*/
|
|
57
|
-
import { defineSemanticTimes } from "
|
|
58
|
-
import { resolveDaysFromPeriod } from "
|
|
59
|
-
import { ModelBuilder } from "
|
|
60
|
-
import { builtInCpsatRuleFactories } from "
|
|
61
|
-
import { parseSolverResponse, resolveAssignments } from "
|
|
62
|
-
import { calculateScheduleCost } from "
|
|
63
|
-
|
|
64
|
-
// Primitives
|
|
65
|
-
// ============================================================================
|
|
66
|
-
/**
|
|
67
|
-
* Creates a {@link TimeOfDay} value.
|
|
68
|
-
*
|
|
69
|
-
* @param hours - Hour component (0-23)
|
|
70
|
-
* @param minutes - Minute component (0-59)
|
|
71
|
-
*
|
|
72
|
-
* @example Hours only
|
|
73
|
-
* ```ts
|
|
74
|
-
* t(9) // { hours: 9, minutes: 0 }
|
|
75
|
-
* ```
|
|
76
|
-
*
|
|
77
|
-
* @example Hours and minutes
|
|
78
|
-
* ```ts
|
|
79
|
-
* t(17, 30) // { hours: 17, minutes: 30 }
|
|
80
|
-
* ```
|
|
81
|
-
*
|
|
82
|
-
* @category Time Periods
|
|
83
|
-
*/
|
|
84
|
-
export function t(hours, minutes = 0) {
|
|
85
|
-
return { hours, minutes };
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Monday through Friday.
|
|
89
|
-
*
|
|
90
|
-
* @category Time Periods
|
|
91
|
-
*/
|
|
92
|
-
export const weekdays = [
|
|
93
|
-
"monday",
|
|
94
|
-
"tuesday",
|
|
95
|
-
"wednesday",
|
|
96
|
-
"thursday",
|
|
97
|
-
"friday",
|
|
98
|
-
];
|
|
99
|
-
/**
|
|
100
|
-
* Saturday and Sunday.
|
|
101
|
-
*
|
|
102
|
-
* @category Time Periods
|
|
103
|
-
*/
|
|
104
|
-
export const weekend = ["saturday", "sunday"];
|
|
105
|
-
// ============================================================================
|
|
106
|
-
// Semantic Times
|
|
107
|
-
// ============================================================================
|
|
108
|
-
/**
|
|
109
|
-
* Define a named semantic time period.
|
|
110
|
-
*
|
|
111
|
-
* @remarks
|
|
112
|
-
* Each entry has `startTime`/`endTime` and optional `dayOfWeek` or `dates`
|
|
113
|
-
* scoping. Entries without scoping are the default.
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* ```typescript
|
|
117
|
-
* times: {
|
|
118
|
-
* // Simple: same times every day
|
|
119
|
-
* lunch: time({ startTime: t(12), endTime: t(15) }),
|
|
120
|
-
*
|
|
121
|
-
* // Variants: different times on weekends
|
|
122
|
-
* dinner: time(
|
|
123
|
-
* { startTime: t(17), endTime: t(21) },
|
|
124
|
-
* { startTime: t(18), endTime: t(22), dayOfWeek: weekend },
|
|
125
|
-
* ),
|
|
126
|
-
*
|
|
127
|
-
* // Point-in-time window (keyholder at opening)
|
|
128
|
-
* opening: time({ startTime: t(8, 30), endTime: t(9) }),
|
|
129
|
-
* }
|
|
130
|
-
* ```
|
|
131
|
-
*
|
|
132
|
-
* @privateRemarks
|
|
133
|
-
* Resolution precedence: `dates` > `dayOfWeek` > default.
|
|
134
|
-
*
|
|
135
|
-
* @category Time Periods
|
|
136
|
-
*/
|
|
137
|
-
export function time(...entries) {
|
|
138
|
-
// Validate: at most one default (no dayOfWeek and no dates)
|
|
139
|
-
const defaults = entries.filter((e) => !e.dayOfWeek && !e.dates);
|
|
140
|
-
if (defaults.length > 1) {
|
|
141
|
-
throw new Error("time() accepts at most one default entry (without dayOfWeek or dates). " +
|
|
142
|
-
`Found ${defaults.length} default entries.`);
|
|
143
|
-
}
|
|
144
|
-
// Single entry without scoping: simple SemanticTimeDef
|
|
145
|
-
if (entries.length === 1 && !entries[0].dayOfWeek && !entries[0].dates) {
|
|
146
|
-
return {
|
|
147
|
-
startTime: entries[0].startTime,
|
|
148
|
-
endTime: entries[0].endTime,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
// Multiple entries or scoped entries: shallow-copy to decouple from caller
|
|
152
|
-
return entries.map((entry) => Object.assign({}, entry));
|
|
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
|
-
* Define a shift pattern: a time slot available for employee assignment.
|
|
186
|
-
*
|
|
187
|
-
* @remarks
|
|
188
|
-
* Each pattern repeats daily unless filtered by `dayOfWeek`.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* ```typescript
|
|
192
|
-
* shiftPatterns: [
|
|
193
|
-
* shift("morning", t(11, 30), t(15)),
|
|
194
|
-
* shift("evening", t(17), t(22)),
|
|
195
|
-
*
|
|
196
|
-
* // Role-restricted shift
|
|
197
|
-
* shift("kitchen", t(6), t(14), { roleIds: ["chef", "prep_cook"] }),
|
|
198
|
-
*
|
|
199
|
-
* // Day-restricted shift
|
|
200
|
-
* shift("saturday_short", t(9), t(14), { dayOfWeek: ["saturday"] }),
|
|
201
|
-
*
|
|
202
|
-
* // Location-specific shift
|
|
203
|
-
* shift("terrace_lunch", t(12), t(16), { locationId: "terrace" }),
|
|
204
|
-
* ]
|
|
205
|
-
* ```
|
|
206
|
-
*
|
|
207
|
-
* @category Shift Patterns
|
|
208
|
-
*/
|
|
209
|
-
export function shift(id, startTime, endTime, opts) {
|
|
210
|
-
const pattern = { id, startTime, endTime };
|
|
211
|
-
if (opts?.roleIds)
|
|
212
|
-
pattern.roleIds = opts.roleIds;
|
|
213
|
-
if (opts?.dayOfWeek)
|
|
214
|
-
pattern.dayOfWeek = opts.dayOfWeek;
|
|
215
|
-
if (opts?.locationId)
|
|
216
|
-
pattern.locationId = opts.locationId;
|
|
217
|
-
return pattern;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Creates a rule entry for use in {@link ScheduleConfig.rules}.
|
|
221
|
-
*
|
|
222
|
-
* Built-in rules use the helpers (`maxHoursPerDay`, `timeOff`, etc.).
|
|
223
|
-
* Custom rules can use `defineRule` to create entries that plug into the
|
|
224
|
-
* same resolution and compilation pipeline.
|
|
225
|
-
*
|
|
226
|
-
* @param name - Rule name. Must match a key in the rule factory registry.
|
|
227
|
-
* @param fields - Rule-specific configuration fields.
|
|
228
|
-
* @param resolve - Optional custom resolver. When omitted, the default
|
|
229
|
-
* resolution applies: `appliesTo` is mapped to `roleIds`/`skillIds`/`memberIds`,
|
|
230
|
-
* `dates` is renamed to `specificDates`, and all other fields pass through.
|
|
231
|
-
*
|
|
232
|
-
* @category Rules
|
|
233
|
-
*/
|
|
234
|
-
export function defineRule(name, fields, resolve) {
|
|
235
|
-
const { _type: _, _rule: __, ...safeFields } = fields;
|
|
236
|
-
const entry = { _type: "rule", _rule: name, ...safeFields };
|
|
237
|
-
if (resolve) {
|
|
238
|
-
Object.defineProperty(entry, "_resolve", { value: resolve, enumerable: false });
|
|
239
|
-
}
|
|
240
|
-
return entry;
|
|
241
|
-
}
|
|
242
|
-
function makeRule(rule, fields) {
|
|
243
|
-
return defineRule(rule, fields);
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Limits hours per day.
|
|
247
|
-
*
|
|
248
|
-
* @example
|
|
249
|
-
* ```typescript
|
|
250
|
-
* maxHoursPerDay(10)
|
|
251
|
-
* maxHoursPerDay(4, { appliesTo: "student", dayOfWeek: weekdays })
|
|
252
|
-
* ```
|
|
253
|
-
*
|
|
254
|
-
* @category Rules
|
|
255
|
-
*/
|
|
256
|
-
export function maxHoursPerDay(hours, opts) {
|
|
257
|
-
return makeRule("max-hours-day", { hours, ...opts });
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Limits hours per scheduling week.
|
|
261
|
-
*
|
|
262
|
-
* @example
|
|
263
|
-
* ```typescript
|
|
264
|
-
* maxHoursPerWeek(48)
|
|
265
|
-
* maxHoursPerWeek(20, { appliesTo: "student" })
|
|
266
|
-
* ```
|
|
267
|
-
*
|
|
268
|
-
* @category Rules
|
|
269
|
-
*/
|
|
270
|
-
export function maxHoursPerWeek(hours, opts) {
|
|
271
|
-
return makeRule("max-hours-week", { hours, ...opts });
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Minimum hours when assigned on a day.
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* ```typescript
|
|
278
|
-
* minHoursPerDay(4)
|
|
279
|
-
* ```
|
|
280
|
-
*
|
|
281
|
-
* @category Rules
|
|
282
|
-
*/
|
|
283
|
-
export function minHoursPerDay(hours, opts) {
|
|
284
|
-
return makeRule("min-hours-day", { hours, ...opts });
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Minimum hours per scheduling week.
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* ```typescript
|
|
291
|
-
* minHoursPerWeek(20, { priority: "HIGH" })
|
|
292
|
-
* ```
|
|
293
|
-
*
|
|
294
|
-
* @category Rules
|
|
295
|
-
*/
|
|
296
|
-
export function minHoursPerWeek(hours, opts) {
|
|
297
|
-
return makeRule("min-hours-week", { hours, ...opts });
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Maximum distinct shifts per day.
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```typescript
|
|
304
|
-
* maxShiftsPerDay(1)
|
|
305
|
-
* maxShiftsPerDay(2, { appliesTo: "student", dayOfWeek: weekend })
|
|
306
|
-
* ```
|
|
307
|
-
*
|
|
308
|
-
* @category Rules
|
|
309
|
-
*/
|
|
310
|
-
export function maxShiftsPerDay(shifts, opts) {
|
|
311
|
-
return makeRule("max-shifts-day", { shifts, ...opts });
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Maximum consecutive working days.
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* ```typescript
|
|
318
|
-
* maxConsecutiveDays(5)
|
|
319
|
-
* ```
|
|
320
|
-
*
|
|
321
|
-
* @category Rules
|
|
322
|
-
*/
|
|
323
|
-
export function maxConsecutiveDays(days, opts) {
|
|
324
|
-
return makeRule("max-consecutive-days", { days, ...opts });
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Once working, continue for at least this many consecutive days.
|
|
328
|
-
*
|
|
329
|
-
* @example
|
|
330
|
-
* ```typescript
|
|
331
|
-
* minConsecutiveDays(2, { priority: "HIGH" })
|
|
332
|
-
* ```
|
|
333
|
-
*
|
|
334
|
-
* @category Rules
|
|
335
|
-
*/
|
|
336
|
-
export function minConsecutiveDays(days, opts) {
|
|
337
|
-
return makeRule("min-consecutive-days", { days, ...opts });
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Minimum rest hours between shifts.
|
|
341
|
-
*
|
|
342
|
-
* @example
|
|
343
|
-
* ```typescript
|
|
344
|
-
* minRestBetweenShifts(10)
|
|
345
|
-
* ```
|
|
346
|
-
*
|
|
347
|
-
* @category Rules
|
|
348
|
-
*/
|
|
349
|
-
export function minRestBetweenShifts(hours, opts) {
|
|
350
|
-
return makeRule("min-rest-between-shifts", { hours, ...opts });
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Prefer (`"high"`) or avoid (`"low"`) assigning. Requires `appliesTo`.
|
|
354
|
-
*
|
|
355
|
-
* @example
|
|
356
|
-
* ```typescript
|
|
357
|
-
* preference("high", { appliesTo: "waiter" })
|
|
358
|
-
* preference("low", { appliesTo: "student", dayOfWeek: weekdays })
|
|
359
|
-
* ```
|
|
360
|
-
*
|
|
361
|
-
* @category Rules
|
|
362
|
-
*/
|
|
363
|
-
export function preference(level, opts) {
|
|
364
|
-
return makeRule("assignment-priority", { preference: level, ...opts });
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Prefer assigning to shifts at a specific location. Requires `appliesTo`.
|
|
368
|
-
*
|
|
369
|
-
* @example
|
|
370
|
-
* ```typescript
|
|
371
|
-
* preferLocation("terrace", { appliesTo: "alice" })
|
|
372
|
-
* ```
|
|
373
|
-
*
|
|
374
|
-
* @category Rules
|
|
375
|
-
*/
|
|
376
|
-
export function preferLocation(locationId, opts) {
|
|
377
|
-
return makeRule("location-preference", { locationId, ...opts });
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Tells the solver to minimize total labor cost.
|
|
381
|
-
*
|
|
382
|
-
* @remarks
|
|
383
|
-
* Without this rule, cost modifiers only affect post-solve calculation.
|
|
384
|
-
* When present, the solver actively prefers cheaper assignments.
|
|
385
|
-
*
|
|
386
|
-
* For hourly members, penalizes each assignment proportionally to cost.
|
|
387
|
-
* For salaried members, adds a fixed weekly salary cost when they have
|
|
388
|
-
* any assignment that week (zero marginal cost up to contracted hours).
|
|
389
|
-
*
|
|
390
|
-
* Cost modifiers adjust the calculation:
|
|
391
|
-
* - `dayMultiplier(factor, opts?)` - multiply base rate on specific days
|
|
392
|
-
* - `daySurcharge(amount, opts?)` - flat extra per hour on specific days
|
|
393
|
-
* - `timeSurcharge(amount, window, opts?)` - flat extra per hour during a time window
|
|
394
|
-
* - `overtimeMultiplier({ after, factor }, opts?)` - weekly overtime multiplier
|
|
395
|
-
* - `overtimeSurcharge({ after, amount }, opts?)` - weekly overtime surcharge
|
|
396
|
-
* - `dailyOvertimeMultiplier({ after, factor }, opts?)` - daily overtime multiplier
|
|
397
|
-
* - `dailyOvertimeSurcharge({ after, amount }, opts?)` - daily overtime surcharge
|
|
398
|
-
* - `tieredOvertimeMultiplier(tiers, opts?)` - multiple overtime thresholds
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* ```ts
|
|
402
|
-
* minimizeCost()
|
|
403
|
-
* ```
|
|
404
|
-
*
|
|
405
|
-
* @category Cost Optimization
|
|
406
|
-
*/
|
|
407
|
-
export function minimizeCost(opts) {
|
|
408
|
-
return makeRule("minimize-cost", { ...opts });
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Multiplies the base rate for assignments on specified days.
|
|
412
|
-
*
|
|
413
|
-
* @remarks
|
|
414
|
-
* The base cost (1x) is already counted by {@link minimizeCost};
|
|
415
|
-
* this rule adds only the extra portion above 1x.
|
|
416
|
-
*
|
|
417
|
-
* @category Cost Optimization
|
|
418
|
-
*
|
|
419
|
-
* @example Weekend multiplier
|
|
420
|
-
* ```typescript
|
|
421
|
-
* dayMultiplier(1.5, { dayOfWeek: weekend })
|
|
422
|
-
* ```
|
|
423
|
-
*/
|
|
424
|
-
export function dayMultiplier(factor, opts) {
|
|
425
|
-
return makeRule("day-cost-multiplier", { factor, ...opts });
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Adds a flat extra amount per hour for assignments on specified days.
|
|
429
|
-
*
|
|
430
|
-
* @remarks
|
|
431
|
-
* The surcharge is independent of the member's base rate.
|
|
432
|
-
*
|
|
433
|
-
* @category Cost Optimization
|
|
434
|
-
*
|
|
435
|
-
* @example Weekend surcharge
|
|
436
|
-
* ```typescript
|
|
437
|
-
* daySurcharge(500, { dayOfWeek: weekend })
|
|
438
|
-
* ```
|
|
439
|
-
*/
|
|
440
|
-
export function daySurcharge(amountPerHour, opts) {
|
|
441
|
-
return makeRule("day-cost-surcharge", { amountPerHour, ...opts });
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Adds a flat surcharge per hour for the portion of a shift that overlaps a time-of-day window.
|
|
445
|
-
*
|
|
446
|
-
* @remarks
|
|
447
|
-
* The window supports overnight spans (e.g., 22:00-06:00). The surcharge
|
|
448
|
-
* is independent of the member's base rate.
|
|
449
|
-
*
|
|
450
|
-
* @param amountPerHour - Flat surcharge per hour in smallest currency unit
|
|
451
|
-
* @param window - Time-of-day window
|
|
452
|
-
* @param opts - Entity and time scoping
|
|
453
|
-
*
|
|
454
|
-
* @category Cost Optimization
|
|
455
|
-
*
|
|
456
|
-
* @example Night differential
|
|
457
|
-
* ```typescript
|
|
458
|
-
* timeSurcharge(200, { from: t(22), until: t(6) })
|
|
459
|
-
* ```
|
|
460
|
-
*/
|
|
461
|
-
export function timeSurcharge(amountPerHour, window, opts) {
|
|
462
|
-
return makeRule("time-cost-surcharge", { amountPerHour, window, ...opts });
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Applies a multiplier to hours beyond a weekly threshold.
|
|
466
|
-
*
|
|
467
|
-
* @remarks
|
|
468
|
-
* Only the extra portion above 1x is added (the base cost is already
|
|
469
|
-
* counted by {@link minimizeCost}).
|
|
470
|
-
*
|
|
471
|
-
* @category Cost Optimization
|
|
472
|
-
*
|
|
473
|
-
* @example
|
|
474
|
-
* ```typescript
|
|
475
|
-
* overtimeMultiplier({ after: 40, factor: 1.5 })
|
|
476
|
-
* ```
|
|
477
|
-
*/
|
|
478
|
-
export function overtimeMultiplier(opts) {
|
|
479
|
-
return makeRule("overtime-weekly-multiplier", { ...opts });
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Adds a flat surcharge per hour beyond a weekly threshold.
|
|
483
|
-
*
|
|
484
|
-
* @remarks
|
|
485
|
-
* The surcharge is independent of the member's base rate.
|
|
486
|
-
*
|
|
487
|
-
* @category Cost Optimization
|
|
488
|
-
*
|
|
489
|
-
* @example
|
|
490
|
-
* ```typescript
|
|
491
|
-
* overtimeSurcharge({ after: 40, amount: 1000 })
|
|
492
|
-
* ```
|
|
493
|
-
*/
|
|
494
|
-
export function overtimeSurcharge(opts) {
|
|
495
|
-
return makeRule("overtime-weekly-surcharge", { ...opts });
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Applies a multiplier to hours beyond a daily threshold.
|
|
499
|
-
*
|
|
500
|
-
* @remarks
|
|
501
|
-
* Only the extra portion above 1x is added (the base cost is already
|
|
502
|
-
* counted by {@link minimizeCost}).
|
|
503
|
-
*
|
|
504
|
-
* @category Cost Optimization
|
|
505
|
-
*
|
|
506
|
-
* @example
|
|
507
|
-
* ```typescript
|
|
508
|
-
* dailyOvertimeMultiplier({ after: 8, factor: 1.5 })
|
|
509
|
-
* ```
|
|
510
|
-
*/
|
|
511
|
-
export function dailyOvertimeMultiplier(opts) {
|
|
512
|
-
return makeRule("overtime-daily-multiplier", { ...opts });
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Adds a flat surcharge per hour beyond a daily threshold.
|
|
516
|
-
*
|
|
517
|
-
* @remarks
|
|
518
|
-
* The surcharge is independent of the member's base rate.
|
|
519
|
-
*
|
|
520
|
-
* @category Cost Optimization
|
|
521
|
-
*
|
|
522
|
-
* @example
|
|
523
|
-
* ```typescript
|
|
524
|
-
* dailyOvertimeSurcharge({ after: 8, amount: 500 })
|
|
525
|
-
* ```
|
|
526
|
-
*/
|
|
527
|
-
export function dailyOvertimeSurcharge(opts) {
|
|
528
|
-
return makeRule("overtime-daily-surcharge", { ...opts });
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Applies multiple overtime thresholds with increasing multipliers.
|
|
532
|
-
*
|
|
533
|
-
* @remarks
|
|
534
|
-
* Each tier applies only to the hours between its threshold and the next.
|
|
535
|
-
* Tiers must be sorted by threshold ascending.
|
|
536
|
-
*
|
|
537
|
-
* @category Cost Optimization
|
|
538
|
-
*
|
|
539
|
-
* @example
|
|
540
|
-
* ```typescript
|
|
541
|
-
* // Hours 0-40: base rate
|
|
542
|
-
* // Hours 40-48: 1.5x
|
|
543
|
-
* // Hours 48+: 2.0x
|
|
544
|
-
* tieredOvertimeMultiplier([
|
|
545
|
-
* { after: 40, factor: 1.5 },
|
|
546
|
-
* { after: 48, factor: 2.0 },
|
|
547
|
-
* ])
|
|
548
|
-
* ```
|
|
549
|
-
*/
|
|
550
|
-
export function tieredOvertimeMultiplier(tiers, opts) {
|
|
551
|
-
return makeRule("overtime-tiered-multiplier", { tiers, ...opts });
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Block assignments during specified periods.
|
|
555
|
-
* Requires at least one time scope (`dayOfWeek`, `dateRange`, `dates`, or `from`/`until`).
|
|
556
|
-
*
|
|
557
|
-
* @example
|
|
558
|
-
* ```typescript
|
|
559
|
-
* // Full days off
|
|
560
|
-
* timeOff({ appliesTo: "alice", dateRange: { start: "2024-02-01", end: "2024-02-05" } })
|
|
561
|
-
*
|
|
562
|
-
* // Every weekend off
|
|
563
|
-
* timeOff({ appliesTo: "mauro", dayOfWeek: weekend })
|
|
564
|
-
*
|
|
565
|
-
* // Wednesday afternoons off
|
|
566
|
-
* timeOff({ appliesTo: "student", dayOfWeek: ["wednesday"], from: t(14) })
|
|
567
|
-
* ```
|
|
568
|
-
*
|
|
569
|
-
* @category Rules
|
|
570
|
-
*/
|
|
571
|
-
export function timeOff(opts) {
|
|
572
|
-
const { from, until, ...rest } = opts;
|
|
573
|
-
return defineRule("time-off", { from, until, ...rest }, (ctx) => {
|
|
574
|
-
if (!rest.dayOfWeek && !rest.dateRange && !rest.dates && !rest.recurringPeriods) {
|
|
575
|
-
throw new Error("timeOff() requires at least one time scope (dayOfWeek, dateRange, dates, or recurringPeriods).");
|
|
576
|
-
}
|
|
577
|
-
const { appliesTo, dates, ...passthrough } = rest;
|
|
578
|
-
const entityScope = resolveAppliesTo(appliesTo, ctx.roles, ctx.skills, ctx.memberIds);
|
|
579
|
-
const resolvedDates = dates ? { specificDates: dates } : {};
|
|
580
|
-
const partialDay = {};
|
|
581
|
-
if (from && until) {
|
|
582
|
-
partialDay.startTime = from;
|
|
583
|
-
partialDay.endTime = until;
|
|
584
|
-
}
|
|
585
|
-
else if (from) {
|
|
586
|
-
partialDay.startTime = from;
|
|
587
|
-
partialDay.endTime = { hours: 23, minutes: 59 };
|
|
588
|
-
}
|
|
589
|
-
else if (until) {
|
|
590
|
-
partialDay.startTime = { hours: 0, minutes: 0 };
|
|
591
|
-
partialDay.endTime = until;
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
name: "time-off",
|
|
595
|
-
...passthrough,
|
|
596
|
-
...entityScope,
|
|
597
|
-
...resolvedDates,
|
|
598
|
-
...partialDay,
|
|
599
|
-
};
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Members work the same shifts on days they are both assigned.
|
|
604
|
-
*
|
|
605
|
-
* @example
|
|
606
|
-
* ```typescript
|
|
607
|
-
* assignTogether(["alice", "bob"])
|
|
608
|
-
* assignTogether(["alice", "bob", "charlie"], { priority: "HIGH" })
|
|
609
|
-
* ```
|
|
610
|
-
*
|
|
611
|
-
* @category Rules
|
|
612
|
-
*/
|
|
613
|
-
export function assignTogether(memberIds, opts) {
|
|
614
|
-
return defineRule("assign-together", { members: memberIds, ...opts }, (ctx) => {
|
|
615
|
-
for (const member of memberIds) {
|
|
616
|
-
if (!ctx.memberIds.has(member)) {
|
|
617
|
-
throw new Error(`assignTogether references unknown member "${member}". ` +
|
|
618
|
-
`Known member IDs: ${[...ctx.memberIds].join(", ")}`);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return {
|
|
622
|
-
name: "assign-together",
|
|
623
|
-
groupMemberIds: memberIds,
|
|
624
|
-
...opts,
|
|
625
|
-
};
|
|
626
|
-
});
|
|
627
|
-
}
|
|
6
|
+
import { defineSemanticTimes } from "../cpsat/semantic-time.js";
|
|
7
|
+
import { resolveDaysFromPeriod } from "../datetime.utils.js";
|
|
8
|
+
import { ModelBuilder } from "../cpsat/model-builder.js";
|
|
9
|
+
import { builtInCpsatRuleFactories } from "../cpsat/rules/registry.js";
|
|
10
|
+
import { parseSolverResponse, resolveAssignments } from "../cpsat/response.js";
|
|
11
|
+
import { calculateScheduleCost } from "../cpsat/cost.js";
|
|
12
|
+
import { resolveAppliesTo } from "./rules.js";
|
|
628
13
|
// ============================================================================
|
|
629
14
|
// Schedule class
|
|
630
15
|
// ============================================================================
|
|
@@ -1220,55 +605,6 @@ function buildVariantCoverageRequirement(entry, roles, skills) {
|
|
|
1220
605
|
// ============================================================================
|
|
1221
606
|
// Internal: Rule Translation
|
|
1222
607
|
// ============================================================================
|
|
1223
|
-
/**
|
|
1224
|
-
* Resolves an `appliesTo` value into entity scope fields.
|
|
1225
|
-
*
|
|
1226
|
-
* Each target string is checked against roles, skills, then member IDs.
|
|
1227
|
-
* If all targets resolve to the same namespace, they are combined into one
|
|
1228
|
-
* scope field. If they span namespaces, an error is thrown; the caller
|
|
1229
|
-
* should use separate rule entries instead.
|
|
1230
|
-
*/
|
|
1231
|
-
function resolveAppliesTo(appliesTo, roles, skills, memberIds) {
|
|
1232
|
-
if (!appliesTo)
|
|
1233
|
-
return {};
|
|
1234
|
-
const targets = Array.isArray(appliesTo) ? appliesTo : [appliesTo];
|
|
1235
|
-
if (targets.length === 0)
|
|
1236
|
-
return {};
|
|
1237
|
-
const resolvedRoles = [];
|
|
1238
|
-
const resolvedSkills = [];
|
|
1239
|
-
const resolvedMembers = [];
|
|
1240
|
-
for (const target of targets) {
|
|
1241
|
-
if (roles.has(target)) {
|
|
1242
|
-
resolvedRoles.push(target);
|
|
1243
|
-
}
|
|
1244
|
-
else if (skills.has(target)) {
|
|
1245
|
-
resolvedSkills.push(target);
|
|
1246
|
-
}
|
|
1247
|
-
else if (memberIds.has(target)) {
|
|
1248
|
-
resolvedMembers.push(target);
|
|
1249
|
-
}
|
|
1250
|
-
else {
|
|
1251
|
-
throw new Error(`appliesTo target "${target}" is not a declared role, skill, or member ID.`);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
// Count how many namespaces were used
|
|
1255
|
-
const namespacesUsed = [resolvedRoles, resolvedSkills, resolvedMembers].filter((arr) => arr.length > 0).length;
|
|
1256
|
-
if (namespacesUsed > 1) {
|
|
1257
|
-
throw new Error(`appliesTo targets span multiple namespaces (roles: [${resolvedRoles.join(", ")}], ` +
|
|
1258
|
-
`skills: [${resolvedSkills.join(", ")}], members: [${resolvedMembers.join(", ")}]). ` +
|
|
1259
|
-
`Use separate rule entries for each namespace.`);
|
|
1260
|
-
}
|
|
1261
|
-
if (resolvedRoles.length > 0) {
|
|
1262
|
-
return { roleIds: resolvedRoles };
|
|
1263
|
-
}
|
|
1264
|
-
if (resolvedSkills.length > 0) {
|
|
1265
|
-
return { skillIds: resolvedSkills };
|
|
1266
|
-
}
|
|
1267
|
-
if (resolvedMembers.length > 0) {
|
|
1268
|
-
return { memberIds: resolvedMembers };
|
|
1269
|
-
}
|
|
1270
|
-
return {};
|
|
1271
|
-
}
|
|
1272
608
|
function resolveRules(rules, roles, skills, memberIds) {
|
|
1273
609
|
const ctx = { roles, skills, memberIds };
|
|
1274
610
|
return rules.map((rule) => {
|
|
@@ -1320,4 +656,4 @@ function applyDaysFilter(schedulingPeriod, dayOfWeek) {
|
|
|
1320
656
|
const intersected = dayOfWeek.filter((day) => existingSet.has(day));
|
|
1321
657
|
return { ...schedulingPeriod, dayOfWeek: intersected };
|
|
1322
658
|
}
|
|
1323
|
-
//# sourceMappingURL=
|
|
659
|
+
//# sourceMappingURL=definition.js.map
|