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
@@ -4,7 +4,7 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import type { DayOfWeek, SchedulingPeriod } from "../types.js";
7
+ import type { DateString, DayOfWeek, SchedulingPeriod } from "../types.js";
8
8
  import type { SemanticTimeEntry } from "../cpsat/semantic-time.js";
9
9
  import type { MixedCoverageRequirement, CoverageVariant } from "../cpsat/semantic-time.js";
10
10
  import { defineSemanticTimes } from "../cpsat/semantic-time.js";
@@ -12,9 +12,18 @@ import { resolveDaysFromPeriod } from "../datetime.utils.js";
12
12
  import type { ModelBuilderConfig, CompilationResult } from "../cpsat/model-builder.js";
13
13
  import { ModelBuilder } from "../cpsat/model-builder.js";
14
14
  import type { SchedulingMember, ShiftPattern, Priority } from "../cpsat/types.js";
15
- import type { CpsatRuleConfigEntry, CreateCpsatRuleFunction } from "../cpsat/rules/rules.types.js";
16
- import { builtInCpsatRuleFactories } from "../cpsat/rules/registry.js";
17
- import type { SolverClient, SolverResponse } from "../client.types.js";
15
+ import type {
16
+ AnyCpsatRuleConfigEntry,
17
+ BuiltInCpsatRuleRegistry,
18
+ CpsatRuleConfigEntryFor,
19
+ CpsatRuleRegistry,
20
+ } from "../cpsat/rules/rules.types.js";
21
+ import {
22
+ assertNoBuiltInCpsatRuleOverrides,
23
+ assertValidCpsatRuleRegistry,
24
+ builtInCpsatRuleRegistry,
25
+ } from "../cpsat/rules/registry.js";
26
+ import type { SolverClient, SolverRequest, SolverResponse } from "../client.types.js";
18
27
  import { parseSolverResponse, resolveAssignments } from "../cpsat/response.js";
19
28
  import type { ShiftAssignment } from "../cpsat/response.js";
20
29
  import type { ScheduleValidation } from "../cpsat/validation.types.js";
@@ -22,11 +31,239 @@ import { calculateScheduleCost } from "../cpsat/cost.js";
22
31
  import type { CostBreakdown } from "../cpsat/cost.js";
23
32
 
24
33
  import type { CoverageEntry } from "./coverage.js";
25
- import type { RuleEntry, RuleResolveContext } from "./rules.js";
34
+ import type { RuleEntry, RuleResolveContext, ScheduleRuleEntry } from "./rules.js";
26
35
  import { resolveAppliesTo } from "./rules.js";
27
36
 
28
37
  /** A value that can be passed to {@link Schedule.with}. */
29
- type WithArg = Schedule | SchedulingMember[];
38
+ type WithArg = Schedule<CpsatRuleRegistry> | SchedulingMember[];
39
+
40
+ type EmptyRuleRegistry = Record<never, never>;
41
+ type BuiltInRuleRegistry = BuiltInCpsatRuleRegistry;
42
+ type CustomRuleRegistry<TRuleRegistry extends BuiltInRuleRegistry> = CpsatRuleRegistry & {
43
+ [K in Extract<Exclude<keyof TRuleRegistry, keyof BuiltInRuleRegistry>, string>]: TRuleRegistry[K];
44
+ };
45
+
46
+ type RegistryFromWithArg<T> = T extends Schedule<infer Registry> ? Registry : EmptyRuleRegistry;
47
+
48
+ type RuleEntryFields<T> = T extends RuleEntry<any, infer Fields> ? Fields : never;
49
+
50
+ type BuiltInScheduleRuleAuthoringFields<Name extends keyof BuiltInRuleRegistry> =
51
+ Name extends "max-hours-day"
52
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").maxHoursPerDay>>
53
+ : Name extends "max-hours-week"
54
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").maxHoursPerWeek>>
55
+ : Name extends "min-hours-day"
56
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").minHoursPerDay>>
57
+ : Name extends "min-hours-week"
58
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").minHoursPerWeek>>
59
+ : Name extends "max-days-week"
60
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").maxDaysPerWeek>>
61
+ : Name extends "min-days-week"
62
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").minDaysPerWeek>>
63
+ : Name extends "target-days-week"
64
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").targetDaysPerWeek>>
65
+ : Name extends "max-shifts-day"
66
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").maxShiftsPerDay>>
67
+ : Name extends "max-concurrent-assignments"
68
+ ? RuleEntryFields<
69
+ ReturnType<typeof import("./rules.js").maxConcurrentAssignments>
70
+ >
71
+ : Name extends "target-peak-concurrent-assignments"
72
+ ? RuleEntryFields<
73
+ ReturnType<typeof import("./rules.js").targetPeakConcurrentAssignments>
74
+ >
75
+ : Name extends "max-consecutive-days"
76
+ ? RuleEntryFields<
77
+ ReturnType<typeof import("./rules.js").maxConsecutiveDays>
78
+ >
79
+ : Name extends "min-consecutive-days"
80
+ ? RuleEntryFields<
81
+ ReturnType<typeof import("./rules.js").minConsecutiveDays>
82
+ >
83
+ : Name extends "min-rest-between-shifts"
84
+ ? RuleEntryFields<
85
+ ReturnType<typeof import("./rules.js").minRestBetweenShifts>
86
+ >
87
+ : Name extends "must-assign"
88
+ ? RuleEntryFields<ReturnType<typeof import("./rules.js").mustAssign>>
89
+ : Name extends "assignment-priority"
90
+ ? RuleEntryFields<
91
+ ReturnType<typeof import("./rules.js").preferAssignment>
92
+ >
93
+ : Name extends "role-preference"
94
+ ? RuleEntryFields<
95
+ ReturnType<typeof import("./rules.js").preferRole>
96
+ >
97
+ : Name extends "location-preference"
98
+ ? RuleEntryFields<
99
+ ReturnType<typeof import("./rules.js").preferLocation>
100
+ >
101
+ : Name extends "time-off"
102
+ ? RuleEntryFields<
103
+ ReturnType<typeof import("./rules.js").timeOff>
104
+ >
105
+ : Name extends "assign-together"
106
+ ? RuleEntryFields<
107
+ ReturnType<typeof import("./rules.js").assignTogether>
108
+ >
109
+ : Name extends "max-days-of-week-per-period"
110
+ ? RuleEntryFields<
111
+ ReturnType<
112
+ typeof import("./rules.js").maxDaysOfWeekPerPeriod
113
+ >
114
+ >
115
+ : Name extends "min-days-of-week-per-period"
116
+ ? RuleEntryFields<
117
+ ReturnType<
118
+ typeof import("./rules.js").minDaysOfWeekPerPeriod
119
+ >
120
+ >
121
+ : Name extends "minimize-cost"
122
+ ? RuleEntryFields<
123
+ ReturnType<
124
+ typeof import("./cost.js").minimizeCost
125
+ >
126
+ >
127
+ : Name extends "day-cost-multiplier"
128
+ ? RuleEntryFields<
129
+ ReturnType<
130
+ typeof import("./cost.js").dayMultiplier
131
+ >
132
+ >
133
+ : Name extends "day-cost-surcharge"
134
+ ? RuleEntryFields<
135
+ ReturnType<
136
+ typeof import("./cost.js").daySurcharge
137
+ >
138
+ >
139
+ : Name extends "time-cost-surcharge"
140
+ ? RuleEntryFields<
141
+ ReturnType<
142
+ typeof import("./cost.js").timeSurcharge
143
+ >
144
+ >
145
+ : Name extends "overtime-weekly-multiplier"
146
+ ? RuleEntryFields<
147
+ ReturnType<
148
+ typeof import("./cost.js").overtimeMultiplier
149
+ >
150
+ >
151
+ : Name extends "overtime-weekly-surcharge"
152
+ ? RuleEntryFields<
153
+ ReturnType<
154
+ typeof import("./cost.js").overtimeSurcharge
155
+ >
156
+ >
157
+ : Name extends "overtime-daily-multiplier"
158
+ ? RuleEntryFields<
159
+ ReturnType<
160
+ typeof import("./cost.js").dailyOvertimeMultiplier
161
+ >
162
+ >
163
+ : Name extends "overtime-daily-surcharge"
164
+ ? RuleEntryFields<
165
+ ReturnType<
166
+ typeof import("./cost.js").dailyOvertimeSurcharge
167
+ >
168
+ >
169
+ : Name extends "overtime-tiered-multiplier"
170
+ ? RuleEntryFields<
171
+ ReturnType<
172
+ typeof import("./cost.js").tieredOvertimeMultiplier
173
+ >
174
+ >
175
+ : never;
176
+
177
+ type ValidateScheduleRuleEntry<TRuleRegistry extends CpsatRuleRegistry, Entry> =
178
+ Entry extends ScheduleRuleEntry<TRuleRegistry>
179
+ ? Entry
180
+ : Entry extends RuleEntry<infer Name, infer Fields>
181
+ ? string extends Name
182
+ ? Entry
183
+ : Name extends keyof TRuleRegistry & string
184
+ ? Name extends keyof BuiltInRuleRegistry
185
+ ? Fields extends BuiltInScheduleRuleAuthoringFields<Name>
186
+ ? Entry
187
+ : never
188
+ : Fields extends Omit<CpsatRuleConfigEntryFor<Pick<TRuleRegistry, Name>>, "name">
189
+ ? Entry
190
+ : never
191
+ : never
192
+ : never;
193
+
194
+ type ValidateScheduleRules<
195
+ TRuleRegistry extends CpsatRuleRegistry,
196
+ TRules extends readonly unknown[],
197
+ > = {
198
+ readonly [Index in keyof TRules]: ValidateScheduleRuleEntry<TRuleRegistry, TRules[Index]>;
199
+ };
200
+
201
+ type BuiltInScheduleConfigInput<
202
+ R extends readonly string[],
203
+ S extends readonly string[],
204
+ T extends Record<string, SemanticTimeEntry>,
205
+ TRules extends readonly unknown[],
206
+ > = Omit<ScheduleConfig<R, S, T, BuiltInRuleRegistry>, "rules" | "ruleRegistry"> & {
207
+ rules?: ValidateScheduleRules<BuiltInRuleRegistry, TRules>;
208
+ ruleRegistry?: undefined;
209
+ };
210
+
211
+ type CustomScheduleConfigInput<
212
+ R extends readonly string[],
213
+ S extends readonly string[],
214
+ T extends Record<string, SemanticTimeEntry>,
215
+ TCustomRuleRegistry extends CpsatRuleRegistry,
216
+ TRules extends readonly unknown[],
217
+ > = Omit<
218
+ ScheduleConfig<R, S, T, BuiltInRuleRegistry & TCustomRuleRegistry>,
219
+ "rules" | "ruleRegistry"
220
+ > & {
221
+ ruleRegistry: TCustomRuleRegistry;
222
+ rules?: ValidateScheduleRules<BuiltInRuleRegistry & TCustomRuleRegistry, TRules>;
223
+ };
224
+
225
+ type BuiltInPartialScheduleConfigInput<TRules extends readonly unknown[]> = Partial<
226
+ Omit<
227
+ ScheduleConfig<
228
+ readonly string[],
229
+ readonly string[],
230
+ Record<string, SemanticTimeEntry>,
231
+ BuiltInRuleRegistry
232
+ >,
233
+ "rules" | "ruleRegistry"
234
+ >
235
+ > & {
236
+ rules?: ValidateScheduleRules<BuiltInRuleRegistry, TRules>;
237
+ ruleRegistry?: undefined;
238
+ };
239
+
240
+ type CustomPartialScheduleConfigInput<
241
+ TCustomRuleRegistry extends CpsatRuleRegistry,
242
+ TRules extends readonly unknown[],
243
+ > = Partial<
244
+ Omit<
245
+ ScheduleConfig<
246
+ readonly string[],
247
+ readonly string[],
248
+ Record<string, SemanticTimeEntry>,
249
+ BuiltInRuleRegistry & TCustomRuleRegistry
250
+ >,
251
+ "rules" | "ruleRegistry"
252
+ >
253
+ > & {
254
+ ruleRegistry: TCustomRuleRegistry;
255
+ rules?: ValidateScheduleRules<BuiltInRuleRegistry & TCustomRuleRegistry, TRules>;
256
+ };
257
+
258
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (
259
+ value: infer Result,
260
+ ) => void
261
+ ? Result
262
+ : never;
263
+
264
+ type MergedRegistryFromArgs<Args extends readonly WithArg[]> = UnionToIntersection<
265
+ RegistryFromWithArg<Args[number]>
266
+ >;
30
267
 
31
268
  // ============================================================================
32
269
  // SolveResult
@@ -35,9 +272,36 @@ type WithArg = Schedule | SchedulingMember[];
35
272
  /** Status of a solve attempt, using idiomatic lowercase TypeScript literals. */
36
273
  export type SolveStatus = "optimal" | "feasible" | "infeasible" | "no_solution";
37
274
 
275
+ /**
276
+ * Strategy for {@link Schedule.solve} and {@link Schedule.compile}.
277
+ *
278
+ * @remarks
279
+ * `{ type: "optimize" }` optimizes objectives and soft constraints.
280
+ * `{ type: "feasibility-only" }` asks the solver for a hard-feasible solution
281
+ * only, ignoring objectives and soft-constraint penalties. Feasibility-only
282
+ * results do not guarantee cost, objective optimality, or soft-constraint
283
+ * diagnostics.
284
+ *
285
+ * @category Schedule Definition
286
+ */
287
+ export type SolveStrategy =
288
+ | {
289
+ /** Optimize objectives and soft constraints. */
290
+ readonly type: "optimize";
291
+ }
292
+ | {
293
+ /** Find a hard-feasible solution without optimizing objectives or soft constraints. */
294
+ readonly type: "feasibility-only";
295
+ };
296
+
38
297
  /**
39
298
  * Result of {@link Schedule.solve}.
40
299
  *
300
+ * @remarks
301
+ * Feasibility-only solves return hard-feasible assignments when one is found,
302
+ * but they do not optimize objectives or compute objective-derived metadata.
303
+ * Cost is therefore only produced for optimized solves.
304
+ *
41
305
  * @category Schedule Definition
42
306
  */
43
307
  export interface SolveResult {
@@ -45,9 +309,9 @@ export interface SolveResult {
45
309
  status: SolveStatus;
46
310
  /** Shift assignments (empty when infeasible or no solution). */
47
311
  assignments: ShiftAssignment[];
48
- /** Validation diagnostics from compilation. */
312
+ /** Validation results collected during compilation and solving. */
49
313
  validation: ScheduleValidation;
50
- /** Cost breakdown (present when cost rules are used and a solution is found). */
314
+ /** Cost breakdown for optimized solves when cost rules are used and a solution is found. */
51
315
  cost?: CostBreakdown;
52
316
  }
53
317
 
@@ -58,7 +322,18 @@ export interface SolveResult {
58
322
  */
59
323
  export interface SolveOptions {
60
324
  /** The date range to schedule. */
61
- dateRange: { start: string; end: string };
325
+ dateRange: { start: DateString; end: DateString };
326
+ /** Solver time limit in seconds. */
327
+ timeLimitSeconds?: number;
328
+ /**
329
+ * Strategy used by the solver.
330
+ *
331
+ * `{ type: "optimize" }` optimizes objectives and soft constraints.
332
+ * `{ type: "feasibility-only" }` finds any hard-feasible solution, ignoring
333
+ * objectives, soft-constraint penalties, and objective-derived metadata such
334
+ * as cost.
335
+ */
336
+ strategy?: SolveStrategy;
62
337
  /**
63
338
  * Fixed assignments from a prior solve (e.g., rolling schedule).
64
339
  * These are injected as fixed variables in the solver.
@@ -84,12 +359,17 @@ export interface SolveOptions {
84
359
  * `roleIds`, `times`, `coverage`, and `shiftPatterns` are required.
85
360
  * These four fields form the minimum solvable schedule.
86
361
  *
362
+ * The `TRuleRegistry` generic represents the full active rule registry for the
363
+ * schedule. In `schedule()`, that registry always includes the built-in rules,
364
+ * and `ruleRegistry` adds any custom extensions on top.
365
+ *
87
366
  * @category Schedule Definition
88
367
  */
89
368
  export interface ScheduleConfig<
90
369
  R extends readonly string[] = readonly string[],
91
370
  S extends readonly string[] = readonly [],
92
371
  T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
372
+ TRuleRegistry extends BuiltInRuleRegistry = BuiltInRuleRegistry,
93
373
  > {
94
374
  /** Declared role IDs. */
95
375
  roleIds: R;
@@ -102,13 +382,51 @@ export interface ScheduleConfig<
102
382
  /** Available shift patterns. */
103
383
  shiftPatterns: ShiftPattern[];
104
384
  /** Scheduling rules and constraints. */
105
- rules?: RuleEntry[];
385
+ rules?: ScheduleRuleEntry<TRuleRegistry>[];
106
386
  /**
107
- * Custom rule factories. Keys are rule names, values are functions
108
- * that take a config object and return a {@link CompilationRule}.
387
+ * Additional rule descriptors keyed by rule name.
388
+ *
389
+ * @remarks
390
+ * These entries are merged with the built-in rule registry at compile time.
109
391
  * Built-in rule names cannot be overridden.
110
392
  */
111
- ruleFactories?: Record<string, CreateCpsatRuleFunction>;
393
+ ruleRegistry?: CustomRuleRegistry<TRuleRegistry>;
394
+ /** Team members (typically added via `.with()` at runtime). */
395
+ members?: SchedulingMember[];
396
+ /** Days of the week the business operates (inclusion filter). */
397
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
398
+ /** Which day starts the week for weekly rules. Defaults to `"monday"`. */
399
+ weekStartsOn?: DayOfWeek;
400
+ }
401
+
402
+ /**
403
+ * Configuration for {@link scheduleWithRuleRegistry}.
404
+ *
405
+ * @remarks
406
+ * Unlike {@link ScheduleConfig}, this config does not implicitly include the
407
+ * built-in rule registry. The provided registry is the complete active rule
408
+ * registry for the schedule.
409
+ *
410
+ * @category Schedule Definition
411
+ */
412
+ export interface ScheduleWithRuleRegistryConfig<
413
+ R extends readonly string[] = readonly string[],
414
+ S extends readonly string[] = readonly [],
415
+ T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
416
+ TRuleRegistry extends CpsatRuleRegistry = CpsatRuleRegistry,
417
+ > {
418
+ /** Declared role IDs. */
419
+ roleIds: R;
420
+ /** Declared skill IDs. When omitted, coverage targets can only be roles. */
421
+ skillIds?: S;
422
+ /** Named semantic time periods. */
423
+ times: T;
424
+ /** Staffing requirements per time period (entries stack additively). */
425
+ coverage: NoInfer<CoverageEntry<keyof T & string, R[number] | S[number]>>[];
426
+ /** Available shift patterns. */
427
+ shiftPatterns: ShiftPattern[];
428
+ /** Scheduling rules and constraints. */
429
+ rules?: ScheduleRuleEntry<TRuleRegistry>[];
112
430
  /** Team members (typically added via `.with()` at runtime). */
113
431
  members?: SchedulingMember[];
114
432
  /** Days of the week the business operates (inclusion filter). */
@@ -128,8 +446,8 @@ interface MergedScheduleConfig {
128
446
  times: Record<string, SemanticTimeEntry>;
129
447
  coverage: CoverageEntry[];
130
448
  shiftPatterns: ShiftPattern[];
131
- rules: RuleEntry[];
132
- ruleFactories: Record<string, CreateCpsatRuleFunction>;
449
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[];
450
+ ruleRegistry: CpsatRuleRegistry;
133
451
  members: SchedulingMember[];
134
452
  dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
135
453
  weekStartsOn?: DayOfWeek;
@@ -147,7 +465,7 @@ interface MergedScheduleConfig {
147
465
  *
148
466
  * @category Schedule Definition
149
467
  */
150
- export class Schedule {
468
+ export class Schedule<TRuleRegistry extends CpsatRuleRegistry = BuiltInRuleRegistry> {
151
469
  readonly #config: Readonly<MergedScheduleConfig>;
152
470
 
153
471
  /** @internal */
@@ -165,7 +483,7 @@ export class Schedule {
165
483
  coverage: [...this.#config.coverage],
166
484
  shiftPatterns: [...this.#config.shiftPatterns],
167
485
  rules: [...this.#config.rules],
168
- ruleFactories: { ...this.#config.ruleFactories },
486
+ ruleRegistry: { ...this.#config.ruleRegistry },
169
487
  members: [...this.#config.members],
170
488
  };
171
489
  }
@@ -221,7 +539,9 @@ export class Schedule {
221
539
  * Validation runs eagerly: role/skill disjointness, coverage targets
222
540
  * referencing declared roles/skills, member role references, etc.
223
541
  */
224
- with(...args: WithArg[]): Schedule {
542
+ with<const Args extends readonly WithArg[]>(
543
+ ...args: Args
544
+ ): Schedule<TRuleRegistry & MergedRegistryFromArgs<Args>> {
225
545
  const merged = mergeConfig(this.#config, args);
226
546
  return new Schedule(merged);
227
547
  }
@@ -237,6 +557,7 @@ export class Schedule {
237
557
  * @param options - Date range and optional pinned assignments
238
558
  */
239
559
  async solve(client: SolverClient, options: SolveOptions): Promise<SolveResult> {
560
+ const strategy = resolveSolveStrategy(options.strategy);
240
561
  const compiled = this.compile(options);
241
562
  if (!compiled.canSolve) {
242
563
  return {
@@ -247,7 +568,7 @@ export class Schedule {
247
568
  }
248
569
 
249
570
  const response = await client.solve(compiled.request);
250
- return buildSolveResult(response, compiled, this.#config);
571
+ return buildSolveResult(response, compiled, this.#config, strategy);
251
572
  }
252
573
 
253
574
  /**
@@ -260,7 +581,8 @@ export class Schedule {
260
581
  throw new Error("Pinned assignments are not yet supported.");
261
582
  }
262
583
 
263
- const modelConfig = resolveToModelConfig(this.#config, options);
584
+ const strategy = resolveSolveStrategy(options.strategy);
585
+ const modelConfig = resolveToModelConfig(this.#config, options, strategy);
264
586
  const builder = new ModelBuilder(modelConfig);
265
587
  const result = builder.compile();
266
588
  return { ...result, builder };
@@ -277,6 +599,10 @@ export class Schedule {
277
599
  * Returns an immutable {@link Schedule} that can be composed via `.with()`
278
600
  * and solved via `.solve()`.
279
601
  *
602
+ * @remarks
603
+ * `schedule()` always uses the built-in rule registry as its default. Passing
604
+ * `ruleRegistry` extends that default with custom rules.
605
+ *
280
606
  * @example
281
607
  * ```typescript
282
608
  * const venue = schedule({
@@ -313,8 +639,65 @@ export function schedule<
313
639
  const R extends readonly string[],
314
640
  const S extends readonly string[] = readonly [],
315
641
  const T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
316
- >(config: ScheduleConfig<R, S, T>): Schedule {
317
- const merged = buildMergedConfig(config as unknown as ScheduleConfig);
642
+ const TRules extends readonly unknown[] = readonly ScheduleRuleEntry<BuiltInRuleRegistry>[],
643
+ >(config: BuiltInScheduleConfigInput<R, S, T, TRules>): Schedule<BuiltInRuleRegistry>;
644
+ export function schedule<
645
+ const R extends readonly string[],
646
+ const S extends readonly string[] = readonly [],
647
+ const T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
648
+ TCustomRuleRegistry extends CpsatRuleRegistry = CpsatRuleRegistry,
649
+ const TRules extends readonly unknown[] = readonly ScheduleRuleEntry<
650
+ BuiltInRuleRegistry & TCustomRuleRegistry
651
+ >[],
652
+ >(
653
+ config: CustomScheduleConfigInput<R, S, T, TCustomRuleRegistry, TRules>,
654
+ ): Schedule<BuiltInRuleRegistry & TCustomRuleRegistry>;
655
+ export function schedule(config: {
656
+ roleIds: readonly string[];
657
+ skillIds?: readonly string[];
658
+ times: Record<string, SemanticTimeEntry>;
659
+ coverage: CoverageEntry[];
660
+ shiftPatterns: ShiftPattern[];
661
+ rules?: readonly RuleEntry[];
662
+ ruleRegistry?: CpsatRuleRegistry;
663
+ members?: SchedulingMember[];
664
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
665
+ weekStartsOn?: DayOfWeek;
666
+ }): Schedule<CpsatRuleRegistry> {
667
+ const customRuleRegistry = config.ruleRegistry ?? {};
668
+ validateRuleRegistryExtension(customRuleRegistry);
669
+ const merged = buildMergedConfig(config, {
670
+ ...builtInCpsatRuleRegistry,
671
+ ...customRuleRegistry,
672
+ });
673
+ validateConfig(merged);
674
+ return new Schedule(merged);
675
+ }
676
+
677
+ /**
678
+ * Create a schedule definition with an explicit complete rule registry.
679
+ *
680
+ * @remarks
681
+ * Use this when you want full control over the active rule registry, including
682
+ * schedules that do not include the built-in rules.
683
+ *
684
+ * @category Schedule Definition
685
+ */
686
+ export function scheduleWithRuleRegistry<
687
+ const R extends readonly string[],
688
+ const S extends readonly string[] = readonly [],
689
+ const T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
690
+ TRuleRegistry extends CpsatRuleRegistry = CpsatRuleRegistry,
691
+ const TRules extends readonly unknown[] = readonly ScheduleRuleEntry<TRuleRegistry>[],
692
+ >(
693
+ ruleRegistry: TRuleRegistry,
694
+ config: Omit<ScheduleWithRuleRegistryConfig<R, S, T, TRuleRegistry>, "rules"> & {
695
+ rules?: ValidateScheduleRules<TRuleRegistry, TRules>;
696
+ },
697
+ ): Schedule<TRuleRegistry> {
698
+ assertValidCpsatRuleRegistry(ruleRegistry);
699
+ assertNoBuiltInCpsatRuleOverrides(ruleRegistry);
700
+ const merged = buildMergedConfig(config, ruleRegistry);
318
701
  validateConfig(merged);
319
702
  return new Schedule(merged);
320
703
  }
@@ -337,16 +720,46 @@ export function schedule<
337
720
  *
338
721
  * @category Schedule Definition
339
722
  */
723
+ export function partialSchedule<
724
+ const TRules extends readonly unknown[] = readonly ScheduleRuleEntry<BuiltInRuleRegistry>[],
725
+ >(config: BuiltInPartialScheduleConfigInput<TRules>): Schedule<BuiltInRuleRegistry>;
726
+ export function partialSchedule<
727
+ TCustomRuleRegistry extends CpsatRuleRegistry = CpsatRuleRegistry,
728
+ const TRules extends readonly unknown[] = readonly ScheduleRuleEntry<
729
+ BuiltInRuleRegistry & TCustomRuleRegistry
730
+ >[],
731
+ >(
732
+ config: CustomPartialScheduleConfigInput<TCustomRuleRegistry, TRules>,
733
+ ): Schedule<BuiltInRuleRegistry & TCustomRuleRegistry>;
340
734
  export function partialSchedule(
341
- config: Partial<ScheduleConfig<readonly string[], readonly string[]>>,
342
- ): Schedule {
343
- const merged = buildMergedConfig({
344
- roleIds: [],
345
- times: {},
346
- coverage: [],
347
- shiftPatterns: [],
348
- ...config,
349
- } as ScheduleConfig);
735
+ config: Partial<{
736
+ roleIds: readonly string[];
737
+ skillIds: readonly string[];
738
+ times: Record<string, SemanticTimeEntry>;
739
+ coverage: CoverageEntry[];
740
+ shiftPatterns: ShiftPattern[];
741
+ rules: readonly RuleEntry[];
742
+ ruleRegistry: CpsatRuleRegistry;
743
+ members: SchedulingMember[];
744
+ dayOfWeek: readonly [DayOfWeek, ...DayOfWeek[]];
745
+ weekStartsOn: DayOfWeek;
746
+ }>,
747
+ ): Schedule<CpsatRuleRegistry> {
748
+ const customRuleRegistry = config.ruleRegistry ?? {};
749
+ validateRuleRegistryExtension(customRuleRegistry);
750
+ const merged = buildMergedConfig(
751
+ {
752
+ roleIds: [],
753
+ times: {},
754
+ coverage: [],
755
+ shiftPatterns: [],
756
+ ...config,
757
+ } as ScheduleConfig,
758
+ {
759
+ ...builtInCpsatRuleRegistry,
760
+ ...customRuleRegistry,
761
+ },
762
+ );
350
763
  validateConfig(merged);
351
764
  return new Schedule(merged);
352
765
  }
@@ -355,15 +768,30 @@ export function partialSchedule(
355
768
  // Internal: Build merged config from user input
356
769
  // ============================================================================
357
770
 
358
- function buildMergedConfig(config: ScheduleConfig): MergedScheduleConfig {
771
+ function buildMergedConfig<
772
+ R extends readonly string[],
773
+ S extends readonly string[],
774
+ T extends Record<string, SemanticTimeEntry>,
775
+ TRuleRegistry extends CpsatRuleRegistry,
776
+ TRules extends readonly unknown[] = readonly ScheduleRuleEntry<CpsatRuleRegistry>[],
777
+ >(
778
+ config:
779
+ | (Omit<ScheduleConfig<R, S, T, BuiltInRuleRegistry & TRuleRegistry>, "rules"> & {
780
+ rules?: TRules;
781
+ })
782
+ | (Omit<ScheduleWithRuleRegistryConfig<R, S, T, TRuleRegistry>, "rules"> & {
783
+ rules?: TRules;
784
+ }),
785
+ ruleRegistry: CpsatRuleRegistry,
786
+ ): MergedScheduleConfig {
359
787
  return {
360
788
  roleIds: [...config.roleIds],
361
789
  skillIds: [...(config.skillIds ?? [])],
362
790
  times: { ...config.times },
363
791
  coverage: [...config.coverage],
364
792
  shiftPatterns: [...config.shiftPatterns],
365
- rules: [...(config.rules ?? [])],
366
- ruleFactories: config.ruleFactories ? { ...config.ruleFactories } : {},
793
+ rules: [...(config.rules ?? [])] as ScheduleRuleEntry<CpsatRuleRegistry>[],
794
+ ruleRegistry: { ...ruleRegistry },
367
795
  members: [...(config.members ?? [])],
368
796
  dayOfWeek: config.dayOfWeek,
369
797
  weekStartsOn: config.weekStartsOn,
@@ -374,18 +802,16 @@ function buildMergedConfig(config: ScheduleConfig): MergedScheduleConfig {
374
802
  // Internal: Validate merged config
375
803
  // ============================================================================
376
804
 
805
+ function validateRuleRegistryExtension(ruleRegistry: CpsatRuleRegistry): void {
806
+ assertValidCpsatRuleRegistry(ruleRegistry);
807
+ assertNoBuiltInCpsatRuleOverrides(ruleRegistry);
808
+ }
809
+
377
810
  function validateConfig(config: MergedScheduleConfig): void {
378
811
  const roles = new Set<string>(config.roleIds);
379
812
  const skills = new Set<string>(config.skillIds);
380
813
 
381
- // Validate custom rule factories don't override built-in names
382
- for (const name of Object.keys(config.ruleFactories)) {
383
- if (name in builtInCpsatRuleFactories) {
384
- throw new Error(
385
- `Custom rule factory "${name}" conflicts with a built-in rule. Choose a different name.`,
386
- );
387
- }
388
- }
814
+ assertValidCpsatRuleRegistry(config.ruleRegistry);
389
815
 
390
816
  // Validate role/skill disjointness
391
817
  for (const skill of skills) {
@@ -491,7 +917,10 @@ function validateCoverageEntry(
491
917
  // Internal: Merge logic
492
918
  // ============================================================================
493
919
 
494
- function mergeConfig(base: Readonly<MergedScheduleConfig>, args: WithArg[]): MergedScheduleConfig {
920
+ function mergeConfig(
921
+ base: Readonly<MergedScheduleConfig>,
922
+ args: readonly WithArg[],
923
+ ): MergedScheduleConfig {
495
924
  const result: MergedScheduleConfig = {
496
925
  roleIds: [...base.roleIds],
497
926
  skillIds: [...base.skillIds],
@@ -499,7 +928,7 @@ function mergeConfig(base: Readonly<MergedScheduleConfig>, args: WithArg[]): Mer
499
928
  coverage: [...base.coverage],
500
929
  shiftPatterns: [...base.shiftPatterns],
501
930
  rules: [...base.rules],
502
- ruleFactories: { ...base.ruleFactories },
931
+ ruleRegistry: { ...base.ruleRegistry },
503
932
  members: [...base.members],
504
933
  dayOfWeek: base.dayOfWeek,
505
934
  weekStartsOn: base.weekStartsOn,
@@ -522,7 +951,7 @@ function mergeConfig(base: Readonly<MergedScheduleConfig>, args: WithArg[]): Mer
522
951
  return result;
523
952
  }
524
953
 
525
- function mergeScheduleFragment(result: MergedScheduleConfig, s: Schedule): void {
954
+ function mergeScheduleFragment(result: MergedScheduleConfig, s: Schedule<CpsatRuleRegistry>): void {
526
955
  const other = s._getConfig();
527
956
 
528
957
  // dayOfWeek: error on conflict (semantics of union vs intersection are ambiguous)
@@ -597,14 +1026,14 @@ function mergeScheduleFragment(result: MergedScheduleConfig, s: Schedule): void
597
1026
  // Rules: additive
598
1027
  result.rules.push(...other.rules);
599
1028
 
600
- // Rule factories: merge, error on collision
601
- for (const [name, factory] of Object.entries(other.ruleFactories)) {
602
- if (name in result.ruleFactories && result.ruleFactories[name] !== factory) {
1029
+ // Rule registry: merge, error on collision
1030
+ for (const [name, descriptor] of Object.entries(other.ruleRegistry)) {
1031
+ if (name in result.ruleRegistry && result.ruleRegistry[name] !== descriptor) {
603
1032
  throw new Error(
604
- `Rule factory "${name}" already registered. Cannot merge schedules with colliding rule factories.`,
1033
+ `Rule registry entry "${name}" already registered. Cannot merge schedules with colliding rule registries.`,
605
1034
  );
606
1035
  }
607
- result.ruleFactories[name] = factory;
1036
+ result.ruleRegistry[name] = descriptor;
608
1037
  }
609
1038
 
610
1039
  // Members: additive, error on duplicate ID
@@ -640,7 +1069,8 @@ function mergeMembers(result: MergedScheduleConfig, incoming: SchedulingMember[]
640
1069
  function resolveToModelConfig(
641
1070
  config: Readonly<MergedScheduleConfig>,
642
1071
  options: SolveOptions,
643
- ): ModelBuilderConfig {
1072
+ strategy: SolveStrategy,
1073
+ ): ModelBuilderConfig<CpsatRuleRegistry, AnyCpsatRuleConfigEntry> {
644
1074
  const roles = new Set<string>(config.roleIds);
645
1075
  const skills = new Set<string>(config.skillIds);
646
1076
  const memberIds = new Set<string>(config.members.map((m) => m.id));
@@ -677,7 +1107,7 @@ function resolveToModelConfig(
677
1107
  "overtime-tiered-multiplier",
678
1108
  ]);
679
1109
  const hasCostRules = allRules.some((r) => costRuleNames.has(r._rule));
680
- if (hasCostRules) {
1110
+ if (strategy.type === "optimize" && hasCostRules) {
681
1111
  const missingPay = config.members.filter((m) => !m.pay).map((m) => m.id);
682
1112
  if (missingPay.length > 0) {
683
1113
  throw new Error(
@@ -696,11 +1126,10 @@ function resolveToModelConfig(
696
1126
  schedulingPeriod: resolvedPeriod,
697
1127
  coverage: resolvedCoverage,
698
1128
  ruleConfigs,
699
- ruleFactories:
700
- Object.keys(config.ruleFactories).length > 0
701
- ? { ...builtInCpsatRuleFactories, ...config.ruleFactories }
702
- : undefined,
1129
+ ruleRegistry: config.ruleRegistry,
703
1130
  weekStartsOn: config.weekStartsOn,
1131
+ solverOptions: buildSolverOptions(options),
1132
+ strategy,
704
1133
  };
705
1134
  }
706
1135
 
@@ -708,10 +1137,37 @@ function resolveToModelConfig(
708
1137
  // Internal: Build SolveResult from solver response
709
1138
  // ============================================================================
710
1139
 
711
- function mapSolverStatus(solverStatus: SolverResponse["status"]): SolveStatus {
1140
+ function resolveSolveStrategy(strategy: unknown): SolveStrategy {
1141
+ if (strategy === undefined) {
1142
+ return { type: "optimize" };
1143
+ }
1144
+
1145
+ if (typeof strategy !== "object" || strategy === null || !("type" in strategy)) {
1146
+ throw new Error("Solve strategy must be an object with a type field.");
1147
+ }
1148
+
1149
+ const strategyType = (strategy as { type: unknown }).type;
1150
+ if (strategyType === "optimize" || strategyType === "feasibility-only") {
1151
+ return { type: strategyType };
1152
+ }
1153
+
1154
+ throw new Error(`Unknown solve strategy "${String(strategyType)}".`);
1155
+ }
1156
+
1157
+ function buildSolverOptions(options: SolveOptions): SolverRequest["options"] | undefined {
1158
+ if (options.timeLimitSeconds === undefined) {
1159
+ return undefined;
1160
+ }
1161
+ return { timeLimitSeconds: options.timeLimitSeconds };
1162
+ }
1163
+
1164
+ function mapSolverStatus(
1165
+ solverStatus: SolverResponse["status"],
1166
+ strategy: SolveStrategy,
1167
+ ): SolveStatus {
712
1168
  switch (solverStatus) {
713
1169
  case "OPTIMAL":
714
- return "optimal";
1170
+ return strategy.type === "feasibility-only" ? "feasible" : "optimal";
715
1171
  case "FEASIBLE":
716
1172
  return "feasible";
717
1173
  case "INFEASIBLE":
@@ -728,14 +1184,26 @@ function buildSolveResult(
728
1184
  response: SolverResponse,
729
1185
  compiled: CompilationResult & { builder: ModelBuilder },
730
1186
  config: Readonly<MergedScheduleConfig>,
1187
+ strategy: SolveStrategy,
731
1188
  ): SolveResult {
732
- const status = mapSolverStatus(response.status);
733
- const parsed = parseSolverResponse(response);
1189
+ const status = mapSolverStatus(response.status, strategy);
1190
+ const hasFinalSolution = status === "optimal" || status === "feasible";
1191
+ const parsed = hasFinalSolution
1192
+ ? parseSolverResponse(response)
1193
+ : {
1194
+ status: response.status,
1195
+ assignments: [],
1196
+ statistics: response.statistics,
1197
+ error: response.error,
1198
+ };
1199
+
1200
+ compiled.builder.reporter.analyzeSolution(response, {
1201
+ analyzeSoftConstraints: strategy.type === "optimize",
1202
+ });
734
1203
 
735
1204
  // Run post-solve validation when a solution exists
736
- if (parsed.assignments.length > 0 && (status === "optimal" || status === "feasible")) {
1205
+ if (parsed.assignments.length > 0 && hasFinalSolution && strategy.type === "optimize") {
737
1206
  const resolved = resolveAssignments(parsed.assignments, compiled.builder.shiftPatterns);
738
- compiled.builder.reporter.analyzeSolution(response);
739
1207
  compiled.builder.validateSolution(resolved);
740
1208
  }
741
1209
 
@@ -747,14 +1215,18 @@ function buildSolveResult(
747
1215
  validation,
748
1216
  };
749
1217
 
750
- // Compute cost breakdown when cost rules are present and a solution was found
751
- if (parsed.assignments.length > 0 && (status === "optimal" || status === "feasible")) {
752
- const hasCostRules = config.rules.some((r) => r._rule === "minimize-cost");
753
- if (hasCostRules) {
1218
+ // Compute cost breakdown when cost artifacts are present and a solution was found
1219
+ if (parsed.assignments.length > 0 && hasFinalSolution && strategy.type === "optimize") {
1220
+ const hasCostArtifacts = compiled.builder.rules.some((rule) =>
1221
+ rule.artifacts.some((artifact) => artifact.kind === "cost"),
1222
+ );
1223
+ if (hasCostArtifacts) {
754
1224
  result.cost = calculateScheduleCost(parsed.assignments, {
755
1225
  members: config.members,
756
1226
  shiftPatterns: config.shiftPatterns,
757
1227
  rules: compiled.builder.rules,
1228
+ days: compiled.builder.days,
1229
+ weekStartsOn: compiled.builder.weekStartsOn,
758
1230
  });
759
1231
  }
760
1232
  }
@@ -783,7 +1255,7 @@ function buildCoverageRequirements<T extends string>(
783
1255
  targetCount: number;
784
1256
  priority?: Priority;
785
1257
  dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
786
- dates?: string[];
1258
+ dates?: DateString[];
787
1259
  } = {
788
1260
  semanticTime: entry.timeName,
789
1261
  targetCount: entry.count,
@@ -809,7 +1281,7 @@ function buildSimpleCoverageTarget<T extends string>(
809
1281
  targetCount: number;
810
1282
  priority?: Priority;
811
1283
  dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
812
- dates?: string[];
1284
+ dates?: DateString[];
813
1285
  },
814
1286
  roles: Set<string>,
815
1287
  skills: Set<string>,
@@ -849,12 +1321,27 @@ function buildSimpleCoverageTarget<T extends string>(
849
1321
  /**
850
1322
  * Build a VariantCoverageRequirement from a variant-form CoverageEntry.
851
1323
  */
1324
+ function requireCoverageVariants<T extends string>(
1325
+ entry: CoverageEntry<T, string>,
1326
+ ): [CoverageVariant, ...CoverageVariant[]] {
1327
+ if (!entry.variants || entry.variants.length === 0) {
1328
+ throw new Error(`Coverage entry for "${entry.timeName}" must define at least one variant.`);
1329
+ }
1330
+
1331
+ const first = entry.variants[0];
1332
+ if (!first) {
1333
+ throw new Error(`Coverage entry for "${entry.timeName}" must define at least one variant.`);
1334
+ }
1335
+
1336
+ return [first, ...entry.variants.slice(1)];
1337
+ }
1338
+
852
1339
  function buildVariantCoverageRequirement<T extends string>(
853
1340
  entry: CoverageEntry<T, string>,
854
1341
  roles: Set<string>,
855
1342
  skills: Set<string>,
856
1343
  ): MixedCoverageRequirement<T> {
857
- const variants = entry.variants! as unknown as [CoverageVariant, ...CoverageVariant[]];
1344
+ const variants = requireCoverageVariants(entry);
858
1345
 
859
1346
  const resolveTarget = (): {
860
1347
  roleIds?: [string, ...string[]];
@@ -885,23 +1372,23 @@ function buildVariantCoverageRequirement<T extends string>(
885
1372
  // ============================================================================
886
1373
 
887
1374
  function resolveRules(
888
- rules: RuleEntry[],
1375
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[],
889
1376
  roles: Set<string>,
890
1377
  skills: Set<string>,
891
1378
  memberIds: Set<string>,
892
- ): CpsatRuleConfigEntry[] {
1379
+ ): AnyCpsatRuleConfigEntry[] {
893
1380
  const ctx: RuleResolveContext = { roles, skills, memberIds };
894
1381
 
895
1382
  return rules.map((rule) => {
896
1383
  // Rules with custom resolvers handle their own translation
897
1384
  if (rule._resolve) {
898
- return rule._resolve(ctx) as CpsatRuleConfigEntry;
1385
+ return rule._resolve(ctx) as AnyCpsatRuleConfigEntry;
899
1386
  }
900
1387
 
901
1388
  // Default resolution: appliesTo → entity scope, dates → specificDates
902
1389
  const { _type, _rule, _resolve, appliesTo, dates, ...passthrough } = rule as RuleEntry & {
903
1390
  appliesTo?: string | string[];
904
- dates?: string[];
1391
+ dates?: DateString[];
905
1392
  };
906
1393
 
907
1394
  const entityScope = resolveAppliesTo(appliesTo, roles, skills, memberIds);
@@ -912,8 +1399,8 @@ function resolveRules(
912
1399
  ...passthrough,
913
1400
  ...entityScope,
914
1401
  ...resolvedDates,
915
- } as CpsatRuleConfigEntry;
916
- }) as CpsatRuleConfigEntry[];
1402
+ } as AnyCpsatRuleConfigEntry;
1403
+ }) as AnyCpsatRuleConfigEntry[];
917
1404
  }
918
1405
 
919
1406
  // ============================================================================
@@ -927,7 +1414,9 @@ function resolveRules(
927
1414
  * (multipliers, surcharges) reference cost variables it creates.
928
1415
  * Non-cost rules retain their original relative order.
929
1416
  */
930
- function sortCostRulesFirst(rules: RuleEntry[]): RuleEntry[] {
1417
+ function sortCostRulesFirst(
1418
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[],
1419
+ ): ScheduleRuleEntry<CpsatRuleRegistry>[] {
931
1420
  return rules.toSorted((a, b) => {
932
1421
  const aIsCostBase = a._rule === "minimize-cost" ? 0 : 1;
933
1422
  const bIsCostBase = b._rule === "minimize-cost" ? 0 : 1;