dabke 0.82.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 (341) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +22 -24
  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 -21
  66. package/dist/cpsat/rules/index.d.ts.map +1 -1
  67. package/dist/cpsat/rules/index.js +30 -21
  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 +30 -0
  86. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
  87. package/dist/cpsat/rules/max-days-week.js +83 -0
  88. package/dist/cpsat/rules/max-days-week.js.map +1 -0
  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 +21 -0
  110. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
  111. package/dist/cpsat/rules/min-days-week.js +95 -0
  112. package/dist/cpsat/rules/min-days-week.js.map +1 -0
  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 +12 -0
  130. package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
  131. package/dist/cpsat/rules/must-assign.js +54 -0
  132. package/dist/cpsat/rules/must-assign.js.map +1 -0
  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 -27
  162. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  163. package/dist/cpsat/rules/registry.js +98 -31
  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 +93 -17
  174. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  175. package/dist/cpsat/rules/scope.types.d.ts +25 -12
  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 +15 -9
  226. package/dist/index.d.ts.map +1 -1
  227. package/dist/index.js +11 -4
  228. package/dist/index.js.map +1 -1
  229. package/dist/schedule/cost.d.ts +192 -0
  230. package/dist/schedule/cost.d.ts.map +1 -0
  231. package/dist/schedule/cost.js +151 -0
  232. package/dist/schedule/cost.js.map +1 -0
  233. package/dist/schedule/coverage.d.ts +114 -0
  234. package/dist/schedule/coverage.d.ts.map +1 -0
  235. package/dist/schedule/coverage.js +56 -0
  236. package/dist/schedule/coverage.js.map +1 -0
  237. package/dist/schedule/definition.d.ts +346 -0
  238. package/dist/schedule/definition.d.ts.map +1 -0
  239. package/dist/schedule/definition.js +668 -0
  240. package/dist/schedule/definition.js.map +1 -0
  241. package/dist/schedule/index.d.ts +67 -0
  242. package/dist/schedule/index.d.ts.map +1 -0
  243. package/dist/schedule/index.js +69 -0
  244. package/dist/schedule/index.js.map +1 -0
  245. package/dist/schedule/rules.d.ts +678 -0
  246. package/dist/schedule/rules.d.ts.map +1 -0
  247. package/dist/schedule/rules.js +554 -0
  248. package/dist/schedule/rules.js.map +1 -0
  249. package/dist/schedule/shift-patterns.d.ts +17 -0
  250. package/dist/schedule/shift-patterns.d.ts.map +1 -0
  251. package/dist/schedule/shift-patterns.js +24 -0
  252. package/dist/schedule/shift-patterns.js.map +1 -0
  253. package/dist/schedule/time-periods.d.ts +79 -0
  254. package/dist/schedule/time-periods.d.ts.map +1 -0
  255. package/dist/schedule/time-periods.js +101 -0
  256. package/dist/schedule/time-periods.js.map +1 -0
  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 -14
  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 -21
  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 +117 -0
  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 +122 -0
  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 +71 -0
  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 -56
  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 +92 -17
  309. package/src/cpsat/rules/scope.types.ts +32 -27
  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 +72 -15
  323. package/src/schedule/cost.ts +221 -0
  324. package/src/schedule/coverage.ts +164 -0
  325. package/src/schedule/definition.ts +1447 -0
  326. package/src/schedule/index.ts +132 -0
  327. package/src/schedule/rules.ts +1028 -0
  328. package/src/schedule/shift-patterns.ts +29 -0
  329. package/src/schedule/time-periods.ts +120 -0
  330. package/src/types.ts +114 -2
  331. package/dist/llms.d.ts +0 -2
  332. package/dist/llms.d.ts.map +0 -1
  333. package/dist/llms.js +0 -3
  334. package/dist/llms.js.map +0 -1
  335. package/dist/schedule.d.ts +0 -917
  336. package/dist/schedule.d.ts.map +0 -1
  337. package/dist/schedule.js +0 -1323
  338. package/dist/schedule.js.map +0 -1
  339. package/llms.txt +0 -758
  340. package/src/llms.ts +0 -3
  341. package/src/schedule.ts +0 -1960
@@ -0,0 +1,1447 @@
1
+ /**
2
+ * Schedule definition, compilation, and solving.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import type { DateString, DayOfWeek, SchedulingPeriod } from "../types.js";
8
+ import type { SemanticTimeEntry } from "../cpsat/semantic-time.js";
9
+ import type { MixedCoverageRequirement, CoverageVariant } from "../cpsat/semantic-time.js";
10
+ import { defineSemanticTimes } from "../cpsat/semantic-time.js";
11
+ import { resolveDaysFromPeriod } from "../datetime.utils.js";
12
+ import type { ModelBuilderConfig, CompilationResult } from "../cpsat/model-builder.js";
13
+ import { ModelBuilder } from "../cpsat/model-builder.js";
14
+ import type { SchedulingMember, ShiftPattern, Priority } from "../cpsat/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";
27
+ import { parseSolverResponse, resolveAssignments } from "../cpsat/response.js";
28
+ import type { ShiftAssignment } from "../cpsat/response.js";
29
+ import type { ScheduleValidation } from "../cpsat/validation.types.js";
30
+ import { calculateScheduleCost } from "../cpsat/cost.js";
31
+ import type { CostBreakdown } from "../cpsat/cost.js";
32
+
33
+ import type { CoverageEntry } from "./coverage.js";
34
+ import type { RuleEntry, RuleResolveContext, ScheduleRuleEntry } from "./rules.js";
35
+ import { resolveAppliesTo } from "./rules.js";
36
+
37
+ /** A value that can be passed to {@link Schedule.with}. */
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
+ >;
267
+
268
+ // ============================================================================
269
+ // SolveResult
270
+ // ============================================================================
271
+
272
+ /** Status of a solve attempt, using idiomatic lowercase TypeScript literals. */
273
+ export type SolveStatus = "optimal" | "feasible" | "infeasible" | "no_solution";
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
+
297
+ /**
298
+ * Result of {@link Schedule.solve}.
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
+ *
305
+ * @category Schedule Definition
306
+ */
307
+ export interface SolveResult {
308
+ /** Outcome of the solve attempt. */
309
+ status: SolveStatus;
310
+ /** Shift assignments (empty when infeasible or no solution). */
311
+ assignments: ShiftAssignment[];
312
+ /** Validation results collected during compilation and solving. */
313
+ validation: ScheduleValidation;
314
+ /** Cost breakdown for optimized solves when cost rules are used and a solution is found. */
315
+ cost?: CostBreakdown;
316
+ }
317
+
318
+ /**
319
+ * Options for {@link Schedule.solve} and {@link Schedule.compile}.
320
+ *
321
+ * @category Schedule Definition
322
+ */
323
+ export interface SolveOptions {
324
+ /** The date range to schedule. */
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;
337
+ /**
338
+ * Fixed assignments from a prior solve (e.g., rolling schedule).
339
+ * These are injected as fixed variables in the solver.
340
+ *
341
+ * Not yet implemented. Providing pinned assignments throws an error.
342
+ */
343
+ pinned?: ShiftAssignment[];
344
+ }
345
+
346
+ // ============================================================================
347
+ // Schedule Configuration
348
+ // ============================================================================
349
+
350
+ /**
351
+ * Configuration for {@link schedule}.
352
+ *
353
+ * @remarks
354
+ * Coverage entries for the same semantic time and target stack additively.
355
+ * An unscoped entry applies every day; adding a weekend-only entry on top
356
+ * doubles the count on those days. Use mutually exclusive `dayOfWeek` on
357
+ * both entries to avoid stacking. See {@link cover} for details.
358
+ *
359
+ * `roleIds`, `times`, `coverage`, and `shiftPatterns` are required.
360
+ * These four fields form the minimum solvable schedule.
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
+ *
366
+ * @category Schedule Definition
367
+ */
368
+ export interface ScheduleConfig<
369
+ R extends readonly string[] = readonly string[],
370
+ S extends readonly string[] = readonly [],
371
+ T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
372
+ TRuleRegistry extends BuiltInRuleRegistry = BuiltInRuleRegistry,
373
+ > {
374
+ /** Declared role IDs. */
375
+ roleIds: R;
376
+ /** Declared skill IDs. When omitted, coverage targets can only be roles. */
377
+ skillIds?: S;
378
+ /** Named semantic time periods. */
379
+ times: T;
380
+ /** Staffing requirements per time period (entries stack additively). */
381
+ coverage: NoInfer<CoverageEntry<keyof T & string, R[number] | S[number]>>[];
382
+ /** Available shift patterns. */
383
+ shiftPatterns: ShiftPattern[];
384
+ /** Scheduling rules and constraints. */
385
+ rules?: ScheduleRuleEntry<TRuleRegistry>[];
386
+ /**
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.
391
+ * Built-in rule names cannot be overridden.
392
+ */
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>[];
430
+ /** Team members (typically added via `.with()` at runtime). */
431
+ members?: SchedulingMember[];
432
+ /** Days of the week the business operates (inclusion filter). */
433
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
434
+ /** Which day starts the week for weekly rules. Defaults to `"monday"`. */
435
+ weekStartsOn?: DayOfWeek;
436
+ }
437
+
438
+ // ============================================================================
439
+ // Internal merged config
440
+ // ============================================================================
441
+
442
+ /** Internal representation of a fully merged schedule. */
443
+ interface MergedScheduleConfig {
444
+ roleIds: string[];
445
+ skillIds: string[];
446
+ times: Record<string, SemanticTimeEntry>;
447
+ coverage: CoverageEntry[];
448
+ shiftPatterns: ShiftPattern[];
449
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[];
450
+ ruleRegistry: CpsatRuleRegistry;
451
+ members: SchedulingMember[];
452
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
453
+ weekStartsOn?: DayOfWeek;
454
+ }
455
+
456
+ // ============================================================================
457
+ // Schedule class
458
+ // ============================================================================
459
+
460
+ /**
461
+ * An immutable schedule definition.
462
+ *
463
+ * Created by {@link schedule}, composed via {@link Schedule.with},
464
+ * and solved via {@link Schedule.solve}.
465
+ *
466
+ * @category Schedule Definition
467
+ */
468
+ export class Schedule<TRuleRegistry extends CpsatRuleRegistry = BuiltInRuleRegistry> {
469
+ readonly #config: Readonly<MergedScheduleConfig>;
470
+
471
+ /** @internal */
472
+ constructor(config: MergedScheduleConfig) {
473
+ this.#config = config;
474
+ }
475
+
476
+ /** @internal Returns a defensive copy of the config for merging. */
477
+ _getConfig(): MergedScheduleConfig {
478
+ return {
479
+ ...this.#config,
480
+ roleIds: [...this.#config.roleIds],
481
+ skillIds: [...this.#config.skillIds],
482
+ times: { ...this.#config.times },
483
+ coverage: [...this.#config.coverage],
484
+ shiftPatterns: [...this.#config.shiftPatterns],
485
+ rules: [...this.#config.rules],
486
+ ruleRegistry: { ...this.#config.ruleRegistry },
487
+ members: [...this.#config.members],
488
+ };
489
+ }
490
+
491
+ // --------------------------------------------------------------------------
492
+ // Inspection
493
+ // --------------------------------------------------------------------------
494
+
495
+ /** Declared role IDs. */
496
+ get roleIds(): readonly string[] {
497
+ return this.#config.roleIds;
498
+ }
499
+
500
+ /** Declared skill IDs. */
501
+ get skillIds(): readonly string[] {
502
+ return this.#config.skillIds;
503
+ }
504
+
505
+ /** Names of declared semantic times. */
506
+ get timeNames(): readonly string[] {
507
+ return Object.keys(this.#config.times);
508
+ }
509
+
510
+ /** Shift pattern IDs. */
511
+ get shiftPatternIds(): readonly string[] {
512
+ return this.#config.shiftPatterns.map((sp) => sp.id);
513
+ }
514
+
515
+ /** Internal rule identifiers in kebab-case. */
516
+ get ruleNames(): readonly string[] {
517
+ return this.#config.rules.map((r) => r._rule);
518
+ }
519
+
520
+ // --------------------------------------------------------------------------
521
+ // Composition
522
+ // --------------------------------------------------------------------------
523
+
524
+ /**
525
+ * Merges schedules or members onto this schedule, returning a new
526
+ * immutable `Schedule`. The original is untouched.
527
+ *
528
+ * Accepts any mix of `Schedule` instances and `SchedulingMember[]` arrays.
529
+ *
530
+ * Merge semantics (when merging schedules):
531
+ * - Roles: union (additive)
532
+ * - Skills: union (additive)
533
+ * - Times: additive; error on name collision
534
+ * - Coverage: additive
535
+ * - Shift patterns: additive; error on ID collision
536
+ * - Rules: additive
537
+ * - Members: additive; error on duplicate ID
538
+ *
539
+ * Validation runs eagerly: role/skill disjointness, coverage targets
540
+ * referencing declared roles/skills, member role references, etc.
541
+ */
542
+ with<const Args extends readonly WithArg[]>(
543
+ ...args: Args
544
+ ): Schedule<TRuleRegistry & MergedRegistryFromArgs<Args>> {
545
+ const merged = mergeConfig(this.#config, args);
546
+ return new Schedule(merged);
547
+ }
548
+
549
+ // --------------------------------------------------------------------------
550
+ // Solve / compile
551
+ // --------------------------------------------------------------------------
552
+
553
+ /**
554
+ * Compiles, validates, solves, and parses in one call.
555
+ *
556
+ * @param client - Solver client (e.g., `new HttpSolverClient(fetch, url)`)
557
+ * @param options - Date range and optional pinned assignments
558
+ */
559
+ async solve(client: SolverClient, options: SolveOptions): Promise<SolveResult> {
560
+ const strategy = resolveSolveStrategy(options.strategy);
561
+ const compiled = this.compile(options);
562
+ if (!compiled.canSolve) {
563
+ return {
564
+ status: "infeasible",
565
+ assignments: [],
566
+ validation: compiled.validation,
567
+ };
568
+ }
569
+
570
+ const response = await client.solve(compiled.request);
571
+ return buildSolveResult(response, compiled, this.#config, strategy);
572
+ }
573
+
574
+ /**
575
+ * Diagnostic escape hatch. Compiles the schedule without solving.
576
+ *
577
+ * @param options - Date range and optional pinned assignments
578
+ */
579
+ compile(options: SolveOptions): CompilationResult & { builder: ModelBuilder } {
580
+ if (options.pinned && options.pinned.length > 0) {
581
+ throw new Error("Pinned assignments are not yet supported.");
582
+ }
583
+
584
+ const strategy = resolveSolveStrategy(options.strategy);
585
+ const modelConfig = resolveToModelConfig(this.#config, options, strategy);
586
+ const builder = new ModelBuilder(modelConfig);
587
+ const result = builder.compile();
588
+ return { ...result, builder };
589
+ }
590
+ }
591
+
592
+ // ============================================================================
593
+ // schedule() factory
594
+ // ============================================================================
595
+
596
+ /**
597
+ * Create a schedule definition.
598
+ *
599
+ * Returns an immutable {@link Schedule} that can be composed via `.with()`
600
+ * and solved via `.solve()`.
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
+ *
606
+ * @example
607
+ * ```typescript
608
+ * const venue = schedule({
609
+ * roleIds: ["waiter", "runner", "manager"],
610
+ * skillIds: ["senior"],
611
+ * times: {
612
+ * lunch: time({ startTime: t(12), endTime: t(15) }),
613
+ * dinner: time(
614
+ * { startTime: t(17), endTime: t(21) },
615
+ * { startTime: t(18), endTime: t(22), dayOfWeek: weekend },
616
+ * ),
617
+ * },
618
+ * coverage: [
619
+ * cover("lunch", "waiter", 2),
620
+ * cover("dinner", "waiter", 4, { dayOfWeek: weekdays }),
621
+ * cover("dinner", "waiter", 5, { dayOfWeek: weekend }),
622
+ * cover("dinner", "manager", 1),
623
+ * ],
624
+ * shiftPatterns: [
625
+ * shift("lunch_shift", t(11, 30), t(15)),
626
+ * shift("evening", t(17), t(22)),
627
+ * ],
628
+ * rules: [
629
+ * maxHoursPerDay(10),
630
+ * maxHoursPerWeek(48),
631
+ * minRestBetweenShifts(11),
632
+ * ],
633
+ * });
634
+ * ```
635
+ *
636
+ * @category Schedule Definition
637
+ */
638
+ export function schedule<
639
+ const R extends readonly string[],
640
+ const S extends readonly string[] = readonly [],
641
+ const T extends Record<string, SemanticTimeEntry> = Record<string, SemanticTimeEntry>,
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);
701
+ validateConfig(merged);
702
+ return new Schedule(merged);
703
+ }
704
+
705
+ /**
706
+ * Create a partial schedule for composition via `.with()`.
707
+ *
708
+ * Unlike {@link schedule}, all fields are optional. Use this for
709
+ * schedules that layer rules, coverage, or other config onto a
710
+ * complete base schedule.
711
+ *
712
+ * @example
713
+ * ```typescript
714
+ * const companyPolicy = partialSchedule({
715
+ * rules: [maxHoursPerWeek(40), minRestBetweenShifts(11)],
716
+ * });
717
+ *
718
+ * const ready = venue.with(companyPolicy, teamMembers);
719
+ * ```
720
+ *
721
+ * @category Schedule Definition
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>;
734
+ export function partialSchedule(
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
+ );
763
+ validateConfig(merged);
764
+ return new Schedule(merged);
765
+ }
766
+
767
+ // ============================================================================
768
+ // Internal: Build merged config from user input
769
+ // ============================================================================
770
+
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 {
787
+ return {
788
+ roleIds: [...config.roleIds],
789
+ skillIds: [...(config.skillIds ?? [])],
790
+ times: { ...config.times },
791
+ coverage: [...config.coverage],
792
+ shiftPatterns: [...config.shiftPatterns],
793
+ rules: [...(config.rules ?? [])] as ScheduleRuleEntry<CpsatRuleRegistry>[],
794
+ ruleRegistry: { ...ruleRegistry },
795
+ members: [...(config.members ?? [])],
796
+ dayOfWeek: config.dayOfWeek,
797
+ weekStartsOn: config.weekStartsOn,
798
+ };
799
+ }
800
+
801
+ // ============================================================================
802
+ // Internal: Validate merged config
803
+ // ============================================================================
804
+
805
+ function validateRuleRegistryExtension(ruleRegistry: CpsatRuleRegistry): void {
806
+ assertValidCpsatRuleRegistry(ruleRegistry);
807
+ assertNoBuiltInCpsatRuleOverrides(ruleRegistry);
808
+ }
809
+
810
+ function validateConfig(config: MergedScheduleConfig): void {
811
+ const roles = new Set<string>(config.roleIds);
812
+ const skills = new Set<string>(config.skillIds);
813
+
814
+ assertValidCpsatRuleRegistry(config.ruleRegistry);
815
+
816
+ // Validate role/skill disjointness
817
+ for (const skill of skills) {
818
+ if (roles.has(skill)) {
819
+ throw new Error(
820
+ `"${skill}" is declared as both a role and a skill. Roles and skills must be disjoint.`,
821
+ );
822
+ }
823
+ }
824
+
825
+ // Validate shift pattern role references
826
+ for (const sp of config.shiftPatterns) {
827
+ if (sp.roleIds) {
828
+ for (const role of sp.roleIds) {
829
+ if (!roles.has(role)) {
830
+ throw new Error(
831
+ `Shift pattern "${sp.id}" references unknown role "${role}". ` +
832
+ `Declared roles: ${[...roles].join(", ")}`,
833
+ );
834
+ }
835
+ }
836
+ }
837
+ }
838
+
839
+ // Validate coverage entries
840
+ for (const entry of config.coverage) {
841
+ validateCoverageEntry(entry, roles, skills);
842
+ }
843
+
844
+ // Validate member references
845
+ const memberIds = new Set<string>();
846
+ for (const member of config.members) {
847
+ if (memberIds.has(member.id)) {
848
+ throw new Error(`Duplicate member ID "${member.id}".`);
849
+ }
850
+ memberIds.add(member.id);
851
+
852
+ if (roles.has(member.id)) {
853
+ throw new Error(`Member ID "${member.id}" collides with a declared role name.`);
854
+ }
855
+ if (skills.has(member.id)) {
856
+ throw new Error(`Member ID "${member.id}" collides with a declared skill name.`);
857
+ }
858
+
859
+ for (const role of member.roleIds) {
860
+ if (!roles.has(role)) {
861
+ throw new Error(
862
+ `Member "${member.id}" references unknown role "${role}". ` +
863
+ `Declared roles: ${[...roles].join(", ")}`,
864
+ );
865
+ }
866
+ }
867
+ if (member.skillIds) {
868
+ for (const skill of member.skillIds) {
869
+ if (!skills.has(skill)) {
870
+ throw new Error(
871
+ `Member "${member.id}" references unknown skill "${skill}". ` +
872
+ `Declared skills: ${[...skills].join(", ")}`,
873
+ );
874
+ }
875
+ }
876
+ }
877
+ }
878
+ }
879
+
880
+ function validateCoverageEntry(
881
+ entry: CoverageEntry,
882
+ roles: Set<string>,
883
+ skills: Set<string>,
884
+ ): void {
885
+ const targets = Array.isArray(entry.target) ? entry.target : [entry.target];
886
+ if (Array.isArray(entry.target)) {
887
+ for (const target of targets) {
888
+ if (!roles.has(target)) {
889
+ throw new Error(
890
+ `Coverage for "${entry.timeName}" references "${target}" in a role OR group, ` +
891
+ `but it is not a declared role. Declared roles: ${[...roles].join(", ")}`,
892
+ );
893
+ }
894
+ }
895
+ } else {
896
+ if (!roles.has(entry.target) && !skills.has(entry.target)) {
897
+ throw new Error(
898
+ `Coverage for "${entry.timeName}" references unknown target "${entry.target}". ` +
899
+ `Declared roles: ${[...roles].join(", ")}. ` +
900
+ `Declared skills: ${[...skills].join(", ")}`,
901
+ );
902
+ }
903
+ }
904
+ if (entry.options.skillIds) {
905
+ for (const s of entry.options.skillIds) {
906
+ if (!skills.has(s)) {
907
+ throw new Error(
908
+ `Coverage for "${entry.timeName}" uses skill filter "${s}" ` +
909
+ `which is not a declared skill. Declared skills: ${[...skills].join(", ")}`,
910
+ );
911
+ }
912
+ }
913
+ }
914
+ }
915
+
916
+ // ============================================================================
917
+ // Internal: Merge logic
918
+ // ============================================================================
919
+
920
+ function mergeConfig(
921
+ base: Readonly<MergedScheduleConfig>,
922
+ args: readonly WithArg[],
923
+ ): MergedScheduleConfig {
924
+ const result: MergedScheduleConfig = {
925
+ roleIds: [...base.roleIds],
926
+ skillIds: [...base.skillIds],
927
+ times: { ...base.times },
928
+ coverage: [...base.coverage],
929
+ shiftPatterns: [...base.shiftPatterns],
930
+ rules: [...base.rules],
931
+ ruleRegistry: { ...base.ruleRegistry },
932
+ members: [...base.members],
933
+ dayOfWeek: base.dayOfWeek,
934
+ weekStartsOn: base.weekStartsOn,
935
+ };
936
+
937
+ for (const arg of args) {
938
+ if (arg instanceof Schedule) {
939
+ mergeScheduleFragment(result, arg);
940
+ } else if (Array.isArray(arg)) {
941
+ mergeMembers(result, arg);
942
+ } else {
943
+ throw new Error(
944
+ `Unexpected argument passed to .with(): expected Schedule or SchedulingMember[], got ${typeof arg}`,
945
+ );
946
+ }
947
+ }
948
+
949
+ // Validate the merged result
950
+ validateConfig(result);
951
+ return result;
952
+ }
953
+
954
+ function mergeScheduleFragment(result: MergedScheduleConfig, s: Schedule<CpsatRuleRegistry>): void {
955
+ const other = s._getConfig();
956
+
957
+ // dayOfWeek: error on conflict (semantics of union vs intersection are ambiguous)
958
+ if (other.dayOfWeek !== undefined) {
959
+ if (result.dayOfWeek !== undefined) {
960
+ const baseSet = new Set(result.dayOfWeek);
961
+ const same =
962
+ result.dayOfWeek.length === other.dayOfWeek.length &&
963
+ other.dayOfWeek.every((d) => baseSet.has(d));
964
+ if (!same) {
965
+ throw new Error(
966
+ "Cannot merge schedules with different dayOfWeek filters. " +
967
+ `Base has [${result.dayOfWeek.join(", ")}], ` +
968
+ `incoming has [${other.dayOfWeek.join(", ")}].`,
969
+ );
970
+ }
971
+ } else {
972
+ result.dayOfWeek = other.dayOfWeek;
973
+ }
974
+ }
975
+
976
+ // weekStartsOn: error on conflict (only one week boundary for weekly rules)
977
+ if (other.weekStartsOn !== undefined) {
978
+ if (result.weekStartsOn !== undefined && result.weekStartsOn !== other.weekStartsOn) {
979
+ throw new Error(
980
+ "Cannot merge schedules with different weekStartsOn values. " +
981
+ `Base has "${result.weekStartsOn}", incoming has "${other.weekStartsOn}".`,
982
+ );
983
+ }
984
+ result.weekStartsOn = other.weekStartsOn;
985
+ }
986
+
987
+ // Roles: union
988
+ for (const role of other.roleIds) {
989
+ if (!result.roleIds.includes(role)) {
990
+ result.roleIds.push(role);
991
+ }
992
+ }
993
+
994
+ // Skills: union
995
+ for (const skill of other.skillIds) {
996
+ if (!result.skillIds.includes(skill)) {
997
+ result.skillIds.push(skill);
998
+ }
999
+ }
1000
+
1001
+ // Times: additive, error on collision
1002
+ for (const [name, entry] of Object.entries(other.times)) {
1003
+ if (name in result.times) {
1004
+ throw new Error(
1005
+ `Time name "${name}" already exists. Cannot merge schedules with colliding time names.`,
1006
+ );
1007
+ }
1008
+ result.times[name] = entry;
1009
+ }
1010
+
1011
+ // Coverage: additive
1012
+ result.coverage.push(...other.coverage);
1013
+
1014
+ // Shift patterns: additive, error on ID collision
1015
+ const existingIds = new Set(result.shiftPatterns.map((sp) => sp.id));
1016
+ for (const sp of other.shiftPatterns) {
1017
+ if (existingIds.has(sp.id)) {
1018
+ throw new Error(
1019
+ `Shift pattern ID "${sp.id}" already exists. Cannot merge schedules with colliding shift pattern IDs.`,
1020
+ );
1021
+ }
1022
+ result.shiftPatterns.push(sp);
1023
+ existingIds.add(sp.id);
1024
+ }
1025
+
1026
+ // Rules: additive
1027
+ result.rules.push(...other.rules);
1028
+
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) {
1032
+ throw new Error(
1033
+ `Rule registry entry "${name}" already registered. Cannot merge schedules with colliding rule registries.`,
1034
+ );
1035
+ }
1036
+ result.ruleRegistry[name] = descriptor;
1037
+ }
1038
+
1039
+ // Members: additive, error on duplicate ID
1040
+ const existingMemberIds = new Set(result.members.map((m) => m.id));
1041
+ for (const member of other.members) {
1042
+ if (existingMemberIds.has(member.id)) {
1043
+ throw new Error(
1044
+ `Duplicate member ID "${member.id}". Cannot merge schedules with colliding member IDs.`,
1045
+ );
1046
+ }
1047
+ result.members.push(member);
1048
+ existingMemberIds.add(member.id);
1049
+ }
1050
+ }
1051
+
1052
+ function mergeMembers(result: MergedScheduleConfig, incoming: SchedulingMember[]): void {
1053
+ const existingIds = new Set(result.members.map((m) => m.id));
1054
+ for (const member of incoming) {
1055
+ if (existingIds.has(member.id)) {
1056
+ throw new Error(
1057
+ `Duplicate member ID "${member.id}". Cannot merge members with colliding IDs.`,
1058
+ );
1059
+ }
1060
+ result.members.push(member);
1061
+ existingIds.add(member.id);
1062
+ }
1063
+ }
1064
+
1065
+ // ============================================================================
1066
+ // Internal: Resolve to ModelBuilderConfig
1067
+ // ============================================================================
1068
+
1069
+ function resolveToModelConfig(
1070
+ config: Readonly<MergedScheduleConfig>,
1071
+ options: SolveOptions,
1072
+ strategy: SolveStrategy,
1073
+ ): ModelBuilderConfig<CpsatRuleRegistry, AnyCpsatRuleConfigEntry> {
1074
+ const roles = new Set<string>(config.roleIds);
1075
+ const skills = new Set<string>(config.skillIds);
1076
+ const memberIds = new Set<string>(config.members.map((m) => m.id));
1077
+
1078
+ // Build semantic time context
1079
+ const semanticTimes = defineSemanticTimes(config.times);
1080
+
1081
+ // Convert coverage entries to semantic coverage requirements
1082
+ const coverageReqs = buildCoverageRequirements(config.coverage, roles, skills);
1083
+
1084
+ // Resolve scheduling period with dayOfWeek filter
1085
+ const schedulingPeriod: SchedulingPeriod = {
1086
+ dateRange: options.dateRange,
1087
+ };
1088
+ const resolvedPeriod = applyDaysFilter(schedulingPeriod, config.dayOfWeek);
1089
+ const days = resolveDaysFromPeriod(resolvedPeriod);
1090
+
1091
+ // Resolve coverage
1092
+ const resolvedCoverage = semanticTimes.resolve(coverageReqs, days);
1093
+
1094
+ // Resolve rules
1095
+ const allRules = [...config.rules];
1096
+
1097
+ // Validate pay data when cost rules are present
1098
+ const costRuleNames = new Set([
1099
+ "minimize-cost",
1100
+ "day-cost-multiplier",
1101
+ "day-cost-surcharge",
1102
+ "time-cost-surcharge",
1103
+ "overtime-weekly-multiplier",
1104
+ "overtime-weekly-surcharge",
1105
+ "overtime-daily-multiplier",
1106
+ "overtime-daily-surcharge",
1107
+ "overtime-tiered-multiplier",
1108
+ ]);
1109
+ const hasCostRules = allRules.some((r) => costRuleNames.has(r._rule));
1110
+ if (strategy.type === "optimize" && hasCostRules) {
1111
+ const missingPay = config.members.filter((m) => !m.pay).map((m) => m.id);
1112
+ if (missingPay.length > 0) {
1113
+ throw new Error(
1114
+ `Cost rules require pay data on all members. Missing pay: ${missingPay.join(", ")}`,
1115
+ );
1116
+ }
1117
+ }
1118
+
1119
+ // Sort rules so minimize-cost compiles before modifier rules
1120
+ const sortedRules = sortCostRulesFirst(allRules);
1121
+ const ruleConfigs = resolveRules(sortedRules, roles, skills, memberIds);
1122
+
1123
+ return {
1124
+ members: config.members,
1125
+ shiftPatterns: config.shiftPatterns,
1126
+ schedulingPeriod: resolvedPeriod,
1127
+ coverage: resolvedCoverage,
1128
+ ruleConfigs,
1129
+ ruleRegistry: config.ruleRegistry,
1130
+ weekStartsOn: config.weekStartsOn,
1131
+ solverOptions: buildSolverOptions(options),
1132
+ strategy,
1133
+ };
1134
+ }
1135
+
1136
+ // ============================================================================
1137
+ // Internal: Build SolveResult from solver response
1138
+ // ============================================================================
1139
+
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 {
1168
+ switch (solverStatus) {
1169
+ case "OPTIMAL":
1170
+ return strategy.type === "feasibility-only" ? "feasible" : "optimal";
1171
+ case "FEASIBLE":
1172
+ return "feasible";
1173
+ case "INFEASIBLE":
1174
+ return "infeasible";
1175
+ case "TIMEOUT":
1176
+ case "ERROR":
1177
+ return "no_solution";
1178
+ default:
1179
+ return "no_solution";
1180
+ }
1181
+ }
1182
+
1183
+ function buildSolveResult(
1184
+ response: SolverResponse,
1185
+ compiled: CompilationResult & { builder: ModelBuilder },
1186
+ config: Readonly<MergedScheduleConfig>,
1187
+ strategy: SolveStrategy,
1188
+ ): SolveResult {
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
+ });
1203
+
1204
+ // Run post-solve validation when a solution exists
1205
+ if (parsed.assignments.length > 0 && hasFinalSolution && strategy.type === "optimize") {
1206
+ const resolved = resolveAssignments(parsed.assignments, compiled.builder.shiftPatterns);
1207
+ compiled.builder.validateSolution(resolved);
1208
+ }
1209
+
1210
+ const validation = compiled.builder.reporter.getValidation();
1211
+
1212
+ const result: SolveResult = {
1213
+ status,
1214
+ assignments: parsed.assignments,
1215
+ validation,
1216
+ };
1217
+
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) {
1224
+ result.cost = calculateScheduleCost(parsed.assignments, {
1225
+ members: config.members,
1226
+ shiftPatterns: config.shiftPatterns,
1227
+ rules: compiled.builder.rules,
1228
+ days: compiled.builder.days,
1229
+ weekStartsOn: compiled.builder.weekStartsOn,
1230
+ });
1231
+ }
1232
+ }
1233
+
1234
+ return result;
1235
+ }
1236
+
1237
+ // ============================================================================
1238
+ // Internal: Coverage Translation
1239
+ // ============================================================================
1240
+
1241
+ function buildCoverageRequirements<T extends string>(
1242
+ entries: CoverageEntry<T, string>[],
1243
+ roles: Set<string>,
1244
+ skills: Set<string>,
1245
+ ): MixedCoverageRequirement<T>[] {
1246
+ return entries.map((entry) => {
1247
+ // Variant form: produce a VariantCoverageRequirement
1248
+ if (entry.variants) {
1249
+ return buildVariantCoverageRequirement(entry, roles, skills);
1250
+ }
1251
+
1252
+ // Simple form: produce a SemanticCoverageRequirement
1253
+ const base: {
1254
+ semanticTime: T;
1255
+ targetCount: number;
1256
+ priority?: Priority;
1257
+ dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
1258
+ dates?: DateString[];
1259
+ } = {
1260
+ semanticTime: entry.timeName,
1261
+ targetCount: entry.count,
1262
+ };
1263
+
1264
+ if (entry.options.priority) base.priority = entry.options.priority;
1265
+ if (entry.options.dayOfWeek && entry.options.dayOfWeek.length > 0) {
1266
+ base.dayOfWeek = entry.options.dayOfWeek as [DayOfWeek, ...DayOfWeek[]];
1267
+ }
1268
+ if (entry.options.dates) base.dates = entry.options.dates;
1269
+
1270
+ return buildSimpleCoverageTarget(entry, base, roles, skills);
1271
+ }) as MixedCoverageRequirement<T>[];
1272
+ }
1273
+
1274
+ /**
1275
+ * Resolve the target (role/skill) for a simple coverage entry.
1276
+ */
1277
+ function buildSimpleCoverageTarget<T extends string>(
1278
+ entry: CoverageEntry<T, string>,
1279
+ base: {
1280
+ semanticTime: T;
1281
+ targetCount: number;
1282
+ priority?: Priority;
1283
+ dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
1284
+ dates?: DateString[];
1285
+ },
1286
+ roles: Set<string>,
1287
+ skills: Set<string>,
1288
+ ): MixedCoverageRequirement<T> {
1289
+ if (Array.isArray(entry.target)) {
1290
+ return {
1291
+ ...base,
1292
+ roleIds: entry.target as [string, ...string[]],
1293
+ } satisfies MixedCoverageRequirement<T>;
1294
+ }
1295
+
1296
+ const singleTarget = entry.target as string;
1297
+ if (roles.has(singleTarget)) {
1298
+ if (entry.options.skillIds) {
1299
+ return {
1300
+ ...base,
1301
+ roleIds: [singleTarget] as [string, ...string[]],
1302
+ skillIds: entry.options.skillIds,
1303
+ } satisfies MixedCoverageRequirement<T>;
1304
+ }
1305
+ return {
1306
+ ...base,
1307
+ roleIds: [singleTarget] as [string, ...string[]],
1308
+ } satisfies MixedCoverageRequirement<T>;
1309
+ }
1310
+
1311
+ if (skills.has(singleTarget)) {
1312
+ return {
1313
+ ...base,
1314
+ skillIds: [singleTarget] as [string, ...string[]],
1315
+ } satisfies MixedCoverageRequirement<T>;
1316
+ }
1317
+
1318
+ throw new Error(`Coverage target "${singleTarget}" is not a declared role or skill.`);
1319
+ }
1320
+
1321
+ /**
1322
+ * Build a VariantCoverageRequirement from a variant-form CoverageEntry.
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
+
1339
+ function buildVariantCoverageRequirement<T extends string>(
1340
+ entry: CoverageEntry<T, string>,
1341
+ roles: Set<string>,
1342
+ skills: Set<string>,
1343
+ ): MixedCoverageRequirement<T> {
1344
+ const variants = requireCoverageVariants(entry);
1345
+
1346
+ const resolveTarget = (): {
1347
+ roleIds?: [string, ...string[]];
1348
+ skillIds?: [string, ...string[]];
1349
+ } => {
1350
+ if (Array.isArray(entry.target)) {
1351
+ return { roleIds: entry.target as [string, ...string[]] };
1352
+ }
1353
+ const singleTarget = entry.target as string;
1354
+ if (roles.has(singleTarget)) {
1355
+ return { roleIds: [singleTarget] as [string, ...string[]] };
1356
+ }
1357
+ if (skills.has(singleTarget)) {
1358
+ return { skillIds: [singleTarget] as [string, ...string[]] };
1359
+ }
1360
+ throw new Error(`Coverage target "${singleTarget}" is not a declared role or skill.`);
1361
+ };
1362
+
1363
+ return {
1364
+ semanticTime: entry.timeName,
1365
+ variants,
1366
+ ...resolveTarget(),
1367
+ } as MixedCoverageRequirement<T>;
1368
+ }
1369
+
1370
+ // ============================================================================
1371
+ // Internal: Rule Translation
1372
+ // ============================================================================
1373
+
1374
+ function resolveRules(
1375
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[],
1376
+ roles: Set<string>,
1377
+ skills: Set<string>,
1378
+ memberIds: Set<string>,
1379
+ ): AnyCpsatRuleConfigEntry[] {
1380
+ const ctx: RuleResolveContext = { roles, skills, memberIds };
1381
+
1382
+ return rules.map((rule) => {
1383
+ // Rules with custom resolvers handle their own translation
1384
+ if (rule._resolve) {
1385
+ return rule._resolve(ctx) as AnyCpsatRuleConfigEntry;
1386
+ }
1387
+
1388
+ // Default resolution: appliesTo → entity scope, dates → specificDates
1389
+ const { _type, _rule, _resolve, appliesTo, dates, ...passthrough } = rule as RuleEntry & {
1390
+ appliesTo?: string | string[];
1391
+ dates?: DateString[];
1392
+ };
1393
+
1394
+ const entityScope = resolveAppliesTo(appliesTo, roles, skills, memberIds);
1395
+ const resolvedDates = dates ? { specificDates: dates } : {};
1396
+
1397
+ return {
1398
+ name: _rule,
1399
+ ...passthrough,
1400
+ ...entityScope,
1401
+ ...resolvedDates,
1402
+ } as AnyCpsatRuleConfigEntry;
1403
+ }) as AnyCpsatRuleConfigEntry[];
1404
+ }
1405
+
1406
+ // ============================================================================
1407
+ // Internal: Cost Rule Ordering
1408
+ // ============================================================================
1409
+
1410
+ /**
1411
+ * Sorts rules so that `minimize-cost` compiles before cost modifier rules.
1412
+ *
1413
+ * The `minimize-cost` rule must be compiled first because modifier rules
1414
+ * (multipliers, surcharges) reference cost variables it creates.
1415
+ * Non-cost rules retain their original relative order.
1416
+ */
1417
+ function sortCostRulesFirst(
1418
+ rules: ScheduleRuleEntry<CpsatRuleRegistry>[],
1419
+ ): ScheduleRuleEntry<CpsatRuleRegistry>[] {
1420
+ return rules.toSorted((a, b) => {
1421
+ const aIsCostBase = a._rule === "minimize-cost" ? 0 : 1;
1422
+ const bIsCostBase = b._rule === "minimize-cost" ? 0 : 1;
1423
+ return aIsCostBase - bIsCostBase;
1424
+ });
1425
+ }
1426
+
1427
+ // ============================================================================
1428
+ // Internal: Scheduling Period Helpers
1429
+ // ============================================================================
1430
+
1431
+ function applyDaysFilter(
1432
+ schedulingPeriod: SchedulingPeriod,
1433
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]],
1434
+ ): SchedulingPeriod {
1435
+ if (!dayOfWeek || dayOfWeek.length === 0) {
1436
+ return schedulingPeriod;
1437
+ }
1438
+
1439
+ const existingDays = schedulingPeriod.dayOfWeek;
1440
+ if (!existingDays || existingDays.length === 0) {
1441
+ return { ...schedulingPeriod, dayOfWeek: [...dayOfWeek] };
1442
+ }
1443
+
1444
+ const existingSet = new Set(existingDays);
1445
+ const intersected = dayOfWeek.filter((day) => existingSet.has(day));
1446
+ return { ...schedulingPeriod, dayOfWeek: intersected };
1447
+ }