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.
Files changed (231) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -27
  3. package/dist/client.d.ts +20 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +4 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/client.types.d.ts +9 -0
  8. package/dist/client.types.d.ts.map +1 -1
  9. package/dist/client.types.js +1 -0
  10. package/dist/client.types.js.map +1 -1
  11. package/dist/cpsat/model-builder.d.ts +9 -0
  12. package/dist/cpsat/model-builder.d.ts.map +1 -1
  13. package/dist/cpsat/model-builder.js +36 -34
  14. package/dist/cpsat/model-builder.js.map +1 -1
  15. package/dist/cpsat/response.d.ts +13 -1
  16. package/dist/cpsat/response.d.ts.map +1 -1
  17. package/dist/cpsat/response.js +4 -0
  18. package/dist/cpsat/response.js.map +1 -1
  19. package/dist/cpsat/rules/cost-utils.d.ts +11 -0
  20. package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
  21. package/dist/cpsat/rules/cost-utils.js +24 -0
  22. package/dist/cpsat/rules/cost-utils.js.map +1 -0
  23. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
  24. package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
  25. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
  26. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
  27. package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
  28. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
  29. package/dist/cpsat/rules/index.d.ts +3 -0
  30. package/dist/cpsat/rules/index.d.ts.map +1 -1
  31. package/dist/cpsat/rules/index.js +3 -0
  32. package/dist/cpsat/rules/index.js.map +1 -1
  33. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  34. package/dist/cpsat/rules/max-consecutive-days.js +16 -2
  35. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  36. package/dist/cpsat/rules/max-days-week.d.ts +44 -0
  37. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
  38. package/dist/cpsat/rules/max-days-week.js +95 -0
  39. package/dist/cpsat/rules/max-days-week.js.map +1 -0
  40. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  41. package/dist/cpsat/rules/max-hours-day.js +15 -2
  42. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  43. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  44. package/dist/cpsat/rules/max-hours-week.js +16 -2
  45. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  46. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  47. package/dist/cpsat/rules/max-shifts-day.js +15 -2
  48. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  49. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  50. package/dist/cpsat/rules/min-consecutive-days.js +15 -2
  51. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  52. package/dist/cpsat/rules/min-days-week.d.ts +34 -0
  53. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
  54. package/dist/cpsat/rules/min-days-week.js +84 -0
  55. package/dist/cpsat/rules/min-days-week.js.map +1 -0
  56. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  57. package/dist/cpsat/rules/min-hours-day.js +15 -2
  58. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  59. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  60. package/dist/cpsat/rules/min-hours-week.js +16 -2
  61. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  62. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  63. package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
  64. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  65. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
  66. package/dist/cpsat/rules/minimize-cost.js +2 -23
  67. package/dist/cpsat/rules/minimize-cost.js.map +1 -1
  68. package/dist/cpsat/rules/must-assign.d.ts +49 -0
  69. package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
  70. package/dist/cpsat/rules/must-assign.js +86 -0
  71. package/dist/cpsat/rules/must-assign.js.map +1 -0
  72. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
  73. package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
  74. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
  75. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
  76. package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
  77. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
  78. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
  79. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
  80. package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
  81. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
  82. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
  83. package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
  84. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
  85. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
  86. package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
  87. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
  88. package/dist/cpsat/rules/registry.d.ts +28 -2
  89. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  90. package/dist/cpsat/rules/registry.js +4 -1
  91. package/dist/cpsat/rules/registry.js.map +1 -1
  92. package/dist/cpsat/rules/resolver.js +2 -2
  93. package/dist/cpsat/rules/resolver.js.map +1 -1
  94. package/dist/cpsat/rules/rules.types.d.ts +3 -0
  95. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  96. package/dist/cpsat/rules/scope.types.d.ts +18 -1
  97. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  98. package/dist/cpsat/rules/scope.types.js +59 -16
  99. package/dist/cpsat/rules/scope.types.js.map +1 -1
  100. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
  101. package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
  102. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
  103. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  104. package/dist/cpsat/rules/time-off.js +6 -3
  105. package/dist/cpsat/rules/time-off.js.map +1 -1
  106. package/dist/cpsat/semantic-time.d.ts +44 -42
  107. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  108. package/dist/cpsat/semantic-time.js +64 -46
  109. package/dist/cpsat/semantic-time.js.map +1 -1
  110. package/dist/cpsat/types.d.ts +37 -27
  111. package/dist/cpsat/types.d.ts.map +1 -1
  112. package/dist/cpsat/utils.d.ts.map +1 -1
  113. package/dist/cpsat/utils.js +7 -12
  114. package/dist/cpsat/utils.js.map +1 -1
  115. package/dist/cpsat/validation-reporter.d.ts +10 -7
  116. package/dist/cpsat/validation-reporter.d.ts.map +1 -1
  117. package/dist/cpsat/validation-reporter.js +44 -72
  118. package/dist/cpsat/validation-reporter.js.map +1 -1
  119. package/dist/cpsat/validation.types.d.ts +54 -44
  120. package/dist/cpsat/validation.types.d.ts.map +1 -1
  121. package/dist/cpsat/validation.types.js +15 -10
  122. package/dist/cpsat/validation.types.js.map +1 -1
  123. package/dist/datetime.utils.d.ts +3 -203
  124. package/dist/datetime.utils.d.ts.map +1 -1
  125. package/dist/datetime.utils.js +1 -288
  126. package/dist/datetime.utils.js.map +1 -1
  127. package/dist/index.d.ts +14 -83
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +11 -83
  130. package/dist/index.js.map +1 -1
  131. package/dist/schedule/cost.d.ts +204 -0
  132. package/dist/schedule/cost.d.ts.map +1 -0
  133. package/dist/schedule/cost.js +187 -0
  134. package/dist/schedule/cost.js.map +1 -0
  135. package/dist/schedule/coverage.d.ts +85 -0
  136. package/dist/schedule/coverage.d.ts.map +1 -0
  137. package/dist/schedule/coverage.js +33 -0
  138. package/dist/schedule/coverage.js.map +1 -0
  139. package/dist/schedule/definition.d.ts +227 -0
  140. package/dist/schedule/definition.d.ts.map +1 -0
  141. package/dist/schedule/definition.js +659 -0
  142. package/dist/schedule/definition.js.map +1 -0
  143. package/dist/schedule/index.d.ts +67 -0
  144. package/dist/schedule/index.d.ts.map +1 -0
  145. package/dist/schedule/index.js +69 -0
  146. package/dist/schedule/index.js.map +1 -0
  147. package/dist/schedule/rules.d.ts +353 -0
  148. package/dist/schedule/rules.d.ts.map +1 -0
  149. package/dist/schedule/rules.js +352 -0
  150. package/dist/schedule/rules.js.map +1 -0
  151. package/dist/schedule/shift-patterns.d.ts +34 -0
  152. package/dist/schedule/shift-patterns.d.ts.map +1 -0
  153. package/dist/schedule/shift-patterns.js +41 -0
  154. package/dist/schedule/shift-patterns.js.map +1 -0
  155. package/dist/schedule/time-periods.d.ts +69 -0
  156. package/dist/schedule/time-periods.d.ts.map +1 -0
  157. package/dist/schedule/time-periods.js +91 -0
  158. package/dist/schedule/time-periods.js.map +1 -0
  159. package/dist/types.d.ts +14 -78
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/types.js.map +1 -1
  162. package/package.json +4 -9
  163. package/solver/src/solver/app.py +1 -1
  164. package/solver/src/solver/solver.py +7 -4
  165. package/src/client.ts +6 -8
  166. package/src/client.types.ts +9 -0
  167. package/src/cpsat/model-builder.ts +44 -35
  168. package/src/cpsat/response.ts +13 -1
  169. package/src/cpsat/rules/cost-utils.ts +25 -0
  170. package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
  171. package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
  172. package/src/cpsat/rules/index.ts +3 -0
  173. package/src/cpsat/rules/max-consecutive-days.ts +17 -0
  174. package/src/cpsat/rules/max-days-week.ts +143 -0
  175. package/src/cpsat/rules/max-hours-day.ts +21 -1
  176. package/src/cpsat/rules/max-hours-week.ts +22 -1
  177. package/src/cpsat/rules/max-shifts-day.ts +21 -1
  178. package/src/cpsat/rules/min-consecutive-days.ts +16 -1
  179. package/src/cpsat/rules/min-days-week.ts +120 -0
  180. package/src/cpsat/rules/min-hours-day.ts +16 -1
  181. package/src/cpsat/rules/min-hours-week.ts +17 -1
  182. package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
  183. package/src/cpsat/rules/minimize-cost.ts +2 -29
  184. package/src/cpsat/rules/must-assign.ts +108 -0
  185. package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
  186. package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
  187. package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
  188. package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
  189. package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
  190. package/src/cpsat/rules/registry.ts +8 -2
  191. package/src/cpsat/rules/resolver.ts +2 -2
  192. package/src/cpsat/rules/rules.types.ts +3 -0
  193. package/src/cpsat/rules/scope.types.ts +73 -20
  194. package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
  195. package/src/cpsat/rules/time-off.ts +6 -2
  196. package/src/cpsat/semantic-time.ts +115 -91
  197. package/src/cpsat/types.ts +37 -27
  198. package/src/cpsat/utils.ts +8 -12
  199. package/src/cpsat/validation-reporter.ts +51 -82
  200. package/src/cpsat/validation.types.ts +72 -47
  201. package/src/datetime.utils.ts +3 -334
  202. package/src/index.ts +35 -107
  203. package/src/schedule/cost.ts +242 -0
  204. package/src/schedule/coverage.ts +135 -0
  205. package/src/schedule/definition.ts +958 -0
  206. package/src/schedule/index.ts +112 -0
  207. package/src/schedule/rules.ts +529 -0
  208. package/src/schedule/shift-patterns.ts +46 -0
  209. package/src/schedule/time-periods.ts +110 -0
  210. package/src/types.ts +14 -88
  211. package/dist/errors.d.ts +0 -12
  212. package/dist/errors.d.ts.map +0 -1
  213. package/dist/errors.js +0 -17
  214. package/dist/errors.js.map +0 -1
  215. package/dist/llms.d.ts +0 -2
  216. package/dist/llms.d.ts.map +0 -1
  217. package/dist/llms.js +0 -3
  218. package/dist/llms.js.map +0 -1
  219. package/dist/schedule.d.ts +0 -724
  220. package/dist/schedule.d.ts.map +0 -1
  221. package/dist/schedule.js +0 -899
  222. package/dist/schedule.js.map +0 -1
  223. package/dist/validation.d.ts +0 -105
  224. package/dist/validation.d.ts.map +0 -1
  225. package/dist/validation.js +0 -130
  226. package/dist/validation.js.map +0 -1
  227. package/llms.txt +0 -925
  228. package/src/errors.ts +0 -17
  229. package/src/llms.ts +0 -3
  230. package/src/schedule.ts +0 -1419
  231. 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