dabke 0.83.0 → 0.84.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 (330) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +22 -27
  3. package/dist/client.d.ts +39 -3
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.schemas.d.ts +118 -18
  7. package/dist/client.schemas.d.ts.map +1 -1
  8. package/dist/client.schemas.js +110 -6
  9. package/dist/client.schemas.js.map +1 -1
  10. package/dist/client.types.d.ts +63 -7
  11. package/dist/client.types.d.ts.map +1 -1
  12. package/dist/client.types.js.map +1 -1
  13. package/dist/cpsat/cost.d.ts +5 -2
  14. package/dist/cpsat/cost.d.ts.map +1 -1
  15. package/dist/cpsat/cost.js +13 -6
  16. package/dist/cpsat/cost.js.map +1 -1
  17. package/dist/cpsat/model-builder.d.ts +42 -61
  18. package/dist/cpsat/model-builder.d.ts.map +1 -1
  19. package/dist/cpsat/model-builder.js +451 -56
  20. package/dist/cpsat/model-builder.js.map +1 -1
  21. package/dist/cpsat/response.d.ts +19 -7
  22. package/dist/cpsat/response.d.ts.map +1 -1
  23. package/dist/cpsat/response.js +48 -9
  24. package/dist/cpsat/response.js.map +1 -1
  25. package/dist/cpsat/rule-descriptor.d.ts +250 -0
  26. package/dist/cpsat/rule-descriptor.d.ts.map +1 -0
  27. package/dist/cpsat/rule-descriptor.js +51 -0
  28. package/dist/cpsat/rule-descriptor.js.map +1 -0
  29. package/dist/cpsat/rules/artifacts.d.ts +9 -0
  30. package/dist/cpsat/rules/artifacts.d.ts.map +1 -0
  31. package/dist/cpsat/rules/artifacts.js +31 -0
  32. package/dist/cpsat/rules/artifacts.js.map +1 -0
  33. package/dist/cpsat/rules/assign-together.d.ts +18 -20
  34. package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
  35. package/dist/cpsat/rules/assign-together.js +90 -54
  36. package/dist/cpsat/rules/assign-together.js.map +1 -1
  37. package/dist/cpsat/rules/assigned-day.d.ts +12 -0
  38. package/dist/cpsat/rules/assigned-day.d.ts.map +1 -0
  39. package/dist/cpsat/rules/assigned-day.js +43 -0
  40. package/dist/cpsat/rules/assigned-day.js.map +1 -0
  41. package/dist/cpsat/rules/assignment-priority.d.ts +15 -29
  42. package/dist/cpsat/rules/assignment-priority.d.ts.map +1 -1
  43. package/dist/cpsat/rules/assignment-priority.js +47 -50
  44. package/dist/cpsat/rules/assignment-priority.js.map +1 -1
  45. package/dist/cpsat/rules/assignment-terms.d.ts +6 -0
  46. package/dist/cpsat/rules/assignment-terms.d.ts.map +1 -0
  47. package/dist/cpsat/rules/assignment-terms.js +19 -0
  48. package/dist/cpsat/rules/assignment-terms.js.map +1 -0
  49. package/dist/cpsat/rules/concurrent-intervals.d.ts +19 -0
  50. package/dist/cpsat/rules/concurrent-intervals.d.ts.map +1 -0
  51. package/dist/cpsat/rules/concurrent-intervals.js +57 -0
  52. package/dist/cpsat/rules/concurrent-intervals.js.map +1 -0
  53. package/dist/cpsat/rules/cost-utils.d.ts +1 -3
  54. package/dist/cpsat/rules/cost-utils.d.ts.map +1 -1
  55. package/dist/cpsat/rules/cost-utils.js +0 -7
  56. package/dist/cpsat/rules/cost-utils.js.map +1 -1
  57. package/dist/cpsat/rules/day-cost-multiplier.d.ts +10 -10
  58. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
  59. package/dist/cpsat/rules/day-cost-multiplier.js +88 -77
  60. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
  61. package/dist/cpsat/rules/day-cost-surcharge.d.ts +10 -10
  62. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
  63. package/dist/cpsat/rules/day-cost-surcharge.js +76 -65
  64. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
  65. package/dist/cpsat/rules/index.d.ts +30 -24
  66. package/dist/cpsat/rules/index.d.ts.map +1 -1
  67. package/dist/cpsat/rules/index.js +30 -24
  68. package/dist/cpsat/rules/index.js.map +1 -1
  69. package/dist/cpsat/rules/location-preference.d.ts +14 -21
  70. package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
  71. package/dist/cpsat/rules/location-preference.js +45 -45
  72. package/dist/cpsat/rules/location-preference.js.map +1 -1
  73. package/dist/cpsat/rules/max-concurrent-assignments.d.ts +46 -0
  74. package/dist/cpsat/rules/max-concurrent-assignments.d.ts.map +1 -0
  75. package/dist/cpsat/rules/max-concurrent-assignments.js +121 -0
  76. package/dist/cpsat/rules/max-concurrent-assignments.js.map +1 -0
  77. package/dist/cpsat/rules/max-consecutive-days.d.ts +18 -17
  78. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  79. package/dist/cpsat/rules/max-consecutive-days.js +67 -66
  80. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  81. package/dist/cpsat/rules/max-days-of-week-per-period.d.ts +45 -0
  82. package/dist/cpsat/rules/max-days-of-week-per-period.d.ts.map +1 -0
  83. package/dist/cpsat/rules/max-days-of-week-per-period.js +95 -0
  84. package/dist/cpsat/rules/max-days-of-week-per-period.js.map +1 -0
  85. package/dist/cpsat/rules/max-days-week.d.ts +17 -31
  86. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -1
  87. package/dist/cpsat/rules/max-days-week.js +69 -81
  88. package/dist/cpsat/rules/max-days-week.js.map +1 -1
  89. package/dist/cpsat/rules/max-hours-day.d.ts +14 -31
  90. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  91. package/dist/cpsat/rules/max-hours-day.js +56 -72
  92. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  93. package/dist/cpsat/rules/max-hours-week.d.ts +15 -31
  94. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  95. package/dist/cpsat/rules/max-hours-week.js +60 -75
  96. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  97. package/dist/cpsat/rules/max-shifts-day.d.ts +17 -33
  98. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  99. package/dist/cpsat/rules/max-shifts-day.js +57 -73
  100. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  101. package/dist/cpsat/rules/min-consecutive-days.d.ts +18 -18
  102. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  103. package/dist/cpsat/rules/min-consecutive-days.js +123 -94
  104. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  105. package/dist/cpsat/rules/min-days-of-week-per-period.d.ts +35 -0
  106. package/dist/cpsat/rules/min-days-of-week-per-period.d.ts.map +1 -0
  107. package/dist/cpsat/rules/min-days-of-week-per-period.js +108 -0
  108. package/dist/cpsat/rules/min-days-of-week-per-period.js.map +1 -0
  109. package/dist/cpsat/rules/min-days-week.d.ts +13 -26
  110. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -1
  111. package/dist/cpsat/rules/min-days-week.js +85 -74
  112. package/dist/cpsat/rules/min-days-week.js.map +1 -1
  113. package/dist/cpsat/rules/min-hours-day.d.ts +14 -17
  114. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  115. package/dist/cpsat/rules/min-hours-day.js +72 -54
  116. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  117. package/dist/cpsat/rules/min-hours-week.d.ts +15 -18
  118. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  119. package/dist/cpsat/rules/min-hours-week.js +75 -57
  120. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  121. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +14 -17
  122. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  123. package/dist/cpsat/rules/min-rest-between-shifts.js +148 -138
  124. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  125. package/dist/cpsat/rules/minimize-cost.d.ts +2 -18
  126. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
  127. package/dist/cpsat/rules/minimize-cost.js +151 -163
  128. package/dist/cpsat/rules/minimize-cost.js.map +1 -1
  129. package/dist/cpsat/rules/must-assign.d.ts +5 -42
  130. package/dist/cpsat/rules/must-assign.d.ts.map +1 -1
  131. package/dist/cpsat/rules/must-assign.js +43 -75
  132. package/dist/cpsat/rules/must-assign.js.map +1 -1
  133. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts +6 -10
  134. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
  135. package/dist/cpsat/rules/overtime-daily-multiplier.js +118 -112
  136. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
  137. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts +6 -11
  138. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
  139. package/dist/cpsat/rules/overtime-daily-surcharge.js +104 -99
  140. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
  141. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +9 -21
  142. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
  143. package/dist/cpsat/rules/overtime-tiered-multiplier.js +181 -199
  144. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
  145. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts +13 -12
  146. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
  147. package/dist/cpsat/rules/overtime-weekly-multiplier.js +143 -143
  148. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
  149. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts +7 -11
  150. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
  151. package/dist/cpsat/rules/overtime-weekly-surcharge.js +124 -119
  152. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
  153. package/dist/cpsat/rules/pattern-eligibility.d.ts +6 -0
  154. package/dist/cpsat/rules/pattern-eligibility.d.ts.map +1 -0
  155. package/dist/cpsat/rules/pattern-eligibility.js +16 -0
  156. package/dist/cpsat/rules/pattern-eligibility.js.map +1 -0
  157. package/dist/cpsat/rules/pattern-time.d.ts +5 -0
  158. package/dist/cpsat/rules/pattern-time.d.ts.map +1 -0
  159. package/dist/cpsat/rules/pattern-time.js +20 -0
  160. package/dist/cpsat/rules/pattern-time.js.map +1 -0
  161. package/dist/cpsat/rules/registry.d.ts +217 -30
  162. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  163. package/dist/cpsat/rules/registry.js +98 -34
  164. package/dist/cpsat/rules/registry.js.map +1 -1
  165. package/dist/cpsat/rules/resolver.d.ts +9 -7
  166. package/dist/cpsat/rules/resolver.d.ts.map +1 -1
  167. package/dist/cpsat/rules/resolver.js +19 -18
  168. package/dist/cpsat/rules/resolver.js.map +1 -1
  169. package/dist/cpsat/rules/role-preference.d.ts +24 -0
  170. package/dist/cpsat/rules/role-preference.d.ts.map +1 -0
  171. package/dist/cpsat/rules/role-preference.js +72 -0
  172. package/dist/cpsat/rules/role-preference.js.map +1 -0
  173. package/dist/cpsat/rules/rules.types.d.ts +90 -17
  174. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  175. package/dist/cpsat/rules/scope.types.d.ts +24 -11
  176. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  177. package/dist/cpsat/rules/scope.types.js +14 -19
  178. package/dist/cpsat/rules/scope.types.js.map +1 -1
  179. package/dist/cpsat/rules/target-days-week.d.ts +31 -0
  180. package/dist/cpsat/rules/target-days-week.d.ts.map +1 -0
  181. package/dist/cpsat/rules/target-days-week.js +80 -0
  182. package/dist/cpsat/rules/target-days-week.js.map +1 -0
  183. package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts +33 -0
  184. package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts.map +1 -0
  185. package/dist/cpsat/rules/target-peak-concurrent-assignments.js +146 -0
  186. package/dist/cpsat/rules/target-peak-concurrent-assignments.js.map +1 -0
  187. package/dist/cpsat/rules/time-cost-surcharge.d.ts +15 -14
  188. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
  189. package/dist/cpsat/rules/time-cost-surcharge.js +82 -96
  190. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
  191. package/dist/cpsat/rules/time-off.d.ts +21 -57
  192. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  193. package/dist/cpsat/rules/time-off.js +114 -127
  194. package/dist/cpsat/rules/time-off.js.map +1 -1
  195. package/dist/cpsat/rules/variables.d.ts +4 -0
  196. package/dist/cpsat/rules/variables.d.ts.map +1 -0
  197. package/dist/cpsat/rules/variables.js +10 -0
  198. package/dist/cpsat/rules/variables.js.map +1 -0
  199. package/dist/cpsat/rules.d.ts +20 -3
  200. package/dist/cpsat/rules.d.ts.map +1 -1
  201. package/dist/cpsat/rules.js +19 -2
  202. package/dist/cpsat/rules.js.map +1 -1
  203. package/dist/cpsat/semantic-time.d.ts +7 -6
  204. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  205. package/dist/cpsat/semantic-time.js +15 -28
  206. package/dist/cpsat/semantic-time.js.map +1 -1
  207. package/dist/cpsat/types.d.ts +47 -4
  208. package/dist/cpsat/types.d.ts.map +1 -1
  209. package/dist/cpsat/types.js +24 -1
  210. package/dist/cpsat/types.js.map +1 -1
  211. package/dist/cpsat/utils.d.ts +23 -6
  212. package/dist/cpsat/utils.d.ts.map +1 -1
  213. package/dist/cpsat/utils.js +76 -15
  214. package/dist/cpsat/utils.js.map +1 -1
  215. package/dist/cpsat/validation-reporter.d.ts +6 -2
  216. package/dist/cpsat/validation-reporter.d.ts.map +1 -1
  217. package/dist/cpsat/validation-reporter.js +122 -10
  218. package/dist/cpsat/validation-reporter.js.map +1 -1
  219. package/dist/cpsat/validation.types.d.ts +21 -10
  220. package/dist/cpsat/validation.types.d.ts.map +1 -1
  221. package/dist/datetime.utils.d.ts +7 -13
  222. package/dist/datetime.utils.d.ts.map +1 -1
  223. package/dist/datetime.utils.js +29 -36
  224. package/dist/datetime.utils.js.map +1 -1
  225. package/dist/index.d.ts +12 -8
  226. package/dist/index.d.ts.map +1 -1
  227. package/dist/index.js +8 -3
  228. package/dist/index.js.map +1 -1
  229. package/dist/schedule/cost.d.ts +48 -60
  230. package/dist/schedule/cost.d.ts.map +1 -1
  231. package/dist/schedule/cost.js +15 -51
  232. package/dist/schedule/cost.js.map +1 -1
  233. package/dist/schedule/coverage.d.ts +57 -28
  234. package/dist/schedule/coverage.d.ts.map +1 -1
  235. package/dist/schedule/coverage.js +24 -1
  236. package/dist/schedule/coverage.js.map +1 -1
  237. package/dist/schedule/definition.d.ts +138 -19
  238. package/dist/schedule/definition.d.ts.map +1 -1
  239. package/dist/schedule/definition.js +99 -90
  240. package/dist/schedule/definition.js.map +1 -1
  241. package/dist/schedule/index.d.ts +4 -4
  242. package/dist/schedule/index.d.ts.map +1 -1
  243. package/dist/schedule/index.js +2 -2
  244. package/dist/schedule/index.js.map +1 -1
  245. package/dist/schedule/rules.d.ts +438 -113
  246. package/dist/schedule/rules.d.ts.map +1 -1
  247. package/dist/schedule/rules.js +310 -108
  248. package/dist/schedule/rules.js.map +1 -1
  249. package/dist/schedule/shift-patterns.d.ts +0 -17
  250. package/dist/schedule/shift-patterns.d.ts.map +1 -1
  251. package/dist/schedule/shift-patterns.js +0 -17
  252. package/dist/schedule/shift-patterns.js.map +1 -1
  253. package/dist/schedule/time-periods.d.ts +37 -27
  254. package/dist/schedule/time-periods.d.ts.map +1 -1
  255. package/dist/schedule/time-periods.js +37 -27
  256. package/dist/schedule/time-periods.js.map +1 -1
  257. package/dist/types.d.ts +67 -3
  258. package/dist/types.d.ts.map +1 -1
  259. package/dist/types.js +64 -0
  260. package/dist/types.js.map +1 -1
  261. package/package.json +8 -9
  262. package/solver/src/solver/models.py +41 -4
  263. package/solver/src/solver/solver.py +546 -157
  264. package/solver/tests/test_solver.py +363 -7
  265. package/src/client.schemas.ts +127 -10
  266. package/src/client.ts +3 -0
  267. package/src/client.types.ts +73 -7
  268. package/src/cpsat/cost.ts +17 -7
  269. package/src/cpsat/model-builder.ts +614 -129
  270. package/src/cpsat/response.ts +57 -13
  271. package/src/cpsat/rule-descriptor.ts +311 -0
  272. package/src/cpsat/rules/artifacts.ts +56 -0
  273. package/src/cpsat/rules/assign-together.ts +104 -69
  274. package/src/cpsat/rules/assigned-day.ts +70 -0
  275. package/src/cpsat/rules/assignment-priority.ts +54 -58
  276. package/src/cpsat/rules/assignment-terms.ts +43 -0
  277. package/src/cpsat/rules/concurrent-intervals.ts +93 -0
  278. package/src/cpsat/rules/cost-utils.ts +1 -9
  279. package/src/cpsat/rules/day-cost-multiplier.ts +101 -81
  280. package/src/cpsat/rules/day-cost-surcharge.ts +91 -70
  281. package/src/cpsat/rules/index.ts +30 -24
  282. package/src/cpsat/rules/location-preference.ts +47 -52
  283. package/src/cpsat/rules/max-concurrent-assignments.ts +170 -0
  284. package/src/cpsat/rules/max-consecutive-days.ts +74 -91
  285. package/src/cpsat/rules/max-days-of-week-per-period.ts +127 -0
  286. package/src/cpsat/rules/max-days-week.ts +89 -115
  287. package/src/cpsat/rules/max-hours-day.ts +64 -84
  288. package/src/cpsat/rules/max-hours-week.ts +74 -90
  289. package/src/cpsat/rules/max-shifts-day.ts +65 -85
  290. package/src/cpsat/rules/min-consecutive-days.ts +161 -127
  291. package/src/cpsat/rules/min-days-of-week-per-period.ts +144 -0
  292. package/src/cpsat/rules/min-days-week.ts +93 -91
  293. package/src/cpsat/rules/min-hours-day.ts +79 -58
  294. package/src/cpsat/rules/min-hours-week.ts +87 -62
  295. package/src/cpsat/rules/min-rest-between-shifts.ts +191 -178
  296. package/src/cpsat/rules/minimize-cost.ts +175 -188
  297. package/src/cpsat/rules/must-assign.ts +44 -81
  298. package/src/cpsat/rules/overtime-daily-multiplier.ts +158 -140
  299. package/src/cpsat/rules/overtime-daily-surcharge.ts +141 -126
  300. package/src/cpsat/rules/overtime-tiered-multiplier.ts +234 -253
  301. package/src/cpsat/rules/overtime-weekly-multiplier.ts +183 -175
  302. package/src/cpsat/rules/overtime-weekly-surcharge.ts +161 -150
  303. package/src/cpsat/rules/pattern-eligibility.ts +26 -0
  304. package/src/cpsat/rules/pattern-time.ts +30 -0
  305. package/src/cpsat/rules/registry.ts +140 -62
  306. package/src/cpsat/rules/resolver.ts +58 -30
  307. package/src/cpsat/rules/role-preference.ts +92 -0
  308. package/src/cpsat/rules/rules.types.ts +89 -17
  309. package/src/cpsat/rules/scope.types.ts +31 -26
  310. package/src/cpsat/rules/target-days-week.ts +105 -0
  311. package/src/cpsat/rules/target-peak-concurrent-assignments.ts +212 -0
  312. package/src/cpsat/rules/time-cost-surcharge.ts +116 -122
  313. package/src/cpsat/rules/time-off.ts +154 -163
  314. package/src/cpsat/rules/variables.ts +11 -0
  315. package/src/cpsat/rules.ts +43 -17
  316. package/src/cpsat/semantic-time.ts +29 -43
  317. package/src/cpsat/types.ts +53 -4
  318. package/src/cpsat/utils.ts +92 -20
  319. package/src/cpsat/validation-reporter.ts +170 -19
  320. package/src/cpsat/validation.types.ts +22 -11
  321. package/src/datetime.utils.ts +45 -44
  322. package/src/index.ts +64 -12
  323. package/src/schedule/cost.ts +40 -61
  324. package/src/schedule/coverage.ts +57 -28
  325. package/src/schedule/definition.ts +565 -76
  326. package/src/schedule/index.ts +23 -3
  327. package/src/schedule/rules.ts +631 -132
  328. package/src/schedule/shift-patterns.ts +0 -17
  329. package/src/schedule/time-periods.ts +37 -27
  330. package/src/types.ts +114 -2
@@ -1,12 +1,71 @@
1
1
  /**
2
2
  * Scheduling rules: constraints, preferences, and time-off.
3
3
  *
4
+ * Rules answer "what schedules are allowed or preferred?". Use
5
+ * {@link cover} for lower-bound demand, then use rules for caps, limits, rest
6
+ * periods, time off, fairness, and other constraints on assignments.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * rules: [
11
+ * // Hour and day limits
12
+ * maxHoursPerDay(10),
13
+ * maxHoursPerWeek(48),
14
+ * maxHoursPerWeek(20, { appliesTo: "part-time" }),
15
+ * maxDaysPerWeek(5, { priority: "HIGH" }),
16
+ * targetDaysPerWeek(4, { appliesTo: "full-time", priority: "HIGH" }),
17
+ * minDaysPerWeek(4, { appliesTo: "full-time", priority: "HIGH" }),
18
+ * maxShiftsPerDay(1),
19
+ * maxConcurrentAssignments(5, { appliesTo: "chair_stylist" }),
20
+ * targetPeakConcurrentAssignments(5, { appliesTo: "chair_stylist", dayOfWeek: ["thursday"], priority: "HIGH" }),
21
+ * minRestBetweenShifts(11),
22
+ *
23
+ * // Staffing obligations and preferences
24
+ * mustAssign({ appliesTo: ["diana", "yavuz"] }),
25
+ * timeOff({ appliesTo: "alice", dateRange: { start: "2024-02-01", end: "2024-02-05" } }),
26
+ * timeOff({ appliesTo: "student", dayOfWeek: ["wednesday"], from: t(14) }),
27
+ * preferAssignment({ appliesTo: "waiter" }),
28
+ * avoidAssignment({ appliesTo: "student", dayOfWeek: weekdays }),
29
+ * assignTogether(["alice", "bob"]),
30
+ * ]
31
+ * ```
32
+ *
33
+ * Custom rules can stay type-safe too:
34
+ *
35
+ * ```typescript
36
+ * const debugRuleRegistry = createCpsatRuleRegistry({
37
+ * debug: defineRuleDescriptor({
38
+ * name: "debug",
39
+ * schema: z.object({ flag: z.boolean() }),
40
+ * compile() {
41
+ * return { rule: "debug", artifacts: [] };
42
+ * },
43
+ * }),
44
+ * });
45
+ *
46
+ * const defineDebugRule = defineRuleFor(debugRuleRegistry);
47
+ *
48
+ * schedule({
49
+ * roleIds: ["waiter"],
50
+ * times: { lunch: time({ startTime: t(12), endTime: t(15) }) },
51
+ * coverage: [cover("lunch", "waiter", 1)],
52
+ * shiftPatterns: [shift("lunch", t(12), t(15))],
53
+ * ruleRegistry: debugRuleRegistry,
54
+ * rules: [defineDebugRule("debug", { flag: true })],
55
+ * });
56
+ * ```
57
+ *
4
58
  * @module
5
59
  */
6
60
 
7
61
  import type { DayOfWeek, TimeOfDay } from "../types.js";
8
- import type { Priority } from "../cpsat/types.js";
9
- import type { CpsatRuleName, CpsatRuleConfigEntry } from "../cpsat/rules/rules.types.js";
62
+ import type { Priority, SoftPriority } from "../cpsat/types.js";
63
+ import type {
64
+ BuiltInCpsatRuleRegistry,
65
+ CpsatRuleConfigEntryFor,
66
+ CpsatRuleName,
67
+ CpsatRuleRegistry,
68
+ } from "../cpsat/rules/rules.types.js";
10
69
  import type { RecurringPeriod } from "../cpsat/rules/scope.types.js";
11
70
 
12
71
  // ============================================================================
@@ -20,6 +79,13 @@ import type { RecurringPeriod } from "../cpsat/rules/scope.types.js";
20
79
  * Default priority is `MANDATORY`. Use `appliesTo` to scope to a
21
80
  * role, skill, or member ID. Use time scoping options (`dayOfWeek`,
22
81
  * `dateRange`, `dates`) to limit when the rule applies.
82
+ * Scope answers where a rule is evaluated. It does not change the rule's core
83
+ * meaning. For example, `maxDaysPerWeek(4, { dayOfWeek: ["monday"] })` is still
84
+ * a day-cap concept, just scoped to matching scheduling days. Priority is just
85
+ * as important as scope: scope decides where a rule applies, and priority decides how hard the solver should enforce it. Use
86
+ * `"MANDATORY"` for requirements the schedule cannot violate, and softer
87
+ * priorities for things the requirements describe as preferred, ideal, or
88
+ * "where possible".
23
89
  * Not all rules support all scoping options. Entity-only rules
24
90
  * (e.g., {@link maxConsecutiveDays}) ignore time scoping.
25
91
  *
@@ -46,7 +112,8 @@ export interface RuleOptions {
46
112
  * @remarks
47
113
  * Used by rules whose semantics are inherently per-day or per-week
48
114
  * (e.g., {@link minHoursPerDay}, {@link maxConsecutiveDays}) and cannot
49
- * be meaningfully restricted to a date range or day of week.
115
+ * be meaningfully restricted to a date range or day of week. `priority`
116
+ * still controls whether the rule is a hard requirement or a softer target.
50
117
  *
51
118
  * @category Rules
52
119
  */
@@ -96,6 +163,63 @@ export interface AssignTogetherOptions {
96
163
  priority?: Priority;
97
164
  }
98
165
 
166
+ /**
167
+ * Options for {@link maxConcurrentAssignments}.
168
+ *
169
+ * @remarks
170
+ * Use this when the business has a hard productive cap that is independent from
171
+ * its coverage floor, for example when only a fixed number of assignments can
172
+ * overlap productively at once. Omit `startTime`/`endTime` to cap concurrent
173
+ * assignments across the full day. Provide both together to limit concurrency
174
+ * only within a specific time window.
175
+ *
176
+ * @category Rules
177
+ */
178
+ export interface MaxConcurrentAssignmentsOptions extends RuleOptions {
179
+ /** Optional start of the capped time window. */
180
+ startTime?: TimeOfDay;
181
+ /** Optional end of the capped time window. */
182
+ endTime?: TimeOfDay;
183
+ }
184
+
185
+ /**
186
+ * Options for {@link targetDaysPerWeek}.
187
+ *
188
+ * @remarks
189
+ * Use this for stated weekly patterns like "works a 4-day week" when that
190
+ * pattern should remain flexible. This is soft-only by design. Pair it with
191
+ * {@link maxDaysPerWeek} when the same number is also a hard cap, and use
192
+ * {@link minDaysPerWeek} only when the requirements explicitly require a minimum.
193
+ *
194
+ * @category Rules
195
+ */
196
+ export interface TargetDaysPerWeekOptions extends Omit<EntityOnlyRuleOptions, "priority"> {
197
+ /** Soft only. Defaults to `"HIGH"`. */
198
+ priority?: SoftPriority;
199
+ }
200
+
201
+ /**
202
+ * Options for {@link targetPeakConcurrentAssignments}.
203
+ *
204
+ * @remarks
205
+ * Use this when the business wants to hit a peak concurrency target within a
206
+ * day, without making that target a lower bound across the whole day. This is
207
+ * soft-only by design. Unlike {@link cover}, this helper does not establish a
208
+ * minimum that must hold throughout a semantic time. Instead it shifts the
209
+ * solution toward reaching the desired peak within the scoped days. Use
210
+ * {@link cover} for a whole-window minimum, and pair
211
+ * this helper with
212
+ * {@link maxConcurrentAssignments} when the same number is also a hard cap.
213
+ * This is useful for language like "fill all 5 chairs on peak days" or
214
+ * "ideally reach full capacity during the busy point".
215
+ *
216
+ * @category Rules
217
+ */
218
+ export interface TargetPeakConcurrentAssignmentsOptions extends Omit<RuleOptions, "priority"> {
219
+ /** Soft only. Defaults to `"HIGH"`. */
220
+ priority?: SoftPriority;
221
+ }
222
+
99
223
  // ============================================================================
100
224
  // Rule Entry
101
225
  // ============================================================================
@@ -114,54 +238,177 @@ export interface RuleResolveContext {
114
238
  }
115
239
 
116
240
  // Internal rule entry type
117
- interface RuleEntryBase {
241
+ interface RuleEntryBase<Name extends string = string> {
118
242
  readonly _type: "rule";
119
- readonly _rule: string;
243
+ readonly _rule: Name;
120
244
  /**
121
245
  * Optional custom resolver. When present, `resolveRules()` calls this
122
246
  * instead of the default translation path. Built-in rules that need
123
247
  * special field mapping (e.g., `timeOff`, `assignTogether`) attach one;
124
248
  * all other rules use the default resolver.
125
249
  */
126
- readonly _resolve?: (ctx: RuleResolveContext) => Record<string, unknown> & { name: string };
250
+ readonly _resolve?: (ctx: RuleResolveContext) => Record<string, unknown> & { name: Name };
127
251
  }
128
252
 
129
253
  /**
130
- * An opaque rule entry returned by rule functions.
254
+ * A typed rule entry returned by rule helpers and {@link defineRule}.
131
255
  *
132
256
  * @remarks
133
- * Pass these directly into the `rules` array of {@link ScheduleConfig}.
134
- * The internal fields are resolved during compilation.
257
+ * Pass these directly into the `rules` array of {@link ScheduleConfig}. The
258
+ * internal fields are resolved during compilation.
259
+ *
260
+ * @category Rules
135
261
  */
136
- export type RuleEntry = RuleEntryBase & Record<string, unknown>;
262
+ export type RuleEntry<
263
+ Name extends string = string,
264
+ Fields extends object = Record<string, unknown>,
265
+ > = RuleEntryBase<Name> & Fields & Record<string, unknown>;
266
+
267
+ type TypedRuleEntryFor<Registry extends CpsatRuleRegistry> = {
268
+ [K in keyof Registry & string]: RuleEntry<
269
+ K,
270
+ Omit<CpsatRuleConfigEntryFor<Pick<Registry, K>>, "name">
271
+ >;
272
+ }[keyof Registry & string];
137
273
 
274
+ /**
275
+ * A rule entry accepted by {@link ScheduleConfig.rules}.
276
+ *
277
+ * @remarks
278
+ * Built-in rule helpers return entries that are always valid. Custom rules created
279
+ * via {@link defineRule} are checked against the active rule registry when passed
280
+ * to {@link schedule} or {@link partialSchedule}. For registry-bound authoring,
281
+ * prefer {@link defineRuleFor}.
282
+ *
283
+ * @category Rules
284
+ */
285
+ export type ScheduleRuleEntry<TRuleRegistry extends CpsatRuleRegistry = BuiltInCpsatRuleRegistry> =
286
+ TypedRuleEntryFor<TRuleRegistry>;
287
+
288
+ type SanitizedRuleFields<Fields extends object> = Omit<Fields, "_type" | "_rule">;
289
+ type BuiltInResolvedRuleConfig<Name extends CpsatRuleName> = CpsatRuleConfigEntryFor<
290
+ Pick<BuiltInCpsatRuleRegistry, Name>
291
+ >;
292
+ type PeriodLength = { type: "weeks"; value: number } | { type: "months"; value: number };
293
+ type ResolvedEntityScope = {
294
+ memberIds?: [string, ...string[]];
295
+ roleIds?: [string, ...string[]];
296
+ skillIds?: [string, ...string[]];
297
+ };
298
+ type ResolvedTimeOffRuleConfig = {
299
+ name: "time-off";
300
+ priority: Priority;
301
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
302
+ dateRange?: { start: string; end: string };
303
+ specificDates?: string[];
304
+ recurringPeriods?: [RecurringPeriod, ...RecurringPeriod[]];
305
+ startTime?: TimeOfDay;
306
+ endTime?: TimeOfDay;
307
+ } & ResolvedEntityScope;
308
+ type ResolvedDaysOfWeekPerPeriodRuleConfig<
309
+ Name extends "max-days-of-week-per-period" | "min-days-of-week-per-period",
310
+ > = {
311
+ name: Name;
312
+ days: number;
313
+ dayOfWeek: readonly [DayOfWeek, ...DayOfWeek[]];
314
+ period: PeriodLength;
315
+ priority: Priority;
316
+ } & ResolvedEntityScope;
317
+ type TimeOffRuleEntry = RuleEntry<"time-off", TimeOffOptions>;
318
+ type AssignTogetherRuleEntry = RuleEntry<
319
+ "assign-together",
320
+ { members: [string, string, ...string[]] } & AssignTogetherOptions
321
+ >;
322
+ type DaysOfWeekPerPeriodRuleFields = {
323
+ days: number;
324
+ dayOfWeek: readonly [DayOfWeek, ...DayOfWeek[]];
325
+ period: PeriodLength;
326
+ appliesTo?: string | string[];
327
+ priority?: Priority;
328
+ };
329
+ type MaxDaysOfWeekPerPeriodRuleEntry = RuleEntry<
330
+ "max-days-of-week-per-period",
331
+ DaysOfWeekPerPeriodRuleFields
332
+ >;
333
+ type MinDaysOfWeekPerPeriodRuleEntry = RuleEntry<
334
+ "min-days-of-week-per-period",
335
+ DaysOfWeekPerPeriodRuleFields
336
+ >;
138
337
  /**
139
338
  * Creates a rule entry for use in {@link ScheduleConfig.rules}.
140
339
  *
141
- * Built-in rules use the helpers (`maxHoursPerDay`, `timeOff`, etc.).
142
- * Custom rules can use `defineRule` to create entries that plug into the
143
- * same resolution and compilation pipeline.
340
+ * Built-in rules use the helpers (`maxHoursPerDay`, `timeOff`, etc.). Custom
341
+ * rules can use `defineRule` to create entries that plug into the same
342
+ * resolution and compilation pipeline.
144
343
  *
145
- * @param name - Rule name. Must match a key in the rule factory registry.
344
+ * @remarks
345
+ * The returned entry always preserves its rule name and fields. To validate custom
346
+ * rules at the point of creation, use {@link defineRuleFor} to bind `defineRule`
347
+ * to a specific registry.
348
+ *
349
+ * @param name - Rule name. Must match a key in the active rule registry.
146
350
  * @param fields - Rule-specific configuration fields.
147
351
  * @param resolve - Optional custom resolver. When omitted, the default
148
352
  * resolution applies: `appliesTo` is mapped to `roleIds`/`skillIds`/`memberIds`,
149
353
  * `dates` is renamed to `specificDates`, and all other fields pass through.
150
354
  */
151
- export function defineRule(
152
- name: string,
153
- fields: Record<string, unknown>,
154
- resolve?: (ctx: RuleResolveContext) => Record<string, unknown> & { name: string },
155
- ): RuleEntry {
156
- const { _type: _, _rule: __, ...safeFields } = fields;
157
- const entry: RuleEntry = { _type: "rule", _rule: name, ...safeFields };
355
+ export function defineRule<const Name extends string, const Fields extends object>(
356
+ name: Name,
357
+ fields: Fields,
358
+ resolve?: (ctx: RuleResolveContext) => Record<string, unknown> & { name: Name },
359
+ ): RuleEntry<Name, SanitizedRuleFields<Fields>>;
360
+ export function defineRule<const Name extends string, const Fields extends object>(
361
+ name: Name,
362
+ fields: Fields,
363
+ resolve?: (ctx: RuleResolveContext) => Record<string, unknown> & { name: Name },
364
+ ): RuleEntry<Name, SanitizedRuleFields<Fields>> {
365
+ const safeFields = { ...fields } as SanitizedRuleFields<Fields> & {
366
+ _type?: unknown;
367
+ _rule?: unknown;
368
+ };
369
+ delete safeFields._type;
370
+ delete safeFields._rule;
371
+
372
+ const entry: RuleEntry<Name, SanitizedRuleFields<Fields>> = {
373
+ _type: "rule",
374
+ _rule: name,
375
+ ...safeFields,
376
+ };
158
377
  if (resolve) {
159
378
  Object.defineProperty(entry, "_resolve", { value: resolve, enumerable: false });
160
379
  }
161
380
  return entry;
162
381
  }
163
382
 
164
- function makeRule(rule: CpsatRuleName, fields: Record<string, unknown>): RuleEntry {
383
+ /**
384
+ * Creates a registry-bound `defineRule` helper.
385
+ *
386
+ * @remarks
387
+ * Use this when authoring custom rules directly and you want immediate
388
+ * compile-time validation of the rule name and config fields against a specific
389
+ * rule registry.
390
+ *
391
+ * @category Rules
392
+ */
393
+ export function defineRuleFor<TRuleRegistry extends CpsatRuleRegistry>(
394
+ ruleRegistry: TRuleRegistry,
395
+ ): <const Name extends keyof TRuleRegistry & string>(
396
+ name: Name,
397
+ fields: Omit<CpsatRuleConfigEntryFor<Pick<TRuleRegistry, Name>>, "name">,
398
+ resolve?: (ctx: RuleResolveContext) => CpsatRuleConfigEntryFor<Pick<TRuleRegistry, Name>>,
399
+ ) => RuleEntry<
400
+ Name,
401
+ SanitizedRuleFields<Omit<CpsatRuleConfigEntryFor<Pick<TRuleRegistry, Name>>, "name">>
402
+ > {
403
+ void ruleRegistry;
404
+
405
+ return (name, fields, resolve) => defineRule(name, fields, resolve);
406
+ }
407
+
408
+ function makeRule<const Name extends CpsatRuleName, const Fields extends object>(
409
+ rule: Name,
410
+ fields: Fields,
411
+ ): RuleEntry<Name, SanitizedRuleFields<Fields>> {
165
412
  return defineRule(rule, fields);
166
413
  }
167
414
 
@@ -172,174 +419,326 @@ function makeRule(rule: CpsatRuleName, fields: Record<string, unknown>): RuleEnt
172
419
  /**
173
420
  * Limits hours per day.
174
421
  *
175
- * @example
176
- * ```typescript
177
- * maxHoursPerDay(10)
178
- * maxHoursPerDay(4, { appliesTo: "student", dayOfWeek: weekdays })
179
- * ```
422
+ * @remarks
423
+ * Use this when the requirements or operating constraints explicitly impose a
424
+ * daily hours cap. Do not infer it just from the chosen shift pattern lengths.
180
425
  *
181
426
  * @category Rules
182
427
  */
183
- export function maxHoursPerDay(hours: number, opts?: RuleOptions): RuleEntry {
428
+ export function maxHoursPerDay(
429
+ hours: number,
430
+ opts?: RuleOptions,
431
+ ): RuleEntry<"max-hours-day", { hours: number } & RuleOptions> {
184
432
  return makeRule("max-hours-day", { hours, ...opts });
185
433
  }
186
434
 
187
435
  /**
188
436
  * Limits hours per scheduling week.
189
437
  *
190
- * @example
191
- * ```typescript
192
- * maxHoursPerWeek(48)
193
- * maxHoursPerWeek(20, { appliesTo: "student" })
194
- * ```
438
+ * @remarks
439
+ * Use this when the requirements or operating constraints explicitly impose a
440
+ * weekly hours cap. Do not infer it just from a stated working pattern such
441
+ * as "4-day week" unless the requirements also make the hour limit explicit.
195
442
  *
196
443
  * @category Rules
197
444
  */
198
- export function maxHoursPerWeek(hours: number, opts?: RuleOptions): RuleEntry {
445
+ export function maxHoursPerWeek(
446
+ hours: number,
447
+ opts?: RuleOptions,
448
+ ): RuleEntry<"max-hours-week", { hours: number } & RuleOptions> {
199
449
  return makeRule("max-hours-week", { hours, ...opts });
200
450
  }
201
451
 
202
452
  /**
203
453
  * Minimum hours when assigned on a day.
204
454
  *
205
- * @example
206
- * ```typescript
207
- * minHoursPerDay(4)
208
- * ```
209
- *
210
455
  * @category Rules
211
456
  */
212
- export function minHoursPerDay(hours: number, opts?: EntityOnlyRuleOptions): RuleEntry {
457
+ export function minHoursPerDay(
458
+ hours: number,
459
+ opts?: EntityOnlyRuleOptions,
460
+ ): RuleEntry<"min-hours-day", { hours: number } & EntityOnlyRuleOptions> {
213
461
  return makeRule("min-hours-day", { hours, ...opts });
214
462
  }
215
463
 
216
464
  /**
217
465
  * Minimum hours per scheduling week.
218
466
  *
219
- * @example
220
- * ```typescript
221
- * minHoursPerWeek(20, { priority: "HIGH" })
222
- * ```
223
- *
224
467
  * @category Rules
225
468
  */
226
- export function minHoursPerWeek(hours: number, opts?: EntityOnlyRuleOptions): RuleEntry {
469
+ export function minHoursPerWeek(
470
+ hours: number,
471
+ opts?: EntityOnlyRuleOptions,
472
+ ): RuleEntry<"min-hours-week", { hours: number } & EntityOnlyRuleOptions> {
227
473
  return makeRule("min-hours-week", { hours, ...opts });
228
474
  }
229
475
 
230
476
  /**
231
- * Limits working days per scheduling week.
477
+ * Caps assigned days per scheduling week.
232
478
  *
233
- * @example
234
- * ```typescript
235
- * maxDaysPerWeek(5)
236
- * maxDaysPerWeek(3, { appliesTo: "part-time" })
237
- * ```
479
+ * @remarks
480
+ * Use this when the requirements explicitly impose a weekly day cap. If the same
481
+ * number is the normal weekly pattern rather than just a hard limit, pair it
482
+ * with {@link targetDaysPerWeek}.
238
483
  *
239
484
  * @category Rules
240
485
  */
241
- export function maxDaysPerWeek(days: number, opts?: RuleOptions): RuleEntry {
486
+ export function maxDaysPerWeek(
487
+ days: number,
488
+ opts?: RuleOptions,
489
+ ): RuleEntry<"max-days-week", { days: number } & RuleOptions> {
242
490
  return makeRule("max-days-week", { days, ...opts });
243
491
  }
244
492
 
245
493
  /**
246
- * Minimum working days per scheduling week.
494
+ * Enforces a minimum number of assigned days per scheduling week.
247
495
  *
248
- * @example
249
- * ```typescript
250
- * minDaysPerWeek(3, { priority: "HIGH" })
251
- * minDaysPerWeek(5, { appliesTo: "full-time" })
252
- * ```
496
+ * @remarks
497
+ * Use this when the requirements explicitly require a weekly minimum. Language like
498
+ * "works a 4-day week" often implies a hard cap via {@link maxDaysPerWeek}
499
+ * and possibly a softer target via {@link targetDaysPerWeek}, not a mandatory
500
+ * weekly minimum for every member.
253
501
  *
254
502
  * @category Rules
255
503
  */
256
- export function minDaysPerWeek(days: number, opts?: EntityOnlyRuleOptions): RuleEntry {
504
+ export function minDaysPerWeek(
505
+ days: number,
506
+ opts?: EntityOnlyRuleOptions,
507
+ ): RuleEntry<"min-days-week", { days: number } & EntityOnlyRuleOptions> {
257
508
  return makeRule("min-days-week", { days, ...opts });
258
509
  }
259
510
 
260
511
  /**
261
- * Maximum distinct shifts per day.
512
+ * Softly targets assigned days per scheduling week.
262
513
  *
263
- * @example
264
- * ```typescript
265
- * maxShiftsPerDay(1)
266
- * maxShiftsPerDay(2, { appliesTo: "student", dayOfWeek: weekend })
267
- * ```
514
+ * @remarks
515
+ * Use this for stated working patterns like "works a 4-day week" when the
516
+ * pattern should remain the norm but can flex around holidays, time off, or
517
+ * other tradeoffs. Deviations in either direction are penalized, so working
518
+ * fewer days or more days than the target both count as misses. Pair it with
519
+ * {@link maxDaysPerWeek} when the same number is also a hard cap, and use
520
+ * {@link minDaysPerWeek} only when the requirements explicitly require a minimum.
521
+ *
522
+ * @param days - Target number of assigned days per scheduling week.
523
+ * @param opts - Optional entity scope and soft priority.
268
524
  *
269
525
  * @category Rules
270
526
  */
271
- export function maxShiftsPerDay(shifts: number, opts?: RuleOptions): RuleEntry {
527
+ export function targetDaysPerWeek(
528
+ days: number,
529
+ opts?: TargetDaysPerWeekOptions,
530
+ ): RuleEntry<"target-days-week", { days: number } & TargetDaysPerWeekOptions> {
531
+ return makeRule("target-days-week", { days, ...opts });
532
+ }
533
+
534
+ /**
535
+ * Maximum distinct shifts per day.
536
+ *
537
+ * @category Rules
538
+ */
539
+ export function maxShiftsPerDay(
540
+ shifts: number,
541
+ opts?: RuleOptions,
542
+ ): RuleEntry<"max-shifts-day", { shifts: number } & RuleOptions> {
272
543
  return makeRule("max-shifts-day", { shifts, ...opts });
273
544
  }
274
545
 
275
546
  /**
276
- * Maximum consecutive working days.
547
+ * Caps how many targeted assignments may overlap at the same time.
277
548
  *
278
- * @example
279
- * ```typescript
280
- * maxConsecutiveDays(5)
281
- * ```
549
+ * @remarks
550
+ * Use this for hard productive capacity limits that should stay separate from
551
+ * the minimum staffing requirement expressed by {@link cover}. Pair a
552
+ * lower-bound `cover(...)` with `maxConcurrentAssignments(...)` when the
553
+ * business needs both demand and capacity modeled explicitly.
554
+ *
555
+ * On days with staggered opening and closing shifts, keep lower-bound coverage
556
+ * scoped to the windows that truly need minimum staffing. Do not turn a total
557
+ * daily headcount target into a whole-day lower-bound `cover(...)` unless the
558
+ * business explicitly requires that many people in every overlapping bucket.
559
+ *
560
+ * Unlike {@link maxShiftsPerDay}, this limits simultaneous overlap, not total
561
+ * assignments on a day.
562
+ *
563
+ * @param assignments - Maximum number of concurrent overlapping assignments.
564
+ * @param opts - Optional scope and time-window settings.
565
+ *
566
+ * @category Rules
567
+ */
568
+ export function maxConcurrentAssignments(
569
+ assignments: number,
570
+ opts?: RuleOptions & {
571
+ startTime?: TimeOfDay;
572
+ endTime?: TimeOfDay;
573
+ },
574
+ ): RuleEntry<
575
+ "max-concurrent-assignments",
576
+ { assignments: number } & MaxConcurrentAssignmentsOptions
577
+ > {
578
+ return makeRule("max-concurrent-assignments", { assignments, ...opts });
579
+ }
580
+
581
+ /**
582
+ * Softly targets the daily peak number of concurrent overlapping assignments.
583
+ *
584
+ * @remarks
585
+ * Use this when the business wants to hit full productive occupancy at the
586
+ * busy point of a day, but does not mean that same number must be present
587
+ * throughout the full span. This is not a whole-day lower bound. Pair it with
588
+ * {@link cover} for edge minima and with
589
+ * {@link maxConcurrentAssignments} when the same value is also a hard cap.
590
+ * Prefer this over a whole-day `cover(...)` when the brief means "reach the
591
+ * peak" rather than "maintain this minimum throughout the day".
592
+ *
593
+ * This is a rule-level peak target, not a variant of {@link cover}. Do not
594
+ * introduce a synthetic semantic time just to attach a whole-window
595
+ * `cover(...)` when the requirement is really about the day's peak.
596
+ *
597
+ * @param assignments - Target peak number of concurrent overlapping assignments.
598
+ * @param opts - Optional scope and soft priority.
282
599
  *
283
600
  * @category Rules
284
601
  */
285
- export function maxConsecutiveDays(days: number, opts?: EntityOnlyRuleOptions): RuleEntry {
602
+ export function targetPeakConcurrentAssignments(
603
+ assignments: number,
604
+ opts?: TargetPeakConcurrentAssignmentsOptions,
605
+ ): RuleEntry<
606
+ "target-peak-concurrent-assignments",
607
+ { assignments: number } & TargetPeakConcurrentAssignmentsOptions
608
+ > {
609
+ return makeRule("target-peak-concurrent-assignments", { assignments, ...opts });
610
+ }
611
+
612
+ /**
613
+ * Maximum consecutive assigned days.
614
+ *
615
+ * @category Rules
616
+ */
617
+ export function maxConsecutiveDays(
618
+ days: number,
619
+ opts?: EntityOnlyRuleOptions,
620
+ ): RuleEntry<"max-consecutive-days", { days: number } & EntityOnlyRuleOptions> {
286
621
  return makeRule("max-consecutive-days", { days, ...opts });
287
622
  }
288
623
 
289
624
  /**
290
625
  * Once working, continue for at least this many consecutive days.
291
626
  *
292
- * @example
293
- * ```typescript
294
- * minConsecutiveDays(2, { priority: "HIGH" })
295
- * ```
296
- *
297
627
  * @category Rules
298
628
  */
299
- export function minConsecutiveDays(days: number, opts?: EntityOnlyRuleOptions): RuleEntry {
629
+ export function minConsecutiveDays(
630
+ days: number,
631
+ opts?: EntityOnlyRuleOptions,
632
+ ): RuleEntry<"min-consecutive-days", { days: number } & EntityOnlyRuleOptions> {
300
633
  return makeRule("min-consecutive-days", { days, ...opts });
301
634
  }
302
635
 
303
636
  /**
304
637
  * Minimum rest hours between shifts.
305
638
  *
306
- * @example
307
- * ```typescript
308
- * minRestBetweenShifts(10)
309
- * ```
310
- *
311
639
  * @category Rules
312
640
  */
313
- export function minRestBetweenShifts(hours: number, opts?: EntityOnlyRuleOptions): RuleEntry {
641
+ export function minRestBetweenShifts(
642
+ hours: number,
643
+ opts?: EntityOnlyRuleOptions,
644
+ ): RuleEntry<"min-rest-between-shifts", { hours: number } & EntityOnlyRuleOptions> {
314
645
  return makeRule("min-rest-between-shifts", { hours, ...opts });
315
646
  }
316
647
 
317
648
  /**
318
- * Prefer (`"high"`) or avoid (`"low"`) assigning. Requires `appliesTo`.
649
+ * Nudge the solver toward scheduling targeted members.
319
650
  *
320
- * @example
321
- * ```typescript
322
- * preference("high", { appliesTo: "waiter" })
323
- * preference("low", { appliesTo: "student", dayOfWeek: weekdays })
324
- * ```
651
+ * @remarks
652
+ * Soft constraint. The solver adds a bonus for each assignment of the
653
+ * targeted members, making them more likely to appear on the schedule.
654
+ * Use `priority` to control how strong the nudge is; higher priority
655
+ * means a larger bonus. Even at `"MANDATORY"` strength the solver can
656
+ * still leave a member unassigned if hard constraints prevent it.
657
+ *
658
+ * Opposite of {@link avoidAssignment}. Both map to the same underlying
659
+ * `assignment-priority` rule with different directions.
660
+ *
661
+ * @param opts - See {@link RuleOptions}
325
662
  *
326
663
  * @category Rules
327
664
  */
328
- export function preference(level: "high" | "low", opts?: Omit<RuleOptions, "priority">): RuleEntry {
329
- return makeRule("assignment-priority", { preference: level, ...opts });
665
+ export function preferAssignment(
666
+ opts?: RuleOptions,
667
+ ): RuleEntry<"assignment-priority", { preference: "prefer" } & RuleOptions> {
668
+ return makeRule("assignment-priority", { preference: "prefer", ...opts });
330
669
  }
331
670
 
332
671
  /**
333
- * Prefer assigning to shifts at a specific location. Requires `appliesTo`.
672
+ * Nudge the solver away from scheduling targeted members.
334
673
  *
335
- * @example
336
- * ```typescript
337
- * preferLocation("terrace", { appliesTo: "alice" })
338
- * ```
674
+ * @remarks
675
+ * Soft constraint. The solver adds a penalty for each assignment of the
676
+ * targeted members, making them less likely to appear on the schedule.
677
+ * The member is still assigned when coverage requires it; the penalty
678
+ * only matters when the solver has a choice.
679
+ *
680
+ * Opposite of {@link preferAssignment}. Both map to the same underlying
681
+ * `assignment-priority` rule with different directions.
682
+ *
683
+ * @param opts - See {@link RuleOptions}
684
+ *
685
+ * @category Rules
686
+ */
687
+ export function avoidAssignment(
688
+ opts?: RuleOptions,
689
+ ): RuleEntry<"assignment-priority", { preference: "avoid" } & RuleOptions> {
690
+ return makeRule("assignment-priority", { preference: "avoid", ...opts });
691
+ }
692
+
693
+ /**
694
+ * Steer a multi-role member toward shifts tagged with a specific role.
695
+ *
696
+ * @remarks
697
+ * Penalizes assignment to shift patterns whose `roleIds` do not include
698
+ * the preferred role. The member is still assigned to other roles when
699
+ * coverage requires it; the penalty only matters when the solver has a
700
+ * choice. Shift patterns without any `roleIds` (open to all) are also
701
+ * penalized since they are not explicitly tagged with the preferred role.
702
+ *
703
+ * The targeted members must actually hold the preferred role in their
704
+ * `roleIds`. If none of them do, compilation reports an error because
705
+ * the rule would silently penalize all their assignments (the opposite
706
+ * of the intent).
707
+ *
708
+ * Typical use case: a waiter who can cover kitchen pass shifts in a
709
+ * pinch. Give the member both roles (`roleIds: ["waiter", "pass"]`)
710
+ * and use `preferRole("waiter", ...)` so the solver keeps them on the
711
+ * floor unless the pass team is short-staffed.
712
+ *
713
+ * @param roleId - The role to prefer
714
+ * @param opts - See {@link EntityOnlyRuleOptions}
715
+ *
716
+ * @category Rules
717
+ */
718
+ export function preferRole(
719
+ roleId: string,
720
+ opts?: EntityOnlyRuleOptions,
721
+ ): RuleEntry<"role-preference", { roleId: string } & EntityOnlyRuleOptions> {
722
+ return makeRule("role-preference", { roleId, ...opts });
723
+ }
724
+
725
+ /**
726
+ * Steer a member toward shifts at a specific location.
727
+ *
728
+ * @remarks
729
+ * Penalizes assignment to shift patterns whose `locationId` does not
730
+ * match the preferred location. The member is still assigned elsewhere
731
+ * when coverage requires it.
732
+ *
733
+ * @param locationId - The location to prefer
734
+ * @param opts - See {@link EntityOnlyRuleOptions}
339
735
  *
340
736
  * @category Rules
341
737
  */
342
- export function preferLocation(locationId: string, opts?: EntityOnlyRuleOptions): RuleEntry {
738
+ export function preferLocation(
739
+ locationId: string,
740
+ opts?: EntityOnlyRuleOptions,
741
+ ): RuleEntry<"location-preference", { locationId: string } & EntityOnlyRuleOptions> {
343
742
  return makeRule("location-preference", { locationId, ...opts });
344
743
  }
345
744
 
@@ -357,15 +756,11 @@ export function preferLocation(locationId: string, opts?: EntityOnlyRuleOptions)
357
756
  * messaging from {@link minDaysPerWeek}. Priority is not configurable;
358
757
  * the rule name communicates the intent.
359
758
  *
360
- * @example
361
- * ```typescript
362
- * mustAssign({ appliesTo: "diana" })
363
- * mustAssign({ appliesTo: ["diana", "yavuz"] })
364
- * ```
365
- *
366
759
  * @category Rules
367
760
  */
368
- export function mustAssign(opts?: { appliesTo?: string | string[] }): RuleEntry {
761
+ export function mustAssign(opts?: {
762
+ appliesTo?: string | string[];
763
+ }): RuleEntry<"must-assign", { appliesTo?: string | string[] }> {
369
764
  return makeRule("must-assign", { ...opts });
370
765
  }
371
766
 
@@ -377,21 +772,9 @@ export function mustAssign(opts?: { appliesTo?: string | string[] }): RuleEntry
377
772
  * Block assignments during specified periods.
378
773
  * Requires at least one time scope (`dayOfWeek`, `dateRange`, `dates`, or `from`/`until`).
379
774
  *
380
- * @example
381
- * ```typescript
382
- * // Full days off
383
- * timeOff({ appliesTo: "alice", dateRange: { start: "2024-02-01", end: "2024-02-05" } })
384
- *
385
- * // Every weekend off
386
- * timeOff({ appliesTo: "mauro", dayOfWeek: weekend })
387
- *
388
- * // Wednesday afternoons off
389
- * timeOff({ appliesTo: "student", dayOfWeek: ["wednesday"], from: t(14) })
390
- * ```
391
- *
392
775
  * @category Rules
393
776
  */
394
- export function timeOff(opts: TimeOffOptions): RuleEntry {
777
+ export function timeOff(opts: TimeOffOptions): TimeOffRuleEntry {
395
778
  const { from, until, ...rest } = opts;
396
779
  return defineRule("time-off", { from, until, ...rest }, (ctx) => {
397
780
  if (!rest.dayOfWeek && !rest.dateRange && !rest.dates && !rest.recurringPeriods) {
@@ -400,7 +783,7 @@ export function timeOff(opts: TimeOffOptions): RuleEntry {
400
783
  );
401
784
  }
402
785
 
403
- const { appliesTo, dates, ...passthrough } = rest;
786
+ const { appliesTo, dates, priority, ...passthrough } = rest;
404
787
  const entityScope = resolveAppliesTo(appliesTo, ctx.roles, ctx.skills, ctx.memberIds);
405
788
  const resolvedDates = dates ? { specificDates: dates } : {};
406
789
 
@@ -416,31 +799,28 @@ export function timeOff(opts: TimeOffOptions): RuleEntry {
416
799
  partialDay.endTime = until;
417
800
  }
418
801
 
419
- return {
802
+ const resolved = {
420
803
  name: "time-off",
804
+ priority: priority ?? "MANDATORY",
421
805
  ...passthrough,
422
806
  ...entityScope,
423
807
  ...resolvedDates,
424
808
  ...partialDay,
425
- } as CpsatRuleConfigEntry;
809
+ } satisfies ResolvedTimeOffRuleConfig;
810
+
811
+ return resolved;
426
812
  });
427
813
  }
428
814
 
429
815
  /**
430
816
  * Members work the same shifts on days they are both assigned.
431
817
  *
432
- * @example
433
- * ```typescript
434
- * assignTogether(["alice", "bob"])
435
- * assignTogether(["alice", "bob", "charlie"], { priority: "HIGH" })
436
- * ```
437
- *
438
818
  * @category Rules
439
819
  */
440
820
  export function assignTogether(
441
821
  memberIds: [string, string, ...string[]],
442
822
  opts?: AssignTogetherOptions,
443
- ): RuleEntry {
823
+ ): AssignTogetherRuleEntry {
444
824
  return defineRule("assign-together", { members: memberIds, ...opts }, (ctx) => {
445
825
  for (const member of memberIds) {
446
826
  if (!ctx.memberIds.has(member)) {
@@ -450,14 +830,133 @@ export function assignTogether(
450
830
  );
451
831
  }
452
832
  }
453
- return {
833
+ const resolved = {
454
834
  name: "assign-together",
455
835
  groupMemberIds: memberIds,
456
- ...opts,
457
- } as CpsatRuleConfigEntry;
836
+ priority: opts?.priority ?? "MANDATORY",
837
+ } satisfies BuiltInResolvedRuleConfig<"assign-together">;
838
+
839
+ return resolved;
458
840
  });
459
841
  }
460
842
 
843
+ // ============================================================================
844
+ // Period-Based Day-of-Week Rules
845
+ // ============================================================================
846
+
847
+ /**
848
+ * Options for {@link maxDaysOfWeekPerPeriod} and {@link minDaysOfWeekPerPeriod}.
849
+ *
850
+ * Exactly one of `weeks` or `months` must be provided to define the
851
+ * repeating period. Week periods align to `weekStartsOn`. Month periods
852
+ * align to calendar month boundaries.
853
+ *
854
+ * @category Rules
855
+ */
856
+ export type DaysOfWeekPerPeriodOptions = {
857
+ /** Scope to role, skill, or member ID. */
858
+ appliesTo?: string | string[];
859
+ /** Default `MANDATORY`. */
860
+ priority?: Priority;
861
+ } & ({ weeks: number; months?: never } | { months: number; weeks?: never });
862
+
863
+ /**
864
+ * Caps how many times a person works on specific days of the week within
865
+ * each period.
866
+ *
867
+ * @remarks
868
+ * The scheduling window is divided into non-overlapping chunks of the
869
+ * configured period length. Within each chunk, only days matching the
870
+ * `dayOfWeek` filter are counted. The constraint bounds that count.
871
+ *
872
+ * @param days - Maximum number of matching days allowed per period.
873
+ * @param dayOfWeek - Which days of the week count toward the limit.
874
+ * @param opts - Period length and optional scoping.
875
+ *
876
+ * @category Rules
877
+ */
878
+ export function maxDaysOfWeekPerPeriod(
879
+ days: number,
880
+ dayOfWeek: readonly [DayOfWeek, ...DayOfWeek[]],
881
+ opts: DaysOfWeekPerPeriodOptions,
882
+ ): MaxDaysOfWeekPerPeriodRuleEntry {
883
+ const { appliesTo, priority, ...periodOpts } = opts;
884
+ const period =
885
+ "weeks" in periodOpts && periodOpts.weeks != null
886
+ ? { type: "weeks" as const, value: periodOpts.weeks }
887
+ : { type: "months" as const, value: (periodOpts as { months: number }).months };
888
+
889
+ return defineRule(
890
+ "max-days-of-week-per-period",
891
+ { days, dayOfWeek: [...dayOfWeek], period, appliesTo, priority },
892
+ (ctx) => {
893
+ const entityScope = resolveAppliesTo(appliesTo, ctx.roles, ctx.skills, ctx.memberIds);
894
+ const resolved = {
895
+ name: "max-days-of-week-per-period",
896
+ days,
897
+ dayOfWeek: [...dayOfWeek],
898
+ period,
899
+ priority: priority ?? "MANDATORY",
900
+ ...entityScope,
901
+ } satisfies ResolvedDaysOfWeekPerPeriodRuleConfig<"max-days-of-week-per-period">;
902
+
903
+ return resolved;
904
+ },
905
+ );
906
+ }
907
+
908
+ /**
909
+ * Enforces a minimum number of times a person works on specific days of the
910
+ * week within each period.
911
+ *
912
+ * @remarks
913
+ * The scheduling window is divided into non-overlapping chunks of the
914
+ * configured period length. Within each chunk, only days matching the
915
+ * `dayOfWeek` filter are counted. The constraint ensures the count meets
916
+ * the minimum.
917
+ *
918
+ * When the scheduling window does not align evenly with the period
919
+ * boundaries, the last chunk may contain fewer matching days than the
920
+ * minimum. A MANDATORY priority will make the model infeasible in that
921
+ * case. Use a soft priority (HIGH, MEDIUM, LOW) if partial periods at
922
+ * the edges of the window are expected.
923
+ *
924
+ * @param days - Minimum number of matching days required per period.
925
+ * @param dayOfWeek - Which days of the week count toward the limit.
926
+ * @param opts - Period length and optional scoping.
927
+ *
928
+ * @category Rules
929
+ */
930
+ export function minDaysOfWeekPerPeriod(
931
+ days: number,
932
+ dayOfWeek: readonly [DayOfWeek, ...DayOfWeek[]],
933
+ opts: DaysOfWeekPerPeriodOptions,
934
+ ): MinDaysOfWeekPerPeriodRuleEntry {
935
+ const { appliesTo, priority, ...periodOpts } = opts;
936
+ const period =
937
+ "weeks" in periodOpts && periodOpts.weeks != null
938
+ ? { type: "weeks" as const, value: periodOpts.weeks }
939
+ : { type: "months" as const, value: (periodOpts as { months: number }).months };
940
+
941
+ return defineRule(
942
+ "min-days-of-week-per-period",
943
+ { days, dayOfWeek: [...dayOfWeek], period, appliesTo, priority },
944
+ (ctx) => {
945
+ const entityScope = resolveAppliesTo(appliesTo, ctx.roles, ctx.skills, ctx.memberIds);
946
+ const resolved = {
947
+ name: "min-days-of-week-per-period",
948
+ days,
949
+ dayOfWeek: [...dayOfWeek],
950
+ period,
951
+ priority: priority ?? "MANDATORY",
952
+ ...entityScope,
953
+ } satisfies ResolvedDaysOfWeekPerPeriodRuleConfig<"min-days-of-week-per-period">;
954
+
955
+ return resolved;
956
+ },
957
+ );
958
+ }
959
+
461
960
  // ============================================================================
462
961
  // Internal: appliesTo resolution (shared with definition.ts)
463
962
  // ============================================================================