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
@@ -1,66 +1,59 @@
1
- import type { SolverConstraint, SolverRequest, SolverVariable } from "../client.types.js";
2
- import type { DayOfWeek, SchedulingPeriod } from "../types.js";
3
- import { resolveDaysFromPeriod, toDayOfWeekUTC } from "../datetime.utils.js";
1
+ import type {
2
+ SolverConstraint,
3
+ SolverObjectiveStage,
4
+ SolverRequest,
5
+ SolverVariable,
6
+ } from "../client.types.js";
7
+ import type { DayOfWeek, SchedulingDay, SchedulingPeriod } from "../types.js";
8
+ import { resolveDaysFromPeriod } from "../datetime.utils.js";
4
9
  import {
5
10
  MINUTES_PER_DAY,
6
11
  normalizeEndMinutes,
7
12
  OBJECTIVE_WEIGHTS,
8
- parseDayString,
9
13
  priorityToPenalty,
10
14
  timeOfDayToMinutes,
11
15
  } from "./utils.js";
12
16
  import type {
13
17
  CoverageRequirement,
14
18
  ModelBuilderOptions,
19
+ ModelSolveStrategy,
15
20
  ShiftPattern,
16
21
  SchedulingMember,
17
22
  Term,
18
23
  } from "./types.js";
19
- import type { CpsatRuleConfigEntry, CpsatRuleFactories } from "./rules/rules.types.js";
24
+ import type {
25
+ AnyCpsatRuleConfigEntry,
26
+ BuiltInCpsatRuleRegistry,
27
+ CpsatRuleConfigEntry,
28
+ CpsatRuleRegistry,
29
+ } from "./rules/rules.types.js";
20
30
  import { buildCpsatRules } from "./rules/resolver.js";
21
- import { builtInCpsatRuleFactories } from "./rules/registry.js";
31
+ import { builtInCpsatRuleRegistry } from "./rules/registry.js";
32
+ import { TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID } from "./rules/target-peak-concurrent-assignments.js";
22
33
  import { ValidationReporterImpl } from "./validation-reporter.js";
23
34
  import type { ValidationReporter } from "./validation-reporter.js";
24
35
  import type { ScheduleValidation, CoverageExclusion } from "./validation.types.js";
25
36
  import type { ShiftAssignment, ResolvedShiftAssignment } from "./response.js";
37
+ import type {
38
+ CompiledRule,
39
+ CostArtifact,
40
+ CostContribution,
41
+ PostSolveFeedbackArtifact,
42
+ RuleArtifact,
43
+ RuleCompileContext,
44
+ } from "./rule-descriptor.js";
45
+
46
+ type BoundRuleArtifact = {
47
+ readonly rule: string;
48
+ readonly artifact: RuleArtifact;
49
+ };
50
+
51
+ const UNSTAGED_OBJECTIVE_STAGE_ID = "__dabke_unstaged__";
26
52
 
27
53
  /**
28
54
  * Builds a CP-SAT solver request from high-level scheduling constructs
29
55
  * (team, shift patterns, coverage, and rule compilers).
30
56
  */
31
- /**
32
- * Context provided to rules during post-solve validation.
33
- */
34
- export interface RuleValidationContext {
35
- readonly members: SchedulingMember[];
36
- readonly days: string[];
37
- readonly shiftPatterns: ShiftPattern[];
38
- }
39
-
40
- /**
41
- * A rule that adds constraints or objectives to the solver model.
42
- *
43
- * Rules implement `compile` to emit solver constraints during model building,
44
- * and optionally `validate` to check the solution after solving.
45
- * Use the `create*Rule` functions to create built-in rules.
46
- */
47
- export interface CompilationRule {
48
- /** Emit constraints and objectives into the model builder. */
49
- compile(builder: ModelBuilder): void;
50
- /** Validate the solved schedule and report violations. */
51
- validate?(
52
- assignments: ResolvedShiftAssignment[],
53
- reporter: ValidationReporter,
54
- context: RuleValidationContext,
55
- ): void;
56
- /** Compute cost contribution for a solved schedule. */
57
- cost?(
58
- assignments: ShiftAssignment[],
59
- members: ReadonlyArray<SchedulingMember>,
60
- shiftPatterns: ReadonlyArray<ShiftPattern>,
61
- ): CostContribution;
62
- }
63
-
64
57
  /**
65
58
  * Shared context for cost rules.
66
59
  *
@@ -74,26 +67,6 @@ export interface CostContext {
74
67
  active: boolean;
75
68
  }
76
69
 
77
- /**
78
- * A cost entry produced by a rule's `cost()` method.
79
- *
80
- * The `category` is an open-ended string. Built-in rules use well-known
81
- * categories from {@link COST_CATEGORY}. Custom rules can use any string.
82
- */
83
- export interface CostEntry {
84
- memberId: string;
85
- day: string;
86
- category: string;
87
- amount: number;
88
- }
89
-
90
- /**
91
- * Cost contribution from a single rule.
92
- */
93
- export interface CostContribution {
94
- entries: CostEntry[];
95
- }
96
-
97
70
  /**
98
71
  * Result of {@link ModelBuilder.compile}.
99
72
  *
@@ -123,7 +96,19 @@ export interface CompilationResult {
123
96
  * };
124
97
  * ```
125
98
  */
126
- export interface ModelBuilderConfig extends ModelBuilderOptions {
99
+ /**
100
+ * Configuration for {@link ModelBuilder}.
101
+ *
102
+ * @remarks
103
+ * The optional `Registry` generic keeps `ruleConfigs` type-safe for custom rule
104
+ * registries. When omitted, the config defaults to the built-in CP-SAT registry.
105
+ *
106
+ * @category Model Builder
107
+ */
108
+ export interface ModelBuilderConfig<
109
+ Registry extends CpsatRuleRegistry = BuiltInCpsatRuleRegistry,
110
+ RuleConfigEntry extends AnyCpsatRuleConfigEntry = CpsatRuleConfigEntry<Registry>,
111
+ > extends ModelBuilderOptions {
127
112
  /** Team members available for scheduling. */
128
113
  members: SchedulingMember[];
129
114
  /** Available shift patterns (time slots) that members can be assigned to. */
@@ -137,17 +122,17 @@ export interface ModelBuilderConfig extends ModelBuilderOptions {
137
122
  /**
138
123
  * Pre-compiled rules; use this for custom rules that are not part of the registry.
139
124
  */
140
- rules?: CompilationRule[];
125
+ rules?: CompiledRule[];
141
126
  /**
142
- * Named rule configurations that will be compiled using the provided rule factories.
127
+ * Named rule configurations that will be compiled using the provided rule registry.
143
128
  */
144
- ruleConfigs?: CpsatRuleConfigEntry[];
129
+ ruleConfigs?: RuleConfigEntry[];
145
130
  /**
146
- * Rule factories to use when compiling ruleConfigs. Defaults to built-in CP-SAT rules.
131
+ * Rule registry to use when compiling ruleConfigs. Defaults to the built-in CP-SAT registry.
147
132
  */
148
- ruleFactories?: CpsatRuleFactories;
133
+ ruleRegistry?: Registry;
149
134
  /**
150
- * Optional validation reporter for diagnostics.
135
+ * Optional validation reporter for validation feedback.
151
136
  */
152
137
  reporter?: ValidationReporter;
153
138
  }
@@ -161,11 +146,12 @@ export interface ModelBuilderConfig extends ModelBuilderOptions {
161
146
  export class ModelBuilder {
162
147
  readonly members: SchedulingMember[];
163
148
  readonly shiftPatterns: ShiftPattern[];
164
- readonly days: string[];
149
+ readonly days: SchedulingDay[];
165
150
  readonly coverage: CoverageRequirement[];
166
- readonly rules: CompilationRule[];
151
+ readonly rules: CompiledRule[];
167
152
  readonly weekStartsOn: DayOfWeek;
168
153
  readonly options: SolverRequest["options"] | undefined;
154
+ readonly strategy: ModelSolveStrategy;
169
155
  readonly coverageBucketMinutes: number;
170
156
  readonly reporter: ValidationReporter;
171
157
  readonly fairDistribution: boolean;
@@ -176,22 +162,39 @@ export class ModelBuilder {
176
162
  #variables = new Map<string, SolverVariable>();
177
163
  #constraints: SolverConstraint[] = [];
178
164
  #objective: Term[] = [];
165
+ #objectiveStageTerms = new Map<string, Term[]>();
166
+ #referencedObjectiveStages = new Set<string>();
167
+ #objectiveStageOrder: readonly string[] | undefined;
179
168
  #dayIndex = new Map<string, number>();
169
+ #dayByIso = new Map<string, SchedulingDay>();
180
170
  #shiftPatternMap = new Map<string, ShiftPattern>();
181
171
  #builtRequest: SolverRequest | undefined;
182
172
  #builtCompilation: CompilationResult | undefined;
173
+ #compiledArtifacts: BoundRuleArtifact[] = [];
174
+ #postSolveValidators: PostSolveFeedbackArtifact[] = [];
175
+ #costArtifacts: CostArtifact[] = [];
183
176
 
184
- constructor(config: ModelBuilderConfig) {
177
+ constructor(config: ModelBuilderConfig<CpsatRuleRegistry, AnyCpsatRuleConfigEntry>) {
185
178
  // Validate IDs don't contain the separator character
186
179
  for (const member of config.members) {
187
180
  if (member.id.includes(":")) {
188
181
  throw new Error(`Member ID "${member.id}" cannot contain colons`);
189
182
  }
183
+ for (const roleId of member.roleIds) {
184
+ if (roleId.includes(":")) {
185
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
186
+ }
187
+ }
190
188
  }
191
189
  for (const pattern of config.shiftPatterns) {
192
190
  if (pattern.id.includes(":")) {
193
191
  throw new Error(`Shift pattern ID "${pattern.id}" cannot contain colons`);
194
192
  }
193
+ for (const roleId of pattern.roleIds ?? []) {
194
+ if (roleId.includes(":")) {
195
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
196
+ }
197
+ }
195
198
  }
196
199
 
197
200
  // Validate coverage requirements have at least roles or skills
@@ -203,28 +206,50 @@ export class ModelBuilder {
203
206
  `Coverage requirement for day "${cov.day}" must have at least one of roles or skills`,
204
207
  );
205
208
  }
209
+ for (const roleId of cov.roleIds ?? []) {
210
+ if (roleId.includes(":")) {
211
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
212
+ }
213
+ }
206
214
  }
207
215
 
208
216
  this.members = config.members;
209
217
  this.shiftPatterns = config.shiftPatterns;
210
218
  this.days = resolveDaysFromPeriod(config.schedulingPeriod);
211
219
  this.coverage = config.coverage;
220
+ const ruleCompileContext: RuleCompileContext = {
221
+ members: this.members,
222
+ shiftPatterns: this.shiftPatterns,
223
+ days: this.days,
224
+ weekStartsOn: config.weekStartsOn ?? "monday",
225
+ coverageBucketMinutes: config.coverageBucketMinutes ?? 15,
226
+ };
212
227
  const compiledRuleConfigs = config.ruleConfigs
213
228
  ? buildCpsatRules(
214
229
  config.ruleConfigs,
215
230
  this.members,
216
- config.ruleFactories ?? builtInCpsatRuleFactories,
231
+ ruleCompileContext,
232
+ config.ruleRegistry ?? builtInCpsatRuleRegistry,
217
233
  )
218
234
  : [];
219
235
  this.rules = [...compiledRuleConfigs, ...(config.rules ?? [])];
220
236
  this.weekStartsOn = config.weekStartsOn ?? "monday";
221
237
  this.options = config.solverOptions;
238
+ this.strategy = resolveModelSolveStrategy(config.strategy);
239
+ this.#objectiveStageOrder =
240
+ config.objectiveStageOrder === undefined ? undefined : [...config.objectiveStageOrder];
222
241
  this.coverageBucketMinutes = config.coverageBucketMinutes ?? 15;
223
242
  this.reporter = config.reporter ?? new ValidationReporterImpl();
224
243
  this.fairDistribution = config.fairDistribution ?? true;
225
244
 
226
- this.days.forEach((day, idx) => this.#dayIndex.set(day, idx));
245
+ this.days.forEach((day, idx) => {
246
+ this.#dayIndex.set(day.iso, idx);
247
+ this.#dayByIso.set(day.iso, day);
248
+ });
227
249
  this.shiftPatterns.forEach((pattern) => this.#shiftPatternMap.set(pattern.id, pattern));
250
+ this.#compiledArtifacts = this.rules.flatMap((rule) =>
251
+ rule.artifacts.map((artifact) => ({ rule: rule.rule, artifact })),
252
+ );
228
253
  }
229
254
 
230
255
  boolVar(name: string): string {
@@ -253,20 +278,54 @@ export class ModelBuilder {
253
278
  return name;
254
279
  }
255
280
 
256
- shiftActive(patternId: string, day: string): string {
257
- return this.boolVar(`shift:${patternId}:${day}`);
281
+ shiftActive(patternId: string, day: SchedulingDay): string {
282
+ return this.boolVar(`shift:${patternId}:${day.iso}`);
258
283
  }
259
284
 
260
- assignment(memberId: string, patternId: string, day: string): string {
261
- return this.boolVar(`assign:${memberId}:${patternId}:${day}`);
285
+ /**
286
+ * Returns the aggregate shift assignment variable for a member, pattern, and day.
287
+ *
288
+ * @remarks
289
+ * This variable means "the member works this shift pattern on this day". It is
290
+ * the presence literal for optional interval variables and the common variable
291
+ * used by shift-level rules, objectives, and skill-only coverage. Role-specific
292
+ * assignments are modeled separately and linked to this aggregate variable when
293
+ * a concrete role choice is available.
294
+ */
295
+ assignment(memberId: string, patternId: string, day: SchedulingDay): string {
296
+ return this.boolVar(`assign:${memberId}:${patternId}:${day.iso}`);
262
297
  }
263
298
 
264
- addLinear(terms: Term[], op: "<=" | ">=" | "==", rhs: number): void {
265
- this.#constraints.push({ type: "linear", terms, op, rhs });
299
+ /** Returns the role-specific assignment variable for a concrete role choice. */
300
+ #roleAssignment(memberId: string, patternId: string, roleId: string, day: SchedulingDay): string {
301
+ return this.boolVar(`assign_role:${memberId}:${patternId}:${roleId}:${day.iso}`);
266
302
  }
267
303
 
268
- addSoftLinear(terms: Term[], op: "<=" | ">=", rhs: number, penalty: number, id?: string): void {
269
- this.#constraints.push({ type: "soft_linear", terms, op, rhs, penalty, id });
304
+ addLinear(terms: Term[], op: "<=" | ">=" | "==", rhs: number, id?: string): void {
305
+ const constraint: SolverConstraint =
306
+ id === undefined
307
+ ? { type: "linear", terms, op, rhs }
308
+ : { type: "linear", terms, op, rhs, id };
309
+ this.#constraints.push(constraint);
310
+ }
311
+
312
+ addSoftLinear(
313
+ terms: Term[],
314
+ op: "<=" | ">=",
315
+ rhs: number,
316
+ penalty: number,
317
+ id?: string,
318
+ stage?: string,
319
+ ): void {
320
+ if (!this.#isOptimizing()) {
321
+ return;
322
+ }
323
+ this.#markObjectiveStageReference(stage);
324
+ const constraint: SolverConstraint =
325
+ stage === undefined
326
+ ? { type: "soft_linear", terms, op, rhs, penalty, id }
327
+ : { type: "soft_linear", terms, op, rhs, penalty, id, stage };
328
+ this.#constraints.push(constraint);
270
329
  }
271
330
 
272
331
  addExactlyOne(vars: string[]): void {
@@ -334,10 +393,25 @@ export class ModelBuilder {
334
393
  }
335
394
 
336
395
  addPenalty(varName: string, weight: number): void {
396
+ this.#addPenalty(varName, weight);
397
+ }
398
+
399
+ #addPenalty(varName: string, weight: number, stage?: string): void {
400
+ if (!this.#isOptimizing()) {
401
+ return;
402
+ }
403
+ this.#markObjectiveStageReference(stage);
337
404
  // CP-SAT requires integer coefficients. Round to nearest integer.
338
405
  const rounded = Math.round(weight);
339
406
  if (rounded === 0) return;
340
- this.#objective.push({ var: varName, coeff: rounded });
407
+ const term = { var: varName, coeff: rounded };
408
+ if (stage !== undefined) {
409
+ const stageTerms = this.#objectiveStageTerms.get(stage) ?? [];
410
+ stageTerms.push(term);
411
+ this.#objectiveStageTerms.set(stage, stageTerms);
412
+ return;
413
+ }
414
+ this.#objective.push(term);
341
415
  }
342
416
 
343
417
  membersWithRole(roleId: string): SchedulingMember[] {
@@ -377,19 +451,60 @@ export class ModelBuilder {
377
451
  return pattern.roleIds.some((role) => member.roleIds.includes(role));
378
452
  }
379
453
 
454
+ #assignableRoleIds(member: SchedulingMember, pattern: ShiftPattern): string[] {
455
+ const patternRoleIds = pattern.roleIds;
456
+ const roles =
457
+ patternRoleIds && patternRoleIds.length > 0
458
+ ? member.roleIds.filter((roleId) => patternRoleIds.includes(roleId))
459
+ : member.roleIds;
460
+
461
+ return [...new Set(roles)];
462
+ }
463
+
464
+ /**
465
+ * Returns the variables that can satisfy one coverage bucket for this member and pattern.
466
+ *
467
+ * Role coverage uses role-specific variables so a single shift assignment fills
468
+ * one concrete role. Skill-only coverage has no role to choose, so it uses the
469
+ * aggregate assignment variable directly.
470
+ */
471
+ #coverageRoleAssignmentVars(
472
+ member: SchedulingMember,
473
+ pattern: ShiftPattern,
474
+ cov: CoverageRequirement,
475
+ day: SchedulingDay,
476
+ ): string[] {
477
+ if (!cov.roleIds || cov.roleIds.length === 0) {
478
+ return [this.assignment(member.id, pattern.id, day)];
479
+ }
480
+
481
+ const assignableRoles = this.#assignableRoleIds(member, pattern);
482
+ const coverageRoles = assignableRoles.filter((roleId) => cov.roleIds.includes(roleId));
483
+
484
+ return coverageRoles.map((roleId) => this.#roleAssignment(member.id, pattern.id, roleId, day));
485
+ }
486
+
487
+ #canCoverCoverage(
488
+ member: SchedulingMember,
489
+ pattern: ShiftPattern,
490
+ cov: CoverageRequirement,
491
+ ): boolean {
492
+ const assignableRoles = this.#assignableRoleIds(member, pattern);
493
+ if (cov.roleIds && cov.roleIds.length > 0) {
494
+ return assignableRoles.some((roleId) => cov.roleIds.includes(roleId));
495
+ }
496
+ return true;
497
+ }
498
+
380
499
  /**
381
500
  * Checks if a shift pattern can be used on a specific day.
382
501
  * Returns false if the pattern has dayOfWeek restrictions that exclude this day.
383
502
  */
384
- patternAvailableOnDay(pattern: ShiftPattern, day: string): boolean {
385
- // If pattern has no day restrictions, it's available every day
503
+ patternAvailableOnDay(pattern: ShiftPattern, day: SchedulingDay): boolean {
386
504
  if (!pattern.dayOfWeek || pattern.dayOfWeek.length === 0) {
387
505
  return true;
388
506
  }
389
- // Check if this day's day-of-week is in the allowed list
390
- const date = parseDayString(day);
391
- const dow = toDayOfWeekUTC(date);
392
- return pattern.dayOfWeek.includes(dow);
507
+ return pattern.dayOfWeek.includes(day.dayOfWeek);
393
508
  }
394
509
 
395
510
  patternDuration(patternId: string): number {
@@ -401,13 +516,13 @@ export class ModelBuilder {
401
516
  return end - start;
402
517
  }
403
518
 
404
- startMinutes(pattern: ShiftPattern, day: string): number {
405
- const base = this.#dayOffset(day);
519
+ startMinutes(pattern: ShiftPattern, day: SchedulingDay): number {
520
+ const base = this.#dayOffset(day.iso);
406
521
  return base + timeOfDayToMinutes(pattern.startTime);
407
522
  }
408
523
 
409
- endMinutes(pattern: ShiftPattern, day: string): number {
410
- const base = this.#dayOffset(day);
524
+ endMinutes(pattern: ShiftPattern, day: SchedulingDay): number {
525
+ const base = this.#dayOffset(day.iso);
411
526
  const startOffset = timeOfDayToMinutes(pattern.startTime);
412
527
  const endOffset = normalizeEndMinutes(startOffset, timeOfDayToMinutes(pattern.endTime));
413
528
  return base + endOffset;
@@ -418,14 +533,34 @@ export class ModelBuilder {
418
533
  return this.#builtCompilation;
419
534
  }
420
535
 
421
- // 0. Apply all rules first (they may report exclusions and add constraints)
422
- for (const rule of this.rules) {
423
- rule.compile(this);
424
- }
536
+ this.#runRulePrechecks();
537
+ this.#applyRuleArtifacts();
425
538
 
426
- // Build exclusion lookup from reporter (populated by rules during compile)
539
+ // Build exclusion lookup from rule artifacts for coverage feasibility analysis.
427
540
  const mandatoryExclusions = buildExclusionLookup(this.reporter.getExclusions());
428
541
 
542
+ // Link concrete role choices to the aggregate assignment used by intervals and
543
+ // shift-level rules. This is the CP-SAT channeling pattern: exactly one role
544
+ // literal is true iff the aggregate assignment is true. Role-less members on
545
+ // unrestricted shifts deliberately skip this link; skill-only coverage uses
546
+ // the aggregate variable directly because there is no role decision to report.
547
+ for (const emp of this.members) {
548
+ for (const pattern of this.shiftPatterns) {
549
+ if (!this.canAssign(emp, pattern)) continue;
550
+ for (const day of this.days) {
551
+ if (!this.patternAvailableOnDay(pattern, day)) continue;
552
+ const assignmentVar = this.assignment(emp.id, pattern.id, day);
553
+ const roleTerms = this.#assignableRoleIds(emp, pattern).map((roleId) => ({
554
+ var: this.#roleAssignment(emp.id, pattern.id, roleId, day),
555
+ coeff: 1,
556
+ }));
557
+ if (roleTerms.length === 0) continue;
558
+
559
+ this.addLinear([...roleTerms, { var: assignmentVar, coeff: -1 }], "==", 0);
560
+ }
561
+ }
562
+ }
563
+
429
564
  // 1. Assignment implies shift is active
430
565
  for (const emp of this.members) {
431
566
  for (const pattern of this.shiftPatterns) {
@@ -455,7 +590,7 @@ export class ModelBuilder {
455
590
  const end = this.endMinutes(pattern, day);
456
591
  const size = end - start;
457
592
 
458
- const intervalName = `interval:${emp.id}:${pattern.id}:${day}`;
593
+ const intervalName = `interval:${emp.id}:${pattern.id}:${day.iso}`;
459
594
  this.intervalVar(intervalName, start, end, size, presenceVar);
460
595
  empIntervals.push(intervalName);
461
596
  }
@@ -505,6 +640,8 @@ export class ModelBuilder {
505
640
  }
506
641
 
507
642
  for (const cov of this.coverage) {
643
+ const covDay = this.#dayByIso.get(cov.day);
644
+ if (!covDay) continue;
508
645
  const covStart = timeOfDayToMinutes(cov.startTime);
509
646
  const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
510
647
  const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
@@ -551,7 +688,7 @@ export class ModelBuilder {
551
688
  const lookupBucketStart = bucketStart % MINUTES_PER_DAY;
552
689
  // Get patterns that overlap this time bucket, then filter by day availability
553
690
  const allPatterns = patternsByBucketStart.get(lookupBucketStart) ?? [];
554
- const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, cov.day));
691
+ const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, covDay));
555
692
 
556
693
  if (patterns.length === 0) {
557
694
  recordBucketIssue(
@@ -580,6 +717,7 @@ export class ModelBuilder {
580
717
  for (const emp of eligibleMembers) {
581
718
  for (const pattern of patterns) {
582
719
  if (!this.canAssign(emp, pattern)) continue;
720
+ if (!this.#canCoverCoverage(emp, pattern, cov)) continue;
583
721
  assignableMembers.add(emp.id);
584
722
  break;
585
723
  }
@@ -654,7 +792,14 @@ export class ModelBuilder {
654
792
  for (const pattern of patterns) {
655
793
  for (const emp of eligibleMembers) {
656
794
  if (!this.canAssign(emp, pattern)) continue;
657
- coveringVarsSet.add(this.assignment(emp.id, pattern.id, cov.day));
795
+ for (const roleAssignment of this.#coverageRoleAssignmentVars(
796
+ emp,
797
+ pattern,
798
+ cov,
799
+ covDay,
800
+ )) {
801
+ coveringVarsSet.add(roleAssignment);
802
+ }
658
803
  }
659
804
  }
660
805
 
@@ -668,8 +813,8 @@ export class ModelBuilder {
668
813
  const constraintId = `coverage:${covKey}:${cov.day}:${bucketStart}`;
669
814
 
670
815
  if (cov.priority === "MANDATORY") {
671
- this.addLinear(terms, ">=", cov.targetCount);
672
- } else {
816
+ this.addLinear(terms, ">=", cov.targetCount, constraintId);
817
+ } else if (this.#isOptimizing()) {
673
818
  this.addSoftLinear(
674
819
  terms,
675
820
  ">=",
@@ -679,23 +824,26 @@ export class ModelBuilder {
679
824
  );
680
825
  }
681
826
 
682
- // Track all coverage constraints (mandatory and soft) for post-solve reporting
683
- this.reporter.trackConstraint({
684
- id: constraintId,
685
- type: "coverage",
686
- description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
687
- targetValue: cov.targetCount,
688
- comparator: ">=",
689
- day: cov.day,
690
- timeSlot: formatMinutes(bucketStart),
691
- roleIds: cov.roleIds,
692
- skillIds: cov.skillIds,
693
- context: {
694
- days: [cov.day],
695
- memberIds: eligibleMembers.map((e) => e.id),
696
- },
697
- group: cov.group,
698
- });
827
+ // Track coverage constraints that can produce post-solve feedback.
828
+ if (cov.priority === "MANDATORY" || this.#isOptimizing()) {
829
+ this.reporter.trackConstraint({
830
+ id: constraintId,
831
+ type: "coverage",
832
+ source: cov.priority === "MANDATORY" ? "hard" : "soft",
833
+ description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
834
+ targetValue: cov.targetCount,
835
+ comparator: ">=",
836
+ day: cov.day,
837
+ timeSlot: formatMinutes(bucketStart),
838
+ roleIds: cov.roleIds,
839
+ skillIds: cov.skillIds,
840
+ context: {
841
+ days: [cov.day],
842
+ memberIds: eligibleMembers.map((e) => e.id),
843
+ },
844
+ group: cov.group,
845
+ });
846
+ }
699
847
  }
700
848
 
701
849
  for (const issue of bucketIssues.values()) {
@@ -717,11 +865,25 @@ export class ModelBuilder {
717
865
  group: cov.group,
718
866
  });
719
867
  }
720
- // Note: soft coverage warnings are tracked via trackConstraint and reported post-solve
868
+ // Note: optimized solves report soft coverage post-solve via tracked constraints.
721
869
  }
722
870
  }
723
871
 
724
- // 3. Default objective: shift minimization with optional fair distribution
872
+ if (this.#isOptimizing()) {
873
+ this.#addDefaultObjective();
874
+ }
875
+
876
+ this.#builtRequest = this.#buildRequest();
877
+ this.#builtCompilation = {
878
+ request: this.#builtRequest,
879
+ validation: this.reporter.getValidation(),
880
+ canSolve: !this.reporter.hasErrors(),
881
+ };
882
+ return this.#builtCompilation;
883
+ }
884
+
885
+ #addDefaultObjective(): void {
886
+ // Default objective: shift minimization with optional fair distribution
725
887
  //
726
888
  // The objective has three components (see OBJECTIVE_WEIGHTS in utils.ts):
727
889
  // a) Minimize number of active shift patterns (SHIFT_ACTIVE=1000)
@@ -785,14 +947,6 @@ export class ModelBuilder {
785
947
  }
786
948
  }
787
949
  }
788
-
789
- this.#builtRequest = this.#buildRequest();
790
- this.#builtCompilation = {
791
- request: this.#builtRequest,
792
- validation: this.reporter.getValidation(),
793
- canSolve: !this.reporter.hasErrors(),
794
- };
795
- return this.#builtCompilation;
796
950
  }
797
951
 
798
952
  /**
@@ -800,18 +954,203 @@ export class ModelBuilder {
800
954
  * Call this after solving with the resolved assignments.
801
955
  */
802
956
  validateSolution(assignments: ResolvedShiftAssignment[]): void {
803
- const context: RuleValidationContext = {
957
+ const context: RuleCompileContext = {
804
958
  members: this.members,
805
959
  days: this.days,
806
960
  shiftPatterns: this.shiftPatterns,
961
+ weekStartsOn: this.weekStartsOn,
962
+ };
963
+
964
+ for (const validator of this.#postSolveValidators) {
965
+ validator.run(assignments, this.reporter, context);
966
+ }
967
+ }
968
+
969
+ calculateCost(assignments: readonly ShiftAssignment[]): CostContribution {
970
+ const context: RuleCompileContext = {
971
+ members: this.members,
972
+ days: this.days,
973
+ shiftPatterns: this.shiftPatterns,
974
+ weekStartsOn: this.weekStartsOn,
975
+ };
976
+
977
+ const entries = this.#costArtifacts.flatMap((artifact) =>
978
+ artifact.calculateCost ? [...artifact.calculateCost(assignments, context).entries] : [],
979
+ );
980
+
981
+ return { entries };
982
+ }
983
+
984
+ #runRulePrechecks(): void {
985
+ const context: RuleCompileContext = {
986
+ members: this.members,
987
+ days: this.days,
988
+ shiftPatterns: this.shiftPatterns,
989
+ weekStartsOn: this.weekStartsOn,
807
990
  };
808
991
 
809
992
  for (const rule of this.rules) {
810
- rule.validate?.(assignments, this.reporter, context);
993
+ const hasHardConstraint = rule.artifacts.some(
994
+ (artifact) => artifact.kind === "hard-constraint",
995
+ );
996
+ const shouldRunPrechecks = this.#isOptimizing() || hasHardConstraint;
997
+ if (!shouldRunPrechecks) continue;
998
+
999
+ for (const artifact of rule.artifacts) {
1000
+ if (artifact.kind === "pre-solve-feedback") {
1001
+ artifact.run(context, this.reporter);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ #applyRuleArtifacts(): void {
1008
+ this.#postSolveValidators = [];
1009
+ this.#costArtifacts = [];
1010
+
1011
+ for (const { rule, artifact } of this.#compiledArtifacts) {
1012
+ switch (artifact.kind) {
1013
+ case "variable": {
1014
+ if (artifact.variable.type === "bool") {
1015
+ this.boolVar(artifact.variable.name);
1016
+ } else {
1017
+ this.intVar(artifact.variable.name, artifact.variable.min, artifact.variable.max);
1018
+ }
1019
+ break;
1020
+ }
1021
+ case "hard-constraint": {
1022
+ const constraintId =
1023
+ artifact.validation.strategy === "report" ? artifact.validation.id : undefined;
1024
+ this.addLinear(
1025
+ [...artifact.terms],
1026
+ artifact.comparator,
1027
+ artifact.targetValue,
1028
+ constraintId,
1029
+ );
1030
+ this.#reportHardConstraint(rule, artifact);
1031
+ break;
1032
+ }
1033
+ case "soft-constraint": {
1034
+ if (!this.#isOptimizing()) {
1035
+ break;
1036
+ }
1037
+ const solverConstraintId =
1038
+ artifact.validation.strategy === "report" ? artifact.constraintId : undefined;
1039
+ this.addSoftLinear(
1040
+ [...artifact.terms],
1041
+ artifact.comparator,
1042
+ artifact.targetValue,
1043
+ artifact.penalty,
1044
+ solverConstraintId,
1045
+ artifact.stage,
1046
+ );
1047
+ this.#reportSoftConstraint(rule, artifact);
1048
+ break;
1049
+ }
1050
+ case "objective": {
1051
+ if (!this.#isOptimizing()) {
1052
+ break;
1053
+ }
1054
+ this.#markObjectiveStageReference(artifact.stage);
1055
+ for (const term of artifact.terms) {
1056
+ this.#addPenalty(term.var, term.coeff, artifact.stage);
1057
+ }
1058
+ break;
1059
+ }
1060
+ case "coverage-exclusion": {
1061
+ this.reporter.excludeFromCoverage({
1062
+ memberId: artifact.memberId,
1063
+ day: artifact.day,
1064
+ startTime: artifact.startTime,
1065
+ endTime: artifact.endTime,
1066
+ });
1067
+ break;
1068
+ }
1069
+ case "pre-solve-feedback": {
1070
+ break;
1071
+ }
1072
+ case "post-solve-feedback": {
1073
+ if (!this.#isOptimizing()) {
1074
+ break;
1075
+ }
1076
+ this.#postSolveValidators.push(artifact);
1077
+ break;
1078
+ }
1079
+ case "cost": {
1080
+ if (!this.#isOptimizing()) {
1081
+ break;
1082
+ }
1083
+ artifact.compileObjective?.(this);
1084
+ this.#costArtifacts.push(artifact);
1085
+ break;
1086
+ }
1087
+ }
811
1088
  }
812
1089
  }
813
1090
 
1091
+ #reportHardConstraint(
1092
+ rule: string,
1093
+ artifact: Extract<RuleArtifact, { kind: "hard-constraint" }>,
1094
+ ): void {
1095
+ if (artifact.validation.strategy !== "report") {
1096
+ return;
1097
+ }
1098
+
1099
+ this.reporter.trackConstraint({
1100
+ id: artifact.validation.id,
1101
+ type: "rule",
1102
+ source: "hard",
1103
+ rule,
1104
+ description: artifact.description,
1105
+ targetValue: artifact.targetValue,
1106
+ comparator: artifact.comparator,
1107
+ context: artifact.context,
1108
+ group: artifact.group,
1109
+ });
1110
+ }
1111
+
1112
+ #reportSoftConstraint(
1113
+ rule: string,
1114
+ artifact: Extract<RuleArtifact, { kind: "soft-constraint" }>,
1115
+ ): void {
1116
+ if (artifact.validation.strategy !== "report") {
1117
+ return;
1118
+ }
1119
+
1120
+ this.reporter.trackConstraint({
1121
+ id: artifact.constraintId,
1122
+ type: "rule",
1123
+ source: "soft",
1124
+ rule,
1125
+ description: artifact.description,
1126
+ targetValue: artifact.targetValue,
1127
+ comparator: artifact.comparator,
1128
+ context: artifact.context,
1129
+ group: artifact.group,
1130
+ });
1131
+ }
1132
+
814
1133
  #buildRequest(): SolverRequest {
1134
+ if (!this.#isOptimizing()) {
1135
+ return {
1136
+ variables: Array.from(this.#variables.values()),
1137
+ constraints: this.#constraints,
1138
+ mode: "satisfy",
1139
+ options: this.#satisfyOptions(),
1140
+ };
1141
+ }
1142
+
1143
+ const objectiveStageOrder = this.#resolveObjectiveStageOrder();
1144
+
1145
+ if (objectiveStageOrder) {
1146
+ return {
1147
+ variables: Array.from(this.#variables.values()),
1148
+ constraints: this.#constraintsForStagedRequest(),
1149
+ objectiveStages: this.#buildObjectiveStages(objectiveStageOrder),
1150
+ options: this.options,
1151
+ };
1152
+ }
1153
+
815
1154
  return {
816
1155
  variables: Array.from(this.#variables.values()),
817
1156
  constraints: this.#constraints,
@@ -821,6 +1160,135 @@ export class ModelBuilder {
821
1160
  };
822
1161
  }
823
1162
 
1163
+ #markObjectiveStageReference(stage: string | undefined): void {
1164
+ if (stage === undefined) {
1165
+ return;
1166
+ }
1167
+ this.#referencedObjectiveStages.add(stage);
1168
+ }
1169
+
1170
+ #isOptimizing(): boolean {
1171
+ return this.strategy.type === "optimize";
1172
+ }
1173
+
1174
+ #satisfyOptions(): NonNullable<SolverRequest["options"]> {
1175
+ return {
1176
+ ...this.options,
1177
+ solutionLimit: this.options?.solutionLimit ?? 1,
1178
+ };
1179
+ }
1180
+
1181
+ #resolveObjectiveStageOrder(): readonly string[] | undefined {
1182
+ const referencedStages = [...this.#referencedObjectiveStages];
1183
+ if (referencedStages.length === 0) {
1184
+ return undefined;
1185
+ }
1186
+
1187
+ this.#validateReferencedObjectiveStages(referencedStages);
1188
+
1189
+ if (this.options?.solutionLimit === 1) {
1190
+ throw new Error(
1191
+ "ModelBuilder objectiveStageOrder cannot be used with solverOptions.solutionLimit=1.",
1192
+ );
1193
+ }
1194
+
1195
+ const stageOrder = this.#objectiveStageOrder;
1196
+ if (stageOrder === undefined) {
1197
+ if (referencedStages.length === 1) {
1198
+ return [referencedStages[0]!];
1199
+ }
1200
+ throw new Error(
1201
+ `Multiple objective stages were referenced (${referencedStages.join(", ")}); provide ModelBuilder objectiveStageOrder explicitly.`,
1202
+ );
1203
+ }
1204
+
1205
+ if (referencedStages.includes(TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID)) {
1206
+ throw new Error(
1207
+ "targetPeakConcurrentAssignments cannot be combined with ModelBuilder objectiveStageOrder until the multi-stage objective API is public.",
1208
+ );
1209
+ }
1210
+
1211
+ if (stageOrder.length === 0) {
1212
+ throw new Error("ModelBuilder objectiveStageOrder cannot be empty when provided.");
1213
+ }
1214
+
1215
+ const declaredStages = new Set<string>();
1216
+ for (const stage of stageOrder) {
1217
+ if (stage.trim() === "") {
1218
+ throw new Error("ModelBuilder objectiveStageOrder cannot contain an empty stage id.");
1219
+ }
1220
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
1221
+ throw new Error(
1222
+ `ModelBuilder objectiveStageOrder cannot contain reserved stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
1223
+ );
1224
+ }
1225
+ if (declaredStages.has(stage)) {
1226
+ throw new Error(`Duplicate objective stage id "${stage}" in objectiveStageOrder.`);
1227
+ }
1228
+ declaredStages.add(stage);
1229
+ }
1230
+
1231
+ const allowedStages = new Set(declaredStages);
1232
+ allowedStages.add(UNSTAGED_OBJECTIVE_STAGE_ID);
1233
+ for (const stage of referencedStages) {
1234
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
1235
+ throw new Error(
1236
+ `Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
1237
+ );
1238
+ }
1239
+ if (!allowedStages.has(stage)) {
1240
+ throw new Error(
1241
+ `Objective stage "${stage}" is not declared in ModelBuilder objectiveStageOrder.`,
1242
+ );
1243
+ }
1244
+ }
1245
+
1246
+ return stageOrder;
1247
+ }
1248
+
1249
+ #validateReferencedObjectiveStages(referencedStages: readonly string[]): void {
1250
+ for (const stage of referencedStages) {
1251
+ if (stage.trim() === "") {
1252
+ throw new Error("Rule artifacts cannot use an empty objective stage id.");
1253
+ }
1254
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
1255
+ throw new Error(
1256
+ `Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
1257
+ );
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ #constraintsForStagedRequest(): SolverConstraint[] {
1263
+ return this.#constraints.map((constraint) => {
1264
+ if (constraint.type !== "soft_linear" || constraint.stage !== undefined) {
1265
+ return constraint;
1266
+ }
1267
+ return { ...constraint, stage: UNSTAGED_OBJECTIVE_STAGE_ID };
1268
+ });
1269
+ }
1270
+
1271
+ #buildObjectiveStages(objectiveStageOrder: readonly string[]): SolverObjectiveStage[] {
1272
+ const stages = objectiveStageOrder.map((id) => ({
1273
+ id,
1274
+ sense: "minimize" as const,
1275
+ terms: this.#objectiveStageTerms.get(id) ?? [],
1276
+ }));
1277
+ const hasTailSoftConstraint = this.#constraints.some(
1278
+ (constraint) => constraint.type === "soft_linear" && constraint.stage === undefined,
1279
+ );
1280
+
1281
+ if (this.#objective.length > 0 || hasTailSoftConstraint) {
1282
+ stages.push({
1283
+ id: UNSTAGED_OBJECTIVE_STAGE_ID,
1284
+ sense: "minimize",
1285
+ terms: this.#objective,
1286
+ });
1287
+ }
1288
+
1289
+ return stages;
1290
+ }
1291
+
824
1292
  #dayOffset(day: string): number {
825
1293
  const idx = this.#dayIndex.get(day);
826
1294
  if (idx === undefined) {
@@ -844,6 +1312,23 @@ type ExclusionWindow = {
844
1312
  endMinutes: number;
845
1313
  };
846
1314
 
1315
+ function resolveModelSolveStrategy(strategy: unknown): ModelSolveStrategy {
1316
+ if (strategy === undefined) {
1317
+ return { type: "optimize" };
1318
+ }
1319
+
1320
+ if (typeof strategy !== "object" || strategy === null || !("type" in strategy)) {
1321
+ throw new Error("ModelBuilder strategy must be an object with a type field.");
1322
+ }
1323
+
1324
+ const strategyType = (strategy as { type: unknown }).type;
1325
+ if (strategyType === "optimize" || strategyType === "feasibility-only") {
1326
+ return { type: strategyType };
1327
+ }
1328
+
1329
+ throw new Error(`Unknown ModelBuilder strategy "${String(strategyType)}".`);
1330
+ }
1331
+
847
1332
  function recordBucketIssue(
848
1333
  bucketIssues: Map<string, BucketIssueGroup>,
849
1334
  issue: Omit<BucketIssueGroup, "bucketStarts">,