dabke 0.83.0 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +22 -27
  3. package/dist/client.d.ts +39 -3
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.schemas.d.ts +118 -18
  7. package/dist/client.schemas.d.ts.map +1 -1
  8. package/dist/client.schemas.js +110 -6
  9. package/dist/client.schemas.js.map +1 -1
  10. package/dist/client.types.d.ts +63 -7
  11. package/dist/client.types.d.ts.map +1 -1
  12. package/dist/client.types.js.map +1 -1
  13. package/dist/cpsat/cost.d.ts +5 -2
  14. package/dist/cpsat/cost.d.ts.map +1 -1
  15. package/dist/cpsat/cost.js +13 -6
  16. package/dist/cpsat/cost.js.map +1 -1
  17. package/dist/cpsat/model-builder.d.ts +42 -61
  18. package/dist/cpsat/model-builder.d.ts.map +1 -1
  19. package/dist/cpsat/model-builder.js +451 -56
  20. package/dist/cpsat/model-builder.js.map +1 -1
  21. package/dist/cpsat/response.d.ts +19 -7
  22. package/dist/cpsat/response.d.ts.map +1 -1
  23. package/dist/cpsat/response.js +48 -9
  24. package/dist/cpsat/response.js.map +1 -1
  25. package/dist/cpsat/rule-descriptor.d.ts +250 -0
  26. package/dist/cpsat/rule-descriptor.d.ts.map +1 -0
  27. package/dist/cpsat/rule-descriptor.js +51 -0
  28. package/dist/cpsat/rule-descriptor.js.map +1 -0
  29. package/dist/cpsat/rules/artifacts.d.ts +9 -0
  30. package/dist/cpsat/rules/artifacts.d.ts.map +1 -0
  31. package/dist/cpsat/rules/artifacts.js +31 -0
  32. package/dist/cpsat/rules/artifacts.js.map +1 -0
  33. package/dist/cpsat/rules/assign-together.d.ts +18 -20
  34. package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
  35. package/dist/cpsat/rules/assign-together.js +90 -54
  36. package/dist/cpsat/rules/assign-together.js.map +1 -1
  37. package/dist/cpsat/rules/assigned-day.d.ts +12 -0
  38. package/dist/cpsat/rules/assigned-day.d.ts.map +1 -0
  39. package/dist/cpsat/rules/assigned-day.js +43 -0
  40. package/dist/cpsat/rules/assigned-day.js.map +1 -0
  41. package/dist/cpsat/rules/assignment-priority.d.ts +15 -29
  42. package/dist/cpsat/rules/assignment-priority.d.ts.map +1 -1
  43. package/dist/cpsat/rules/assignment-priority.js +47 -50
  44. package/dist/cpsat/rules/assignment-priority.js.map +1 -1
  45. package/dist/cpsat/rules/assignment-terms.d.ts +6 -0
  46. package/dist/cpsat/rules/assignment-terms.d.ts.map +1 -0
  47. package/dist/cpsat/rules/assignment-terms.js +19 -0
  48. package/dist/cpsat/rules/assignment-terms.js.map +1 -0
  49. package/dist/cpsat/rules/concurrent-intervals.d.ts +19 -0
  50. package/dist/cpsat/rules/concurrent-intervals.d.ts.map +1 -0
  51. package/dist/cpsat/rules/concurrent-intervals.js +57 -0
  52. package/dist/cpsat/rules/concurrent-intervals.js.map +1 -0
  53. package/dist/cpsat/rules/cost-utils.d.ts +1 -3
  54. package/dist/cpsat/rules/cost-utils.d.ts.map +1 -1
  55. package/dist/cpsat/rules/cost-utils.js +0 -7
  56. package/dist/cpsat/rules/cost-utils.js.map +1 -1
  57. package/dist/cpsat/rules/day-cost-multiplier.d.ts +10 -10
  58. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
  59. package/dist/cpsat/rules/day-cost-multiplier.js +88 -77
  60. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
  61. package/dist/cpsat/rules/day-cost-surcharge.d.ts +10 -10
  62. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
  63. package/dist/cpsat/rules/day-cost-surcharge.js +76 -65
  64. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
  65. package/dist/cpsat/rules/index.d.ts +30 -24
  66. package/dist/cpsat/rules/index.d.ts.map +1 -1
  67. package/dist/cpsat/rules/index.js +30 -24
  68. package/dist/cpsat/rules/index.js.map +1 -1
  69. package/dist/cpsat/rules/location-preference.d.ts +14 -21
  70. package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
  71. package/dist/cpsat/rules/location-preference.js +45 -45
  72. package/dist/cpsat/rules/location-preference.js.map +1 -1
  73. package/dist/cpsat/rules/max-concurrent-assignments.d.ts +46 -0
  74. package/dist/cpsat/rules/max-concurrent-assignments.d.ts.map +1 -0
  75. package/dist/cpsat/rules/max-concurrent-assignments.js +121 -0
  76. package/dist/cpsat/rules/max-concurrent-assignments.js.map +1 -0
  77. package/dist/cpsat/rules/max-consecutive-days.d.ts +18 -17
  78. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  79. package/dist/cpsat/rules/max-consecutive-days.js +67 -66
  80. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  81. package/dist/cpsat/rules/max-days-of-week-per-period.d.ts +45 -0
  82. package/dist/cpsat/rules/max-days-of-week-per-period.d.ts.map +1 -0
  83. package/dist/cpsat/rules/max-days-of-week-per-period.js +95 -0
  84. package/dist/cpsat/rules/max-days-of-week-per-period.js.map +1 -0
  85. package/dist/cpsat/rules/max-days-week.d.ts +17 -31
  86. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -1
  87. package/dist/cpsat/rules/max-days-week.js +69 -81
  88. package/dist/cpsat/rules/max-days-week.js.map +1 -1
  89. package/dist/cpsat/rules/max-hours-day.d.ts +14 -31
  90. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  91. package/dist/cpsat/rules/max-hours-day.js +56 -72
  92. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  93. package/dist/cpsat/rules/max-hours-week.d.ts +15 -31
  94. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  95. package/dist/cpsat/rules/max-hours-week.js +60 -75
  96. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  97. package/dist/cpsat/rules/max-shifts-day.d.ts +17 -33
  98. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  99. package/dist/cpsat/rules/max-shifts-day.js +57 -73
  100. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  101. package/dist/cpsat/rules/min-consecutive-days.d.ts +18 -18
  102. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  103. package/dist/cpsat/rules/min-consecutive-days.js +123 -94
  104. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  105. package/dist/cpsat/rules/min-days-of-week-per-period.d.ts +35 -0
  106. package/dist/cpsat/rules/min-days-of-week-per-period.d.ts.map +1 -0
  107. package/dist/cpsat/rules/min-days-of-week-per-period.js +108 -0
  108. package/dist/cpsat/rules/min-days-of-week-per-period.js.map +1 -0
  109. package/dist/cpsat/rules/min-days-week.d.ts +13 -26
  110. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -1
  111. package/dist/cpsat/rules/min-days-week.js +85 -74
  112. package/dist/cpsat/rules/min-days-week.js.map +1 -1
  113. package/dist/cpsat/rules/min-hours-day.d.ts +14 -17
  114. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  115. package/dist/cpsat/rules/min-hours-day.js +72 -54
  116. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  117. package/dist/cpsat/rules/min-hours-week.d.ts +15 -18
  118. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  119. package/dist/cpsat/rules/min-hours-week.js +75 -57
  120. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  121. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +14 -17
  122. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  123. package/dist/cpsat/rules/min-rest-between-shifts.js +148 -138
  124. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  125. package/dist/cpsat/rules/minimize-cost.d.ts +2 -18
  126. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
  127. package/dist/cpsat/rules/minimize-cost.js +151 -163
  128. package/dist/cpsat/rules/minimize-cost.js.map +1 -1
  129. package/dist/cpsat/rules/must-assign.d.ts +5 -42
  130. package/dist/cpsat/rules/must-assign.d.ts.map +1 -1
  131. package/dist/cpsat/rules/must-assign.js +43 -75
  132. package/dist/cpsat/rules/must-assign.js.map +1 -1
  133. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts +6 -10
  134. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
  135. package/dist/cpsat/rules/overtime-daily-multiplier.js +118 -112
  136. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
  137. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts +6 -11
  138. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
  139. package/dist/cpsat/rules/overtime-daily-surcharge.js +104 -99
  140. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
  141. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +9 -21
  142. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
  143. package/dist/cpsat/rules/overtime-tiered-multiplier.js +181 -199
  144. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
  145. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts +13 -12
  146. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
  147. package/dist/cpsat/rules/overtime-weekly-multiplier.js +143 -143
  148. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
  149. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts +7 -11
  150. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
  151. package/dist/cpsat/rules/overtime-weekly-surcharge.js +124 -119
  152. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
  153. package/dist/cpsat/rules/pattern-eligibility.d.ts +6 -0
  154. package/dist/cpsat/rules/pattern-eligibility.d.ts.map +1 -0
  155. package/dist/cpsat/rules/pattern-eligibility.js +16 -0
  156. package/dist/cpsat/rules/pattern-eligibility.js.map +1 -0
  157. package/dist/cpsat/rules/pattern-time.d.ts +5 -0
  158. package/dist/cpsat/rules/pattern-time.d.ts.map +1 -0
  159. package/dist/cpsat/rules/pattern-time.js +20 -0
  160. package/dist/cpsat/rules/pattern-time.js.map +1 -0
  161. package/dist/cpsat/rules/registry.d.ts +217 -30
  162. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  163. package/dist/cpsat/rules/registry.js +98 -34
  164. package/dist/cpsat/rules/registry.js.map +1 -1
  165. package/dist/cpsat/rules/resolver.d.ts +9 -7
  166. package/dist/cpsat/rules/resolver.d.ts.map +1 -1
  167. package/dist/cpsat/rules/resolver.js +19 -18
  168. package/dist/cpsat/rules/resolver.js.map +1 -1
  169. package/dist/cpsat/rules/role-preference.d.ts +24 -0
  170. package/dist/cpsat/rules/role-preference.d.ts.map +1 -0
  171. package/dist/cpsat/rules/role-preference.js +72 -0
  172. package/dist/cpsat/rules/role-preference.js.map +1 -0
  173. package/dist/cpsat/rules/rules.types.d.ts +90 -17
  174. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  175. package/dist/cpsat/rules/scope.types.d.ts +24 -11
  176. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  177. package/dist/cpsat/rules/scope.types.js +14 -19
  178. package/dist/cpsat/rules/scope.types.js.map +1 -1
  179. package/dist/cpsat/rules/target-days-week.d.ts +31 -0
  180. package/dist/cpsat/rules/target-days-week.d.ts.map +1 -0
  181. package/dist/cpsat/rules/target-days-week.js +80 -0
  182. package/dist/cpsat/rules/target-days-week.js.map +1 -0
  183. package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts +33 -0
  184. package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts.map +1 -0
  185. package/dist/cpsat/rules/target-peak-concurrent-assignments.js +146 -0
  186. package/dist/cpsat/rules/target-peak-concurrent-assignments.js.map +1 -0
  187. package/dist/cpsat/rules/time-cost-surcharge.d.ts +15 -14
  188. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
  189. package/dist/cpsat/rules/time-cost-surcharge.js +82 -96
  190. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
  191. package/dist/cpsat/rules/time-off.d.ts +21 -57
  192. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  193. package/dist/cpsat/rules/time-off.js +114 -127
  194. package/dist/cpsat/rules/time-off.js.map +1 -1
  195. package/dist/cpsat/rules/variables.d.ts +4 -0
  196. package/dist/cpsat/rules/variables.d.ts.map +1 -0
  197. package/dist/cpsat/rules/variables.js +10 -0
  198. package/dist/cpsat/rules/variables.js.map +1 -0
  199. package/dist/cpsat/rules.d.ts +20 -3
  200. package/dist/cpsat/rules.d.ts.map +1 -1
  201. package/dist/cpsat/rules.js +19 -2
  202. package/dist/cpsat/rules.js.map +1 -1
  203. package/dist/cpsat/semantic-time.d.ts +7 -6
  204. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  205. package/dist/cpsat/semantic-time.js +15 -28
  206. package/dist/cpsat/semantic-time.js.map +1 -1
  207. package/dist/cpsat/types.d.ts +47 -4
  208. package/dist/cpsat/types.d.ts.map +1 -1
  209. package/dist/cpsat/types.js +24 -1
  210. package/dist/cpsat/types.js.map +1 -1
  211. package/dist/cpsat/utils.d.ts +23 -6
  212. package/dist/cpsat/utils.d.ts.map +1 -1
  213. package/dist/cpsat/utils.js +76 -15
  214. package/dist/cpsat/utils.js.map +1 -1
  215. package/dist/cpsat/validation-reporter.d.ts +6 -2
  216. package/dist/cpsat/validation-reporter.d.ts.map +1 -1
  217. package/dist/cpsat/validation-reporter.js +122 -10
  218. package/dist/cpsat/validation-reporter.js.map +1 -1
  219. package/dist/cpsat/validation.types.d.ts +21 -10
  220. package/dist/cpsat/validation.types.d.ts.map +1 -1
  221. package/dist/datetime.utils.d.ts +7 -13
  222. package/dist/datetime.utils.d.ts.map +1 -1
  223. package/dist/datetime.utils.js +29 -36
  224. package/dist/datetime.utils.js.map +1 -1
  225. package/dist/index.d.ts +12 -8
  226. package/dist/index.d.ts.map +1 -1
  227. package/dist/index.js +8 -3
  228. package/dist/index.js.map +1 -1
  229. package/dist/schedule/cost.d.ts +48 -60
  230. package/dist/schedule/cost.d.ts.map +1 -1
  231. package/dist/schedule/cost.js +15 -51
  232. package/dist/schedule/cost.js.map +1 -1
  233. package/dist/schedule/coverage.d.ts +57 -28
  234. package/dist/schedule/coverage.d.ts.map +1 -1
  235. package/dist/schedule/coverage.js +24 -1
  236. package/dist/schedule/coverage.js.map +1 -1
  237. package/dist/schedule/definition.d.ts +138 -19
  238. package/dist/schedule/definition.d.ts.map +1 -1
  239. package/dist/schedule/definition.js +99 -90
  240. package/dist/schedule/definition.js.map +1 -1
  241. package/dist/schedule/index.d.ts +4 -4
  242. package/dist/schedule/index.d.ts.map +1 -1
  243. package/dist/schedule/index.js +2 -2
  244. package/dist/schedule/index.js.map +1 -1
  245. package/dist/schedule/rules.d.ts +438 -113
  246. package/dist/schedule/rules.d.ts.map +1 -1
  247. package/dist/schedule/rules.js +310 -108
  248. package/dist/schedule/rules.js.map +1 -1
  249. package/dist/schedule/shift-patterns.d.ts +0 -17
  250. package/dist/schedule/shift-patterns.d.ts.map +1 -1
  251. package/dist/schedule/shift-patterns.js +0 -17
  252. package/dist/schedule/shift-patterns.js.map +1 -1
  253. package/dist/schedule/time-periods.d.ts +37 -27
  254. package/dist/schedule/time-periods.d.ts.map +1 -1
  255. package/dist/schedule/time-periods.js +37 -27
  256. package/dist/schedule/time-periods.js.map +1 -1
  257. package/dist/types.d.ts +67 -3
  258. package/dist/types.d.ts.map +1 -1
  259. package/dist/types.js +64 -0
  260. package/dist/types.js.map +1 -1
  261. package/package.json +8 -9
  262. package/solver/src/solver/models.py +41 -4
  263. package/solver/src/solver/solver.py +546 -157
  264. package/solver/tests/test_solver.py +363 -7
  265. package/src/client.schemas.ts +127 -10
  266. package/src/client.ts +3 -0
  267. package/src/client.types.ts +73 -7
  268. package/src/cpsat/cost.ts +17 -7
  269. package/src/cpsat/model-builder.ts +614 -129
  270. package/src/cpsat/response.ts +57 -13
  271. package/src/cpsat/rule-descriptor.ts +311 -0
  272. package/src/cpsat/rules/artifacts.ts +56 -0
  273. package/src/cpsat/rules/assign-together.ts +104 -69
  274. package/src/cpsat/rules/assigned-day.ts +70 -0
  275. package/src/cpsat/rules/assignment-priority.ts +54 -58
  276. package/src/cpsat/rules/assignment-terms.ts +43 -0
  277. package/src/cpsat/rules/concurrent-intervals.ts +93 -0
  278. package/src/cpsat/rules/cost-utils.ts +1 -9
  279. package/src/cpsat/rules/day-cost-multiplier.ts +101 -81
  280. package/src/cpsat/rules/day-cost-surcharge.ts +91 -70
  281. package/src/cpsat/rules/index.ts +30 -24
  282. package/src/cpsat/rules/location-preference.ts +47 -52
  283. package/src/cpsat/rules/max-concurrent-assignments.ts +170 -0
  284. package/src/cpsat/rules/max-consecutive-days.ts +74 -91
  285. package/src/cpsat/rules/max-days-of-week-per-period.ts +127 -0
  286. package/src/cpsat/rules/max-days-week.ts +89 -115
  287. package/src/cpsat/rules/max-hours-day.ts +64 -84
  288. package/src/cpsat/rules/max-hours-week.ts +74 -90
  289. package/src/cpsat/rules/max-shifts-day.ts +65 -85
  290. package/src/cpsat/rules/min-consecutive-days.ts +161 -127
  291. package/src/cpsat/rules/min-days-of-week-per-period.ts +144 -0
  292. package/src/cpsat/rules/min-days-week.ts +93 -91
  293. package/src/cpsat/rules/min-hours-day.ts +79 -58
  294. package/src/cpsat/rules/min-hours-week.ts +87 -62
  295. package/src/cpsat/rules/min-rest-between-shifts.ts +191 -178
  296. package/src/cpsat/rules/minimize-cost.ts +175 -188
  297. package/src/cpsat/rules/must-assign.ts +44 -81
  298. package/src/cpsat/rules/overtime-daily-multiplier.ts +158 -140
  299. package/src/cpsat/rules/overtime-daily-surcharge.ts +141 -126
  300. package/src/cpsat/rules/overtime-tiered-multiplier.ts +234 -253
  301. package/src/cpsat/rules/overtime-weekly-multiplier.ts +183 -175
  302. package/src/cpsat/rules/overtime-weekly-surcharge.ts +161 -150
  303. package/src/cpsat/rules/pattern-eligibility.ts +26 -0
  304. package/src/cpsat/rules/pattern-time.ts +30 -0
  305. package/src/cpsat/rules/registry.ts +140 -62
  306. package/src/cpsat/rules/resolver.ts +58 -30
  307. package/src/cpsat/rules/role-preference.ts +92 -0
  308. package/src/cpsat/rules/rules.types.ts +89 -17
  309. package/src/cpsat/rules/scope.types.ts +31 -26
  310. package/src/cpsat/rules/target-days-week.ts +105 -0
  311. package/src/cpsat/rules/target-peak-concurrent-assignments.ts +212 -0
  312. package/src/cpsat/rules/time-cost-surcharge.ts +116 -122
  313. package/src/cpsat/rules/time-off.ts +154 -163
  314. package/src/cpsat/rules/variables.ts +11 -0
  315. package/src/cpsat/rules.ts +43 -17
  316. package/src/cpsat/semantic-time.ts +29 -43
  317. package/src/cpsat/types.ts +53 -4
  318. package/src/cpsat/utils.ts +92 -20
  319. package/src/cpsat/validation-reporter.ts +170 -19
  320. package/src/cpsat/validation.types.ts +22 -11
  321. package/src/datetime.utils.ts +45 -44
  322. package/src/index.ts +64 -12
  323. package/src/schedule/cost.ts +40 -61
  324. package/src/schedule/coverage.ts +57 -28
  325. package/src/schedule/definition.ts +565 -76
  326. package/src/schedule/index.ts +23 -3
  327. package/src/schedule/rules.ts +631 -132
  328. package/src/schedule/shift-patterns.ts +0 -17
  329. package/src/schedule/time-periods.ts +37 -27
  330. package/src/types.ts +114 -2
@@ -1,8 +1,10 @@
1
- import { resolveDaysFromPeriod, toDayOfWeekUTC } from "../datetime.utils.js";
2
- import { MINUTES_PER_DAY, normalizeEndMinutes, OBJECTIVE_WEIGHTS, parseDayString, priorityToPenalty, timeOfDayToMinutes, } from "./utils.js";
1
+ import { resolveDaysFromPeriod } from "../datetime.utils.js";
2
+ import { MINUTES_PER_DAY, normalizeEndMinutes, OBJECTIVE_WEIGHTS, priorityToPenalty, timeOfDayToMinutes, } from "./utils.js";
3
3
  import { buildCpsatRules } from "./rules/resolver.js";
4
- import { builtInCpsatRuleFactories } from "./rules/registry.js";
4
+ import { builtInCpsatRuleRegistry } from "./rules/registry.js";
5
+ import { TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID } from "./rules/target-peak-concurrent-assignments.js";
5
6
  import { ValidationReporterImpl } from "./validation-reporter.js";
7
+ const UNSTAGED_OBJECTIVE_STAGE_ID = "__dabke_unstaged__";
6
8
  /**
7
9
  * Compilation context that creates variables, constraints, and objectives
8
10
  * and emits a `SolverRequest` for the Python CP-SAT solver service.
@@ -17,6 +19,7 @@ export class ModelBuilder {
17
19
  rules;
18
20
  weekStartsOn;
19
21
  options;
22
+ strategy;
20
23
  coverageBucketMinutes;
21
24
  reporter;
22
25
  fairDistribution;
@@ -25,21 +28,38 @@ export class ModelBuilder {
25
28
  #variables = new Map();
26
29
  #constraints = [];
27
30
  #objective = [];
31
+ #objectiveStageTerms = new Map();
32
+ #referencedObjectiveStages = new Set();
33
+ #objectiveStageOrder;
28
34
  #dayIndex = new Map();
35
+ #dayByIso = new Map();
29
36
  #shiftPatternMap = new Map();
30
37
  #builtRequest;
31
38
  #builtCompilation;
39
+ #compiledArtifacts = [];
40
+ #postSolveValidators = [];
41
+ #costArtifacts = [];
32
42
  constructor(config) {
33
43
  // Validate IDs don't contain the separator character
34
44
  for (const member of config.members) {
35
45
  if (member.id.includes(":")) {
36
46
  throw new Error(`Member ID "${member.id}" cannot contain colons`);
37
47
  }
48
+ for (const roleId of member.roleIds) {
49
+ if (roleId.includes(":")) {
50
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
51
+ }
52
+ }
38
53
  }
39
54
  for (const pattern of config.shiftPatterns) {
40
55
  if (pattern.id.includes(":")) {
41
56
  throw new Error(`Shift pattern ID "${pattern.id}" cannot contain colons`);
42
57
  }
58
+ for (const roleId of pattern.roleIds ?? []) {
59
+ if (roleId.includes(":")) {
60
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
61
+ }
62
+ }
43
63
  }
44
64
  // Validate coverage requirements have at least roles or skills
45
65
  for (const cov of config.coverage) {
@@ -48,22 +68,41 @@ export class ModelBuilder {
48
68
  if (!hasRoles && !hasSkills) {
49
69
  throw new Error(`Coverage requirement for day "${cov.day}" must have at least one of roles or skills`);
50
70
  }
71
+ for (const roleId of cov.roleIds ?? []) {
72
+ if (roleId.includes(":")) {
73
+ throw new Error(`Role ID "${roleId}" cannot contain colons`);
74
+ }
75
+ }
51
76
  }
52
77
  this.members = config.members;
53
78
  this.shiftPatterns = config.shiftPatterns;
54
79
  this.days = resolveDaysFromPeriod(config.schedulingPeriod);
55
80
  this.coverage = config.coverage;
81
+ const ruleCompileContext = {
82
+ members: this.members,
83
+ shiftPatterns: this.shiftPatterns,
84
+ days: this.days,
85
+ weekStartsOn: config.weekStartsOn ?? "monday",
86
+ coverageBucketMinutes: config.coverageBucketMinutes ?? 15,
87
+ };
56
88
  const compiledRuleConfigs = config.ruleConfigs
57
- ? buildCpsatRules(config.ruleConfigs, this.members, config.ruleFactories ?? builtInCpsatRuleFactories)
89
+ ? buildCpsatRules(config.ruleConfigs, this.members, ruleCompileContext, config.ruleRegistry ?? builtInCpsatRuleRegistry)
58
90
  : [];
59
91
  this.rules = [...compiledRuleConfigs, ...(config.rules ?? [])];
60
92
  this.weekStartsOn = config.weekStartsOn ?? "monday";
61
93
  this.options = config.solverOptions;
94
+ this.strategy = resolveModelSolveStrategy(config.strategy);
95
+ this.#objectiveStageOrder =
96
+ config.objectiveStageOrder === undefined ? undefined : [...config.objectiveStageOrder];
62
97
  this.coverageBucketMinutes = config.coverageBucketMinutes ?? 15;
63
98
  this.reporter = config.reporter ?? new ValidationReporterImpl();
64
99
  this.fairDistribution = config.fairDistribution ?? true;
65
- this.days.forEach((day, idx) => this.#dayIndex.set(day, idx));
100
+ this.days.forEach((day, idx) => {
101
+ this.#dayIndex.set(day.iso, idx);
102
+ this.#dayByIso.set(day.iso, day);
103
+ });
66
104
  this.shiftPatterns.forEach((pattern) => this.#shiftPatternMap.set(pattern.id, pattern));
105
+ this.#compiledArtifacts = this.rules.flatMap((rule) => rule.artifacts.map((artifact) => ({ rule: rule.rule, artifact })));
67
106
  }
68
107
  boolVar(name) {
69
108
  const existing = this.#variables.get(name);
@@ -88,16 +127,40 @@ export class ModelBuilder {
88
127
  return name;
89
128
  }
90
129
  shiftActive(patternId, day) {
91
- return this.boolVar(`shift:${patternId}:${day}`);
130
+ return this.boolVar(`shift:${patternId}:${day.iso}`);
92
131
  }
132
+ /**
133
+ * Returns the aggregate shift assignment variable for a member, pattern, and day.
134
+ *
135
+ * @remarks
136
+ * This variable means "the member works this shift pattern on this day". It is
137
+ * the presence literal for optional interval variables and the common variable
138
+ * used by shift-level rules, objectives, and skill-only coverage. Role-specific
139
+ * assignments are modeled separately and linked to this aggregate variable when
140
+ * a concrete role choice is available.
141
+ */
93
142
  assignment(memberId, patternId, day) {
94
- return this.boolVar(`assign:${memberId}:${patternId}:${day}`);
143
+ return this.boolVar(`assign:${memberId}:${patternId}:${day.iso}`);
95
144
  }
96
- addLinear(terms, op, rhs) {
97
- this.#constraints.push({ type: "linear", terms, op, rhs });
145
+ /** Returns the role-specific assignment variable for a concrete role choice. */
146
+ #roleAssignment(memberId, patternId, roleId, day) {
147
+ return this.boolVar(`assign_role:${memberId}:${patternId}:${roleId}:${day.iso}`);
98
148
  }
99
- addSoftLinear(terms, op, rhs, penalty, id) {
100
- this.#constraints.push({ type: "soft_linear", terms, op, rhs, penalty, id });
149
+ addLinear(terms, op, rhs, id) {
150
+ const constraint = id === undefined
151
+ ? { type: "linear", terms, op, rhs }
152
+ : { type: "linear", terms, op, rhs, id };
153
+ this.#constraints.push(constraint);
154
+ }
155
+ addSoftLinear(terms, op, rhs, penalty, id, stage) {
156
+ if (!this.#isOptimizing()) {
157
+ return;
158
+ }
159
+ this.#markObjectiveStageReference(stage);
160
+ const constraint = stage === undefined
161
+ ? { type: "soft_linear", terms, op, rhs, penalty, id }
162
+ : { type: "soft_linear", terms, op, rhs, penalty, id, stage };
163
+ this.#constraints.push(constraint);
101
164
  }
102
165
  addExactlyOne(vars) {
103
166
  if (vars.length === 0)
@@ -153,11 +216,25 @@ export class ModelBuilder {
153
216
  this.#constraints.push({ type: "no_overlap", intervals });
154
217
  }
155
218
  addPenalty(varName, weight) {
219
+ this.#addPenalty(varName, weight);
220
+ }
221
+ #addPenalty(varName, weight, stage) {
222
+ if (!this.#isOptimizing()) {
223
+ return;
224
+ }
225
+ this.#markObjectiveStageReference(stage);
156
226
  // CP-SAT requires integer coefficients. Round to nearest integer.
157
227
  const rounded = Math.round(weight);
158
228
  if (rounded === 0)
159
229
  return;
160
- this.#objective.push({ var: varName, coeff: rounded });
230
+ const term = { var: varName, coeff: rounded };
231
+ if (stage !== undefined) {
232
+ const stageTerms = this.#objectiveStageTerms.get(stage) ?? [];
233
+ stageTerms.push(term);
234
+ this.#objectiveStageTerms.set(stage, stageTerms);
235
+ return;
236
+ }
237
+ this.#objective.push(term);
161
238
  }
162
239
  membersWithRole(roleId) {
163
240
  return this.members.filter((m) => m.roleIds.includes(roleId));
@@ -193,19 +270,44 @@ export class ModelBuilder {
193
270
  }
194
271
  return pattern.roleIds.some((role) => member.roleIds.includes(role));
195
272
  }
273
+ #assignableRoleIds(member, pattern) {
274
+ const patternRoleIds = pattern.roleIds;
275
+ const roles = patternRoleIds && patternRoleIds.length > 0
276
+ ? member.roleIds.filter((roleId) => patternRoleIds.includes(roleId))
277
+ : member.roleIds;
278
+ return [...new Set(roles)];
279
+ }
280
+ /**
281
+ * Returns the variables that can satisfy one coverage bucket for this member and pattern.
282
+ *
283
+ * Role coverage uses role-specific variables so a single shift assignment fills
284
+ * one concrete role. Skill-only coverage has no role to choose, so it uses the
285
+ * aggregate assignment variable directly.
286
+ */
287
+ #coverageRoleAssignmentVars(member, pattern, cov, day) {
288
+ if (!cov.roleIds || cov.roleIds.length === 0) {
289
+ return [this.assignment(member.id, pattern.id, day)];
290
+ }
291
+ const assignableRoles = this.#assignableRoleIds(member, pattern);
292
+ const coverageRoles = assignableRoles.filter((roleId) => cov.roleIds.includes(roleId));
293
+ return coverageRoles.map((roleId) => this.#roleAssignment(member.id, pattern.id, roleId, day));
294
+ }
295
+ #canCoverCoverage(member, pattern, cov) {
296
+ const assignableRoles = this.#assignableRoleIds(member, pattern);
297
+ if (cov.roleIds && cov.roleIds.length > 0) {
298
+ return assignableRoles.some((roleId) => cov.roleIds.includes(roleId));
299
+ }
300
+ return true;
301
+ }
196
302
  /**
197
303
  * Checks if a shift pattern can be used on a specific day.
198
304
  * Returns false if the pattern has dayOfWeek restrictions that exclude this day.
199
305
  */
200
306
  patternAvailableOnDay(pattern, day) {
201
- // If pattern has no day restrictions, it's available every day
202
307
  if (!pattern.dayOfWeek || pattern.dayOfWeek.length === 0) {
203
308
  return true;
204
309
  }
205
- // Check if this day's day-of-week is in the allowed list
206
- const date = parseDayString(day);
207
- const dow = toDayOfWeekUTC(date);
208
- return pattern.dayOfWeek.includes(dow);
310
+ return pattern.dayOfWeek.includes(day.dayOfWeek);
209
311
  }
210
312
  patternDuration(patternId) {
211
313
  const pattern = this.#shiftPatternMap.get(patternId);
@@ -216,11 +318,11 @@ export class ModelBuilder {
216
318
  return end - start;
217
319
  }
218
320
  startMinutes(pattern, day) {
219
- const base = this.#dayOffset(day);
321
+ const base = this.#dayOffset(day.iso);
220
322
  return base + timeOfDayToMinutes(pattern.startTime);
221
323
  }
222
324
  endMinutes(pattern, day) {
223
- const base = this.#dayOffset(day);
325
+ const base = this.#dayOffset(day.iso);
224
326
  const startOffset = timeOfDayToMinutes(pattern.startTime);
225
327
  const endOffset = normalizeEndMinutes(startOffset, timeOfDayToMinutes(pattern.endTime));
226
328
  return base + endOffset;
@@ -229,12 +331,33 @@ export class ModelBuilder {
229
331
  if (this.#builtCompilation) {
230
332
  return this.#builtCompilation;
231
333
  }
232
- // 0. Apply all rules first (they may report exclusions and add constraints)
233
- for (const rule of this.rules) {
234
- rule.compile(this);
235
- }
236
- // Build exclusion lookup from reporter (populated by rules during compile)
334
+ this.#runRulePrechecks();
335
+ this.#applyRuleArtifacts();
336
+ // Build exclusion lookup from rule artifacts for coverage feasibility analysis.
237
337
  const mandatoryExclusions = buildExclusionLookup(this.reporter.getExclusions());
338
+ // Link concrete role choices to the aggregate assignment used by intervals and
339
+ // shift-level rules. This is the CP-SAT channeling pattern: exactly one role
340
+ // literal is true iff the aggregate assignment is true. Role-less members on
341
+ // unrestricted shifts deliberately skip this link; skill-only coverage uses
342
+ // the aggregate variable directly because there is no role decision to report.
343
+ for (const emp of this.members) {
344
+ for (const pattern of this.shiftPatterns) {
345
+ if (!this.canAssign(emp, pattern))
346
+ continue;
347
+ for (const day of this.days) {
348
+ if (!this.patternAvailableOnDay(pattern, day))
349
+ continue;
350
+ const assignmentVar = this.assignment(emp.id, pattern.id, day);
351
+ const roleTerms = this.#assignableRoleIds(emp, pattern).map((roleId) => ({
352
+ var: this.#roleAssignment(emp.id, pattern.id, roleId, day),
353
+ coeff: 1,
354
+ }));
355
+ if (roleTerms.length === 0)
356
+ continue;
357
+ this.addLinear([...roleTerms, { var: assignmentVar, coeff: -1 }], "==", 0);
358
+ }
359
+ }
360
+ }
238
361
  // 1. Assignment implies shift is active
239
362
  for (const emp of this.members) {
240
363
  for (const pattern of this.shiftPatterns) {
@@ -261,7 +384,7 @@ export class ModelBuilder {
261
384
  const start = this.startMinutes(pattern, day);
262
385
  const end = this.endMinutes(pattern, day);
263
386
  const size = end - start;
264
- const intervalName = `interval:${emp.id}:${pattern.id}:${day}`;
387
+ const intervalName = `interval:${emp.id}:${pattern.id}:${day.iso}`;
265
388
  this.intervalVar(intervalName, start, end, size, presenceVar);
266
389
  empIntervals.push(intervalName);
267
390
  }
@@ -304,6 +427,9 @@ export class ModelBuilder {
304
427
  patternsByBucketStart.set(bucketStart, patterns);
305
428
  }
306
429
  for (const cov of this.coverage) {
430
+ const covDay = this.#dayByIso.get(cov.day);
431
+ if (!covDay)
432
+ continue;
307
433
  const covStart = timeOfDayToMinutes(cov.startTime);
308
434
  const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
309
435
  const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
@@ -345,7 +471,7 @@ export class ModelBuilder {
345
471
  const lookupBucketStart = bucketStart % MINUTES_PER_DAY;
346
472
  // Get patterns that overlap this time bucket, then filter by day availability
347
473
  const allPatterns = patternsByBucketStart.get(lookupBucketStart) ?? [];
348
- const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, cov.day));
474
+ const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, covDay));
349
475
  if (patterns.length === 0) {
350
476
  recordBucketIssue(bucketIssues, {
351
477
  key: "no_patterns",
@@ -368,6 +494,8 @@ export class ModelBuilder {
368
494
  for (const pattern of patterns) {
369
495
  if (!this.canAssign(emp, pattern))
370
496
  continue;
497
+ if (!this.#canCoverCoverage(emp, pattern, cov))
498
+ continue;
371
499
  assignableMembers.add(emp.id);
372
500
  break;
373
501
  }
@@ -425,7 +553,9 @@ export class ModelBuilder {
425
553
  for (const emp of eligibleMembers) {
426
554
  if (!this.canAssign(emp, pattern))
427
555
  continue;
428
- coveringVarsSet.add(this.assignment(emp.id, pattern.id, cov.day));
556
+ for (const roleAssignment of this.#coverageRoleAssignmentVars(emp, pattern, cov, covDay)) {
557
+ coveringVarsSet.add(roleAssignment);
558
+ }
429
559
  }
430
560
  }
431
561
  const coveringVars = [...coveringVarsSet];
@@ -435,28 +565,31 @@ export class ModelBuilder {
435
565
  const terms = coveringVars.map((v) => ({ var: v, coeff: 1 }));
436
566
  const constraintId = `coverage:${covKey}:${cov.day}:${bucketStart}`;
437
567
  if (cov.priority === "MANDATORY") {
438
- this.addLinear(terms, ">=", cov.targetCount);
568
+ this.addLinear(terms, ">=", cov.targetCount, constraintId);
439
569
  }
440
- else {
570
+ else if (this.#isOptimizing()) {
441
571
  this.addSoftLinear(terms, ">=", cov.targetCount, priorityToPenalty(cov.priority), constraintId);
442
572
  }
443
- // Track all coverage constraints (mandatory and soft) for post-solve reporting
444
- this.reporter.trackConstraint({
445
- id: constraintId,
446
- type: "coverage",
447
- description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
448
- targetValue: cov.targetCount,
449
- comparator: ">=",
450
- day: cov.day,
451
- timeSlot: formatMinutes(bucketStart),
452
- roleIds: cov.roleIds,
453
- skillIds: cov.skillIds,
454
- context: {
455
- days: [cov.day],
456
- memberIds: eligibleMembers.map((e) => e.id),
457
- },
458
- group: cov.group,
459
- });
573
+ // Track coverage constraints that can produce post-solve feedback.
574
+ if (cov.priority === "MANDATORY" || this.#isOptimizing()) {
575
+ this.reporter.trackConstraint({
576
+ id: constraintId,
577
+ type: "coverage",
578
+ source: cov.priority === "MANDATORY" ? "hard" : "soft",
579
+ description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
580
+ targetValue: cov.targetCount,
581
+ comparator: ">=",
582
+ day: cov.day,
583
+ timeSlot: formatMinutes(bucketStart),
584
+ roleIds: cov.roleIds,
585
+ skillIds: cov.skillIds,
586
+ context: {
587
+ days: [cov.day],
588
+ memberIds: eligibleMembers.map((e) => e.id),
589
+ },
590
+ group: cov.group,
591
+ });
592
+ }
460
593
  }
461
594
  for (const issue of bucketIssues.values()) {
462
595
  const ranges = bucketStartsToRanges(issue.bucketStarts, bucket, covEnd).map((range) => `${formatMinutes(range.start)}-${formatMinutes(range.end)}`);
@@ -474,10 +607,22 @@ export class ModelBuilder {
474
607
  group: cov.group,
475
608
  });
476
609
  }
477
- // Note: soft coverage warnings are tracked via trackConstraint and reported post-solve
610
+ // Note: optimized solves report soft coverage post-solve via tracked constraints.
478
611
  }
479
612
  }
480
- // 3. Default objective: shift minimization with optional fair distribution
613
+ if (this.#isOptimizing()) {
614
+ this.#addDefaultObjective();
615
+ }
616
+ this.#builtRequest = this.#buildRequest();
617
+ this.#builtCompilation = {
618
+ request: this.#builtRequest,
619
+ validation: this.reporter.getValidation(),
620
+ canSolve: !this.reporter.hasErrors(),
621
+ };
622
+ return this.#builtCompilation;
623
+ }
624
+ #addDefaultObjective() {
625
+ // Default objective: shift minimization with optional fair distribution
481
626
  //
482
627
  // The objective has three components (see OBJECTIVE_WEIGHTS in utils.ts):
483
628
  // a) Minimize number of active shift patterns (SHIFT_ACTIVE=1000)
@@ -538,13 +683,6 @@ export class ModelBuilder {
538
683
  }
539
684
  }
540
685
  }
541
- this.#builtRequest = this.#buildRequest();
542
- this.#builtCompilation = {
543
- request: this.#builtRequest,
544
- validation: this.reporter.getValidation(),
545
- canSolve: !this.reporter.hasErrors(),
546
- };
547
- return this.#builtCompilation;
548
686
  }
549
687
  /**
550
688
  * Run post-solve validation on all rules.
@@ -555,12 +693,160 @@ export class ModelBuilder {
555
693
  members: this.members,
556
694
  days: this.days,
557
695
  shiftPatterns: this.shiftPatterns,
696
+ weekStartsOn: this.weekStartsOn,
697
+ };
698
+ for (const validator of this.#postSolveValidators) {
699
+ validator.run(assignments, this.reporter, context);
700
+ }
701
+ }
702
+ calculateCost(assignments) {
703
+ const context = {
704
+ members: this.members,
705
+ days: this.days,
706
+ shiftPatterns: this.shiftPatterns,
707
+ weekStartsOn: this.weekStartsOn,
708
+ };
709
+ const entries = this.#costArtifacts.flatMap((artifact) => artifact.calculateCost ? [...artifact.calculateCost(assignments, context).entries] : []);
710
+ return { entries };
711
+ }
712
+ #runRulePrechecks() {
713
+ const context = {
714
+ members: this.members,
715
+ days: this.days,
716
+ shiftPatterns: this.shiftPatterns,
717
+ weekStartsOn: this.weekStartsOn,
558
718
  };
559
719
  for (const rule of this.rules) {
560
- rule.validate?.(assignments, this.reporter, context);
720
+ const hasHardConstraint = rule.artifacts.some((artifact) => artifact.kind === "hard-constraint");
721
+ const shouldRunPrechecks = this.#isOptimizing() || hasHardConstraint;
722
+ if (!shouldRunPrechecks)
723
+ continue;
724
+ for (const artifact of rule.artifacts) {
725
+ if (artifact.kind === "pre-solve-feedback") {
726
+ artifact.run(context, this.reporter);
727
+ }
728
+ }
729
+ }
730
+ }
731
+ #applyRuleArtifacts() {
732
+ this.#postSolveValidators = [];
733
+ this.#costArtifacts = [];
734
+ for (const { rule, artifact } of this.#compiledArtifacts) {
735
+ switch (artifact.kind) {
736
+ case "variable": {
737
+ if (artifact.variable.type === "bool") {
738
+ this.boolVar(artifact.variable.name);
739
+ }
740
+ else {
741
+ this.intVar(artifact.variable.name, artifact.variable.min, artifact.variable.max);
742
+ }
743
+ break;
744
+ }
745
+ case "hard-constraint": {
746
+ const constraintId = artifact.validation.strategy === "report" ? artifact.validation.id : undefined;
747
+ this.addLinear([...artifact.terms], artifact.comparator, artifact.targetValue, constraintId);
748
+ this.#reportHardConstraint(rule, artifact);
749
+ break;
750
+ }
751
+ case "soft-constraint": {
752
+ if (!this.#isOptimizing()) {
753
+ break;
754
+ }
755
+ const solverConstraintId = artifact.validation.strategy === "report" ? artifact.constraintId : undefined;
756
+ this.addSoftLinear([...artifact.terms], artifact.comparator, artifact.targetValue, artifact.penalty, solverConstraintId, artifact.stage);
757
+ this.#reportSoftConstraint(rule, artifact);
758
+ break;
759
+ }
760
+ case "objective": {
761
+ if (!this.#isOptimizing()) {
762
+ break;
763
+ }
764
+ this.#markObjectiveStageReference(artifact.stage);
765
+ for (const term of artifact.terms) {
766
+ this.#addPenalty(term.var, term.coeff, artifact.stage);
767
+ }
768
+ break;
769
+ }
770
+ case "coverage-exclusion": {
771
+ this.reporter.excludeFromCoverage({
772
+ memberId: artifact.memberId,
773
+ day: artifact.day,
774
+ startTime: artifact.startTime,
775
+ endTime: artifact.endTime,
776
+ });
777
+ break;
778
+ }
779
+ case "pre-solve-feedback": {
780
+ break;
781
+ }
782
+ case "post-solve-feedback": {
783
+ if (!this.#isOptimizing()) {
784
+ break;
785
+ }
786
+ this.#postSolveValidators.push(artifact);
787
+ break;
788
+ }
789
+ case "cost": {
790
+ if (!this.#isOptimizing()) {
791
+ break;
792
+ }
793
+ artifact.compileObjective?.(this);
794
+ this.#costArtifacts.push(artifact);
795
+ break;
796
+ }
797
+ }
561
798
  }
562
799
  }
800
+ #reportHardConstraint(rule, artifact) {
801
+ if (artifact.validation.strategy !== "report") {
802
+ return;
803
+ }
804
+ this.reporter.trackConstraint({
805
+ id: artifact.validation.id,
806
+ type: "rule",
807
+ source: "hard",
808
+ rule,
809
+ description: artifact.description,
810
+ targetValue: artifact.targetValue,
811
+ comparator: artifact.comparator,
812
+ context: artifact.context,
813
+ group: artifact.group,
814
+ });
815
+ }
816
+ #reportSoftConstraint(rule, artifact) {
817
+ if (artifact.validation.strategy !== "report") {
818
+ return;
819
+ }
820
+ this.reporter.trackConstraint({
821
+ id: artifact.constraintId,
822
+ type: "rule",
823
+ source: "soft",
824
+ rule,
825
+ description: artifact.description,
826
+ targetValue: artifact.targetValue,
827
+ comparator: artifact.comparator,
828
+ context: artifact.context,
829
+ group: artifact.group,
830
+ });
831
+ }
563
832
  #buildRequest() {
833
+ if (!this.#isOptimizing()) {
834
+ return {
835
+ variables: Array.from(this.#variables.values()),
836
+ constraints: this.#constraints,
837
+ mode: "satisfy",
838
+ options: this.#satisfyOptions(),
839
+ };
840
+ }
841
+ const objectiveStageOrder = this.#resolveObjectiveStageOrder();
842
+ if (objectiveStageOrder) {
843
+ return {
844
+ variables: Array.from(this.#variables.values()),
845
+ constraints: this.#constraintsForStagedRequest(),
846
+ objectiveStages: this.#buildObjectiveStages(objectiveStageOrder),
847
+ options: this.options,
848
+ };
849
+ }
564
850
  return {
565
851
  variables: Array.from(this.#variables.values()),
566
852
  constraints: this.#constraints,
@@ -568,6 +854,102 @@ export class ModelBuilder {
568
854
  options: this.options,
569
855
  };
570
856
  }
857
+ #markObjectiveStageReference(stage) {
858
+ if (stage === undefined) {
859
+ return;
860
+ }
861
+ this.#referencedObjectiveStages.add(stage);
862
+ }
863
+ #isOptimizing() {
864
+ return this.strategy.type === "optimize";
865
+ }
866
+ #satisfyOptions() {
867
+ return {
868
+ ...this.options,
869
+ solutionLimit: this.options?.solutionLimit ?? 1,
870
+ };
871
+ }
872
+ #resolveObjectiveStageOrder() {
873
+ const referencedStages = [...this.#referencedObjectiveStages];
874
+ if (referencedStages.length === 0) {
875
+ return undefined;
876
+ }
877
+ this.#validateReferencedObjectiveStages(referencedStages);
878
+ if (this.options?.solutionLimit === 1) {
879
+ throw new Error("ModelBuilder objectiveStageOrder cannot be used with solverOptions.solutionLimit=1.");
880
+ }
881
+ const stageOrder = this.#objectiveStageOrder;
882
+ if (stageOrder === undefined) {
883
+ if (referencedStages.length === 1) {
884
+ return [referencedStages[0]];
885
+ }
886
+ throw new Error(`Multiple objective stages were referenced (${referencedStages.join(", ")}); provide ModelBuilder objectiveStageOrder explicitly.`);
887
+ }
888
+ if (referencedStages.includes(TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID)) {
889
+ throw new Error("targetPeakConcurrentAssignments cannot be combined with ModelBuilder objectiveStageOrder until the multi-stage objective API is public.");
890
+ }
891
+ if (stageOrder.length === 0) {
892
+ throw new Error("ModelBuilder objectiveStageOrder cannot be empty when provided.");
893
+ }
894
+ const declaredStages = new Set();
895
+ for (const stage of stageOrder) {
896
+ if (stage.trim() === "") {
897
+ throw new Error("ModelBuilder objectiveStageOrder cannot contain an empty stage id.");
898
+ }
899
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
900
+ throw new Error(`ModelBuilder objectiveStageOrder cannot contain reserved stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
901
+ }
902
+ if (declaredStages.has(stage)) {
903
+ throw new Error(`Duplicate objective stage id "${stage}" in objectiveStageOrder.`);
904
+ }
905
+ declaredStages.add(stage);
906
+ }
907
+ const allowedStages = new Set(declaredStages);
908
+ allowedStages.add(UNSTAGED_OBJECTIVE_STAGE_ID);
909
+ for (const stage of referencedStages) {
910
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
911
+ throw new Error(`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
912
+ }
913
+ if (!allowedStages.has(stage)) {
914
+ throw new Error(`Objective stage "${stage}" is not declared in ModelBuilder objectiveStageOrder.`);
915
+ }
916
+ }
917
+ return stageOrder;
918
+ }
919
+ #validateReferencedObjectiveStages(referencedStages) {
920
+ for (const stage of referencedStages) {
921
+ if (stage.trim() === "") {
922
+ throw new Error("Rule artifacts cannot use an empty objective stage id.");
923
+ }
924
+ if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
925
+ throw new Error(`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
926
+ }
927
+ }
928
+ }
929
+ #constraintsForStagedRequest() {
930
+ return this.#constraints.map((constraint) => {
931
+ if (constraint.type !== "soft_linear" || constraint.stage !== undefined) {
932
+ return constraint;
933
+ }
934
+ return { ...constraint, stage: UNSTAGED_OBJECTIVE_STAGE_ID };
935
+ });
936
+ }
937
+ #buildObjectiveStages(objectiveStageOrder) {
938
+ const stages = objectiveStageOrder.map((id) => ({
939
+ id,
940
+ sense: "minimize",
941
+ terms: this.#objectiveStageTerms.get(id) ?? [],
942
+ }));
943
+ const hasTailSoftConstraint = this.#constraints.some((constraint) => constraint.type === "soft_linear" && constraint.stage === undefined);
944
+ if (this.#objective.length > 0 || hasTailSoftConstraint) {
945
+ stages.push({
946
+ id: UNSTAGED_OBJECTIVE_STAGE_ID,
947
+ sense: "minimize",
948
+ terms: this.#objective,
949
+ });
950
+ }
951
+ return stages;
952
+ }
571
953
  #dayOffset(day) {
572
954
  const idx = this.#dayIndex.get(day);
573
955
  if (idx === undefined) {
@@ -576,6 +958,19 @@ export class ModelBuilder {
576
958
  return idx * MINUTES_PER_DAY;
577
959
  }
578
960
  }
961
+ function resolveModelSolveStrategy(strategy) {
962
+ if (strategy === undefined) {
963
+ return { type: "optimize" };
964
+ }
965
+ if (typeof strategy !== "object" || strategy === null || !("type" in strategy)) {
966
+ throw new Error("ModelBuilder strategy must be an object with a type field.");
967
+ }
968
+ const strategyType = strategy.type;
969
+ if (strategyType === "optimize" || strategyType === "feasibility-only") {
970
+ return { type: strategyType };
971
+ }
972
+ throw new Error(`Unknown ModelBuilder strategy "${String(strategyType)}".`);
973
+ }
579
974
  function recordBucketIssue(bucketIssues, issue, bucketStart) {
580
975
  const existing = bucketIssues.get(issue.key);
581
976
  if (existing) {