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.
Files changed (83) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -3
  3. package/dist/cpsat/rules/index.d.ts +3 -0
  4. package/dist/cpsat/rules/index.d.ts.map +1 -1
  5. package/dist/cpsat/rules/index.js +3 -0
  6. package/dist/cpsat/rules/index.js.map +1 -1
  7. package/dist/cpsat/rules/max-days-week.d.ts +44 -0
  8. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
  9. package/dist/cpsat/rules/max-days-week.js +95 -0
  10. package/dist/cpsat/rules/max-days-week.js.map +1 -0
  11. package/dist/cpsat/rules/min-days-week.d.ts +34 -0
  12. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
  13. package/dist/cpsat/rules/min-days-week.js +84 -0
  14. package/dist/cpsat/rules/min-days-week.js.map +1 -0
  15. package/dist/cpsat/rules/must-assign.d.ts +49 -0
  16. package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
  17. package/dist/cpsat/rules/must-assign.js +86 -0
  18. package/dist/cpsat/rules/must-assign.js.map +1 -0
  19. package/dist/cpsat/rules/registry.d.ts +4 -1
  20. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  21. package/dist/cpsat/rules/registry.js +4 -1
  22. package/dist/cpsat/rules/registry.js.map +1 -1
  23. package/dist/cpsat/rules/rules.types.d.ts +3 -0
  24. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  25. package/dist/cpsat/rules/scope.types.d.ts +1 -1
  26. package/dist/index.d.ts +5 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/schedule/cost.d.ts +204 -0
  31. package/dist/schedule/cost.d.ts.map +1 -0
  32. package/dist/schedule/cost.js +187 -0
  33. package/dist/schedule/cost.js.map +1 -0
  34. package/dist/schedule/coverage.d.ts +85 -0
  35. package/dist/schedule/coverage.d.ts.map +1 -0
  36. package/dist/schedule/coverage.js +33 -0
  37. package/dist/schedule/coverage.js.map +1 -0
  38. package/dist/schedule/definition.d.ts +227 -0
  39. package/dist/schedule/definition.d.ts.map +1 -0
  40. package/dist/{schedule.js → schedule/definition.js} +9 -673
  41. package/dist/schedule/definition.js.map +1 -0
  42. package/dist/schedule/index.d.ts +67 -0
  43. package/dist/schedule/index.d.ts.map +1 -0
  44. package/dist/schedule/index.js +69 -0
  45. package/dist/schedule/index.js.map +1 -0
  46. package/dist/schedule/rules.d.ts +353 -0
  47. package/dist/schedule/rules.d.ts.map +1 -0
  48. package/dist/schedule/rules.js +352 -0
  49. package/dist/schedule/rules.js.map +1 -0
  50. package/dist/schedule/shift-patterns.d.ts +34 -0
  51. package/dist/schedule/shift-patterns.d.ts.map +1 -0
  52. package/dist/schedule/shift-patterns.js +41 -0
  53. package/dist/schedule/shift-patterns.js.map +1 -0
  54. package/dist/schedule/time-periods.d.ts +69 -0
  55. package/dist/schedule/time-periods.d.ts.map +1 -0
  56. package/dist/schedule/time-periods.js +91 -0
  57. package/dist/schedule/time-periods.js.map +1 -0
  58. package/package.json +4 -9
  59. package/src/cpsat/rules/index.ts +3 -0
  60. package/src/cpsat/rules/max-days-week.ts +143 -0
  61. package/src/cpsat/rules/min-days-week.ts +120 -0
  62. package/src/cpsat/rules/must-assign.ts +108 -0
  63. package/src/cpsat/rules/registry.ts +6 -0
  64. package/src/cpsat/rules/rules.types.ts +3 -0
  65. package/src/cpsat/rules/scope.types.ts +1 -1
  66. package/src/index.ts +8 -3
  67. package/src/schedule/cost.ts +242 -0
  68. package/src/schedule/coverage.ts +135 -0
  69. package/src/schedule/definition.ts +958 -0
  70. package/src/schedule/index.ts +112 -0
  71. package/src/schedule/rules.ts +529 -0
  72. package/src/schedule/shift-patterns.ts +46 -0
  73. package/src/schedule/time-periods.ts +110 -0
  74. package/dist/llms.d.ts +0 -2
  75. package/dist/llms.d.ts.map +0 -1
  76. package/dist/llms.js +0 -3
  77. package/dist/llms.js.map +0 -1
  78. package/dist/schedule.d.ts +0 -917
  79. package/dist/schedule.d.ts.map +0 -1
  80. package/dist/schedule.js.map +0 -1
  81. package/llms.txt +0 -758
  82. package/src/llms.ts +0 -3
  83. package/src/schedule.ts +0 -1960
@@ -1,630 +1,15 @@
1
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
- * 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 "./cpsat/semantic-time.js";
58
- import { resolveDaysFromPeriod } from "./datetime.utils.js";
59
- import { ModelBuilder } from "./cpsat/model-builder.js";
60
- import { builtInCpsatRuleFactories } from "./cpsat/rules/registry.js";
61
- import { parseSolverResponse, resolveAssignments } from "./cpsat/response.js";
62
- import { calculateScheduleCost } from "./cpsat/cost.js";
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=schedule.js.map
659
+ //# sourceMappingURL=definition.js.map