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,6 +1,6 @@
1
1
  import type { SolverResponse } from "../client.types.js";
2
2
  import type { ShiftPattern } from "./types.js";
3
- import type { TimeOfDay } from "../types.js";
3
+ import { isDateString, type DateString, type TimeOfDay } from "../types.js";
4
4
 
5
5
  /**
6
6
  * A raw assignment from the solver: which member works which shift on which day.
@@ -12,8 +12,10 @@ export interface ShiftAssignment {
12
12
  memberId: string;
13
13
  /** The shift pattern this member is assigned to. */
14
14
  shiftPatternId: string;
15
+ /** The concrete role this member is filling for the assignment. */
16
+ roleId?: string;
15
17
  /** The date of the assignment (YYYY-MM-DD). */
16
- day: string;
18
+ day: DateString;
17
19
  }
18
20
 
19
21
  /**
@@ -24,8 +26,10 @@ export interface ShiftAssignment {
24
26
  export interface ResolvedShiftAssignment {
25
27
  /** The assigned member's ID. */
26
28
  memberId: string;
29
+ /** The concrete role this member is filling for the assignment. */
30
+ roleId?: string;
27
31
  /** The date of the assignment (YYYY-MM-DD). */
28
- day: string;
32
+ day: DateString;
29
33
  /** When the shift starts. */
30
34
  startTime: TimeOfDay;
31
35
  /** When the shift ends. */
@@ -51,12 +55,20 @@ export interface SolverResult {
51
55
  /**
52
56
  * Extracts shift assignments from solver response.
53
57
  *
54
- * Parses variable names matching the pattern `assign:${memberId}:${patternId}:${day}`
55
- * and returns assignments where the variable value is 1 (true).
58
+ * Parses role-specific variable names matching the pattern
59
+ * `assign_role:${memberId}:${patternId}:${roleId}:${day}` and aggregate
60
+ * assignment variable names matching `assign:${memberId}:${patternId}:${day}`.
56
61
  *
57
62
  * @remarks
58
- * IDs are validated by ModelBuilder to not contain colons,
59
- * ensuring unambiguous parsing.
63
+ * The model uses aggregate assignment variables as the canonical shift presence
64
+ * literals for intervals, objectives, shift-level rules, and skill-only coverage.
65
+ * Role-specific variables are emitted when the solver also chooses a concrete
66
+ * role for that shift. When both variable shapes exist for the same member,
67
+ * pattern, and day, the role-specific assignment is returned so callers can see
68
+ * the selected role without receiving a duplicate aggregate assignment.
69
+ *
70
+ * IDs are validated by ModelBuilder to not contain colons, ensuring unambiguous
71
+ * parsing.
60
72
  *
61
73
  * @param response - The solver response containing variable values
62
74
  * @returns Parsed schedule result with assignments
@@ -85,22 +97,49 @@ export function parseSolverResponse(response: SolverResponse): SolverResult {
85
97
  };
86
98
  }
87
99
 
88
- const assignments: ShiftAssignment[] = [];
100
+ const roleAssignments = new Map<string, ShiftAssignment>();
101
+ const aggregateAssignments = new Map<string, ShiftAssignment>();
89
102
 
90
103
  for (const [varName, value] of Object.entries(response.values ?? {})) {
91
104
  if (value !== 1) continue;
105
+
106
+ if (varName.startsWith("assign_role:")) {
107
+ const parts = varName.split(":");
108
+ if (parts.length !== 5) continue;
109
+
110
+ const [, memberId, shiftPatternId, roleId, day] = parts;
111
+ if (!memberId || !shiftPatternId || !roleId || !day) continue;
112
+ if (!isDateString(day)) continue;
113
+
114
+ roleAssignments.set(assignmentKey(memberId, shiftPatternId, day), {
115
+ memberId,
116
+ shiftPatternId,
117
+ roleId,
118
+ day,
119
+ });
120
+ continue;
121
+ }
122
+
92
123
  if (!varName.startsWith("assign:")) continue;
93
124
 
94
- // Pattern: assign:${memberId}:${patternId}:${day}
95
- // IDs are validated to not contain colons, so splitting is unambiguous.
96
125
  const parts = varName.split(":");
97
126
  if (parts.length !== 4) continue;
98
-
99
127
  const [, memberId, shiftPatternId, day] = parts;
100
128
  if (!memberId || !shiftPatternId || !day) continue;
101
- if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
129
+ if (!isDateString(day)) continue;
130
+
131
+ aggregateAssignments.set(assignmentKey(memberId, shiftPatternId, day), {
132
+ memberId,
133
+ shiftPatternId,
134
+ day,
135
+ });
136
+ }
102
137
 
103
- assignments.push({ memberId, shiftPatternId, day });
138
+ const assignments = [...roleAssignments.values()];
139
+ for (const [key, assignment] of aggregateAssignments) {
140
+ if (!roleAssignments.has(key)) {
141
+ assignments.push(assignment);
142
+ }
104
143
  }
105
144
 
106
145
  return {
@@ -142,6 +181,7 @@ export function resolveAssignments(
142
181
 
143
182
  return {
144
183
  memberId: a.memberId,
184
+ ...(a.roleId ? { roleId: a.roleId } : {}),
145
185
  day: a.day,
146
186
  startTime: pattern.startTime,
147
187
  endTime: pattern.endTime,
@@ -149,3 +189,7 @@ export function resolveAssignments(
149
189
  })
150
190
  .filter((a): a is ResolvedShiftAssignment => a !== null);
151
191
  }
192
+
193
+ function assignmentKey(memberId: string, shiftPatternId: string, day: DateString): string {
194
+ return `${memberId}:${shiftPatternId}:${day}`;
195
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Rule descriptor contracts for CP-SAT compilation.
3
+ *
4
+ * @remarks
5
+ * Rules compile into declarative artifacts instead of mutating the solver
6
+ * builder directly. This keeps validation handling and backend lowering
7
+ * centralized. The user-facing feedback unit is the rule or validation group,
8
+ * not an individual solver constraint.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import type { z } from "zod";
14
+ import type { DateString, DayOfWeek, SchedulingDay, TimeOfDay } from "../types.js";
15
+ import type { ModelBuilder } from "./model-builder.js";
16
+ import type { ResolvedShiftAssignment, ShiftAssignment } from "./response.js";
17
+ import type { SchedulingMember, ShiftPattern, Term } from "./types.js";
18
+ import type { ValidationContext, ValidationGroup } from "./validation.types.js";
19
+ import type { ValidationReporter } from "./validation-reporter.js";
20
+
21
+ export type RuleMode =
22
+ | "hard-constraint"
23
+ | "soft-constraint"
24
+ | "objective"
25
+ | "coverage-exclusion"
26
+ | "pre-solve-feedback"
27
+ | "post-solve-feedback"
28
+ | "cost"
29
+ | "variable";
30
+
31
+ /**
32
+ * Inputs available while compiling a rule descriptor.
33
+ *
34
+ * @remarks
35
+ * Descriptor compilation happens before the model builder applies artifacts, so
36
+ * rules receive normalized scheduling inputs and return declarative artifacts
37
+ * instead of mutating the builder directly.
38
+ *
39
+ * @category Rules
40
+ */
41
+ export interface RuleCompileContext {
42
+ readonly members: readonly SchedulingMember[];
43
+ readonly shiftPatterns: readonly ShiftPattern[];
44
+ readonly days: readonly SchedulingDay[];
45
+ readonly weekStartsOn: DayOfWeek;
46
+ readonly coverageBucketMinutes?: number;
47
+ }
48
+
49
+ export type RulePreSolveFeedbackContext = RuleCompileContext;
50
+ export type RulePostSolveFeedbackContext = RuleCompileContext;
51
+ export type CostCalculationContext = RuleCompileContext;
52
+
53
+ export type PreSolveFeedbackFn = (
54
+ ctx: RulePreSolveFeedbackContext,
55
+ reporter: ValidationReporter,
56
+ ) => void;
57
+
58
+ export type PostSolveFeedbackFn = (
59
+ assignments: readonly ResolvedShiftAssignment[],
60
+ reporter: ValidationReporter,
61
+ ctx: RulePostSolveFeedbackContext,
62
+ ) => void;
63
+
64
+ /**
65
+ * Structured opt-out categories for artifact-level validation reporting.
66
+ *
67
+ * @category Rules
68
+ */
69
+ export type ValidationSkipCategory = "scaffolding" | "no-meaningful-feedback";
70
+
71
+ /**
72
+ * Validation strategy that reports a hard constraint into the validation pipeline.
73
+ *
74
+ * @category Rules
75
+ */
76
+ export interface ReportHardConstraintValidationStrategy {
77
+ readonly strategy: "report";
78
+ readonly id: string;
79
+ }
80
+
81
+ /**
82
+ * Validation strategy that reports a soft constraint into the validation pipeline.
83
+ *
84
+ * @remarks
85
+ * Soft constraints report using their artifact `constraintId`.
86
+ *
87
+ * @category Rules
88
+ */
89
+ export interface ReportSoftConstraintValidationStrategy {
90
+ readonly strategy: "report";
91
+ }
92
+
93
+ /**
94
+ * Validation strategy that explicitly skips validation reporting for an artifact.
95
+ *
96
+ * @category Rules
97
+ */
98
+ export interface SkipValidationStrategy {
99
+ readonly strategy: "skip";
100
+ readonly category: ValidationSkipCategory;
101
+ readonly rationale: string;
102
+ }
103
+
104
+ /**
105
+ * Validation strategy attached to hard constraints.
106
+ *
107
+ * @category Rules
108
+ */
109
+ export type HardConstraintValidationStrategy =
110
+ | ReportHardConstraintValidationStrategy
111
+ | SkipValidationStrategy;
112
+
113
+ /**
114
+ * Validation strategy attached to soft constraints.
115
+ *
116
+ * @category Rules
117
+ */
118
+ export type SoftConstraintValidationStrategy =
119
+ | ReportSoftConstraintValidationStrategy
120
+ | SkipValidationStrategy;
121
+
122
+ /**
123
+ * Validation strategy attached to objective artifacts.
124
+ *
125
+ * @category Rules
126
+ */
127
+ export type ObjectiveValidationStrategy = SkipValidationStrategy;
128
+
129
+ /**
130
+ * Validation strategy attached to cost artifacts.
131
+ *
132
+ * @category Rules
133
+ */
134
+ export type CostValidationStrategy = SkipValidationStrategy;
135
+
136
+ /**
137
+ * A compiled hard constraint emitted by a rule descriptor.
138
+ *
139
+ * @remarks
140
+ * Hard constraints are still implementation details. End-user feedback should
141
+ * normally be summarized at the rule or validation-group level. Artifact rule
142
+ * identity is inherited from the containing `CompiledRule`. Solver-stage
143
+ * reporting uses artifact-level validation strategies. Pre-solve feedback
144
+ * should be emitted via dedicated feedback artifacts.
145
+ *
146
+ * @category Rules
147
+ */
148
+ export interface HardConstraintArtifact {
149
+ readonly kind: "hard-constraint";
150
+ readonly group?: ValidationGroup;
151
+ readonly validation: HardConstraintValidationStrategy;
152
+ readonly description: string;
153
+ readonly context: ValidationContext;
154
+ readonly terms: readonly Term[];
155
+ readonly comparator: "<=" | ">=" | "==";
156
+ readonly targetValue: number;
157
+ }
158
+
159
+ export interface SoftConstraintArtifact {
160
+ readonly kind: "soft-constraint";
161
+ readonly group?: ValidationGroup;
162
+ readonly validation: SoftConstraintValidationStrategy;
163
+ readonly description: string;
164
+ readonly context: ValidationContext;
165
+ readonly terms: readonly Term[];
166
+ readonly comparator: "<=" | ">=";
167
+ readonly targetValue: number;
168
+ readonly penalty: number;
169
+ readonly constraintId: string;
170
+ /** Optional objective stage ID used for staged solver requests. */
171
+ readonly stage?: string;
172
+ }
173
+
174
+ export interface ObjectiveArtifact {
175
+ readonly kind: "objective";
176
+ readonly group?: ValidationGroup;
177
+ readonly terms: readonly Term[];
178
+ readonly validation: ObjectiveValidationStrategy;
179
+ /** Optional objective stage ID used for staged solver requests. */
180
+ readonly stage?: string;
181
+ }
182
+
183
+ export interface CoverageExclusionArtifact {
184
+ readonly kind: "coverage-exclusion";
185
+ readonly group?: ValidationGroup;
186
+ readonly memberId: string;
187
+ readonly day: DateString;
188
+ readonly startTime?: TimeOfDay;
189
+ readonly endTime?: TimeOfDay;
190
+ }
191
+
192
+ export interface PreSolveFeedbackArtifact {
193
+ readonly kind: "pre-solve-feedback";
194
+ readonly run: PreSolveFeedbackFn;
195
+ }
196
+
197
+ export interface PostSolveFeedbackArtifact {
198
+ readonly kind: "post-solve-feedback";
199
+ readonly run: PostSolveFeedbackFn;
200
+ }
201
+
202
+ export interface CostEntry {
203
+ readonly memberId: string;
204
+ readonly day: string;
205
+ readonly category: string;
206
+ readonly amount: number;
207
+ }
208
+
209
+ export interface CostContribution {
210
+ readonly entries: readonly CostEntry[];
211
+ }
212
+
213
+ export interface CostArtifact {
214
+ readonly kind: "cost";
215
+ readonly compileObjective?: (builder: ModelBuilder) => void;
216
+ readonly calculateCost?: (
217
+ assignments: readonly ShiftAssignment[],
218
+ ctx: CostCalculationContext,
219
+ ) => CostContribution;
220
+ readonly validation: CostValidationStrategy;
221
+ }
222
+
223
+ export type VariableArtifact =
224
+ | {
225
+ readonly kind: "variable";
226
+ readonly variable: { type: "bool"; name: string };
227
+ }
228
+ | {
229
+ readonly kind: "variable";
230
+ readonly variable: { type: "int"; name: string; min: number; max: number };
231
+ };
232
+
233
+ export type RuleArtifact =
234
+ | VariableArtifact
235
+ | HardConstraintArtifact
236
+ | SoftConstraintArtifact
237
+ | ObjectiveArtifact
238
+ | CoverageExclusionArtifact
239
+ | PreSolveFeedbackArtifact
240
+ | PostSolveFeedbackArtifact
241
+ | CostArtifact;
242
+
243
+ export interface CompiledRule {
244
+ readonly rule: string;
245
+ readonly artifacts: readonly RuleArtifact[];
246
+ }
247
+
248
+ /**
249
+ * A low-level CP-SAT rule descriptor.
250
+ *
251
+ * @remarks
252
+ * Advanced consumers can register custom descriptors via
253
+ * {@link createCpsatRuleRegistry}. Each descriptor owns runtime validation via
254
+ * its Zod schema and compiles a parsed config into declarative rule artifacts.
255
+ *
256
+ * @category Rules
257
+ */
258
+ export interface RuleDescriptor<Name extends string = string, Config = unknown> {
259
+ readonly name: Name;
260
+ readonly schema: z.ZodType<Config>;
261
+ compile(config: Config, ctx: RuleCompileContext): CompiledRule;
262
+ }
263
+
264
+ /**
265
+ * Defines a typed low-level CP-SAT rule descriptor.
266
+ *
267
+ * @remarks
268
+ * Use this for custom rules that should participate in the same descriptor and
269
+ * artifact pipeline as the built-in rules.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * const debugRuleDescriptor = defineRuleDescriptor({
274
+ * name: "debug",
275
+ * schema: z.object({ flag: z.boolean() }),
276
+ * compile(config) {
277
+ * return {
278
+ * rule: "debug",
279
+ * artifacts: config.flag ? [] : [],
280
+ * };
281
+ * },
282
+ * });
283
+ * ```
284
+ *
285
+ * @category Rules
286
+ */
287
+ export function defineRuleDescriptor<Name extends string, Schema extends z.ZodTypeAny>(descriptor: {
288
+ readonly name: Name;
289
+ readonly schema: Schema;
290
+ compile(config: z.infer<Schema>, ctx: RuleCompileContext): CompiledRule;
291
+ }): RuleDescriptor<Name, z.infer<Schema>> {
292
+ return descriptor as RuleDescriptor<Name, z.infer<Schema>>;
293
+ }
294
+
295
+ /**
296
+ * Parses raw config through a descriptor schema, then compiles it into artifacts.
297
+ *
298
+ * @remarks
299
+ * Schema parsing fails loudly. This helper does not recover from invalid JSON or
300
+ * invalid rule config.
301
+ *
302
+ * @category Rules
303
+ */
304
+ export function compileRuleDescriptor<Name extends string, Config>(
305
+ descriptor: RuleDescriptor<Name, Config>,
306
+ rawConfig: unknown,
307
+ ctx: RuleCompileContext,
308
+ ): CompiledRule {
309
+ const config = descriptor.schema.parse(rawConfig);
310
+ return descriptor.compile(config, ctx);
311
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ HardConstraintArtifact,
3
+ ReportHardConstraintValidationStrategy,
4
+ ReportSoftConstraintValidationStrategy,
5
+ SkipValidationStrategy,
6
+ SoftConstraintArtifact,
7
+ ValidationSkipCategory,
8
+ VariableArtifact,
9
+ } from "../rule-descriptor.js";
10
+
11
+ export function boolVariable(name: string): VariableArtifact {
12
+ return {
13
+ kind: "variable",
14
+ variable: { type: "bool", name },
15
+ };
16
+ }
17
+
18
+ export function intVariable(name: string, min: number, max: number): VariableArtifact {
19
+ return {
20
+ kind: "variable",
21
+ variable: { type: "int", name, min, max },
22
+ };
23
+ }
24
+
25
+ export function reportValidation(id: string): ReportHardConstraintValidationStrategy;
26
+ export function reportValidation(): ReportSoftConstraintValidationStrategy;
27
+ export function reportValidation(
28
+ id?: string,
29
+ ): ReportHardConstraintValidationStrategy | ReportSoftConstraintValidationStrategy {
30
+ return id ? { strategy: "report", id } : { strategy: "report" };
31
+ }
32
+
33
+ export function skipValidation(
34
+ category: ValidationSkipCategory,
35
+ rationale: string,
36
+ ): SkipValidationStrategy {
37
+ return { strategy: "skip", category, rationale };
38
+ }
39
+
40
+ export function hardConstraint(
41
+ params: Omit<HardConstraintArtifact, "kind">,
42
+ ): HardConstraintArtifact {
43
+ return {
44
+ ...params,
45
+ kind: "hard-constraint",
46
+ } satisfies HardConstraintArtifact;
47
+ }
48
+
49
+ export function softConstraint(
50
+ params: Omit<SoftConstraintArtifact, "kind">,
51
+ ): SoftConstraintArtifact {
52
+ return {
53
+ ...params,
54
+ kind: "soft-constraint",
55
+ } satisfies SoftConstraintArtifact;
56
+ }
@@ -1,9 +1,12 @@
1
1
  import * as z from "zod";
2
- import type { CompilationRule } from "../model-builder.js";
2
+ import { defineRuleDescriptor } from "../rule-descriptor.js";
3
3
  import { priorityToPenalty } from "../utils.js";
4
4
  import { PrioritySchema } from "./scope.types.js";
5
+ import { assignmentVar } from "./variables.js";
6
+ import { boolVariable, hardConstraint, reportValidation, skipValidation } from "./artifacts.js";
7
+ import { canAssignMemberToPattern, isPatternAvailableOnDay } from "./pattern-eligibility.js";
5
8
 
6
- const AssignTogetherSchema = z.object({
9
+ export const AssignTogetherSchema = z.object({
7
10
  groupMemberIds: z
8
11
  .tuple([z.string(), z.string()])
9
12
  .rest(z.string())
@@ -14,86 +17,118 @@ const AssignTogetherSchema = z.object({
14
17
  });
15
18
 
16
19
  /**
17
- * Configuration for {@link createAssignTogetherRule}.
18
- *
19
- * - `groupMemberIds` (required): member IDs to assign together (at least two, must be unique)
20
- * - `priority` (required): how strictly the solver enforces this rule
20
+ * Configuration for {@link assignTogetherRuleDescriptor}.
21
21
  */
22
22
  export type AssignTogetherConfig = z.infer<typeof AssignTogetherSchema>;
23
23
 
24
24
  /**
25
- * Encourages or enforces that team members in the group work the same shift patterns on a day.
26
- * For each pair of team members in the group, ensures they are assigned to the same shifts.
25
+ * Low-level descriptor for the `assign-together` rule.
26
+ *
27
+ * @remarks
28
+ * Register this in a custom rule registry when you need the descriptor API
29
+ * directly instead of the high-level {@link assignTogether} helper.
27
30
  *
28
- * @param config - See {@link AssignTogetherConfig}
29
- * @example
30
- * ```ts
31
- * const rule = createAssignTogetherRule({
32
- * groupMemberIds: ["alice", "bob", "charlie"],
33
- * priority: "HIGH",
34
- * });
35
- * builder = new ModelBuilder({ ...config, rules: [rule] });
36
- * ```
31
+ * @category Rules
37
32
  */
38
- export function createAssignTogetherRule(config: AssignTogetherConfig): CompilationRule {
39
- const { groupMemberIds, priority } = AssignTogetherSchema.parse(config);
33
+ export const assignTogetherRuleDescriptor = defineRuleDescriptor({
34
+ name: "assign-together",
35
+ schema: AssignTogetherSchema,
36
+ compile(config, ctx) {
37
+ const { groupMemberIds, priority } = config;
38
+ const targetMembers = groupMemberIds
39
+ .map((id: string) => ctx.members.find((member) => member.id === id))
40
+ .filter((member): member is NonNullable<typeof member> => member !== undefined);
40
41
 
41
- return {
42
- compile(b) {
43
- const targetMembers = groupMemberIds
44
- .map((id) => b.members.find((m) => m.id === id))
45
- .filter((m): m is NonNullable<typeof m> => m !== undefined);
42
+ if (targetMembers.length < 2) {
43
+ return { rule: "assign-together", artifacts: [] };
44
+ }
46
45
 
47
- if (targetMembers.length < 2) return;
46
+ const artifacts = targetMembers.flatMap((member, index) => {
47
+ const next = targetMembers[index + 1];
48
+ if (!next) return [];
48
49
 
49
- for (let i = 0; i < targetMembers.length - 1; i++) {
50
- const m1 = targetMembers[i]!;
51
- const m2 = targetMembers[i + 1]!;
50
+ return ctx.shiftPatterns.flatMap((pattern) => {
51
+ if (
52
+ !canAssignMemberToPattern(member, pattern) ||
53
+ !canAssignMemberToPattern(next, pattern)
54
+ ) {
55
+ return [];
56
+ }
52
57
 
53
- for (const pattern of b.shiftPatterns) {
54
- const canAssign1 = b.canAssign(m1, pattern);
55
- const canAssign2 = b.canAssign(m2, pattern);
56
- if (!canAssign1 || !canAssign2) continue;
58
+ return ctx.days.flatMap((day) => {
59
+ if (!isPatternAvailableOnDay(pattern, day)) return [];
57
60
 
58
- for (const day of b.days) {
59
- if (!b.patternAvailableOnDay(pattern, day)) continue;
60
- const var1 = b.assignment(m1.id, pattern.id, day);
61
- const var2 = b.assignment(m2.id, pattern.id, day);
61
+ const var1 = assignmentVar(member.id, pattern.id, day.iso);
62
+ const var2 = assignmentVar(next.id, pattern.id, day.iso);
63
+ const description = `${member.id} and ${next.id} stay together on ${pattern.id} for ${day.iso}`;
64
+ const context = { memberIds: [member.id, next.id], days: [day.iso] };
65
+ const constraintId = `assign-together:${member.id}:${next.id}:${pattern.id}:${day.iso}`;
62
66
 
63
- if (priority === "MANDATORY") {
64
- b.addLinear(
65
- [
67
+ if (priority === "MANDATORY") {
68
+ return [
69
+ hardConstraint({
70
+ description,
71
+ context,
72
+ validation: reportValidation(constraintId),
73
+ terms: [
66
74
  { var: var1, coeff: 1 },
67
75
  { var: var2, coeff: -1 },
68
76
  ],
69
- "==",
70
- 0,
71
- );
72
- } else {
73
- const diffVar = b.boolVar(`together_diff_${m1.id}_${m2.id}_${pattern.id}_${day}`);
74
- b.addLinear(
75
- [
76
- { var: diffVar, coeff: 1 },
77
- { var: var1, coeff: -1 },
78
- { var: var2, coeff: 1 },
79
- ],
80
- ">=",
81
- 0,
82
- );
83
- b.addLinear(
84
- [
85
- { var: diffVar, coeff: 1 },
86
- { var: var1, coeff: 1 },
87
- { var: var2, coeff: -1 },
88
- ],
89
- ">=",
90
- 0,
91
- );
92
- b.addPenalty(diffVar, priorityToPenalty(priority));
93
- }
77
+ comparator: "==",
78
+ targetValue: 0,
79
+ }),
80
+ ];
94
81
  }
95
- }
96
- }
97
- },
98
- };
99
- }
82
+
83
+ const diffVar = `together_diff_${member.id}_${next.id}_${pattern.id}_${day.iso}`;
84
+ return [
85
+ boolVariable(diffVar),
86
+ hardConstraint({
87
+ description: `${diffVar} captures ${var1} without ${var2}`,
88
+ validation: skipValidation(
89
+ "scaffolding",
90
+ "This helper variable measures pair mismatch so a soft pairing preference can be optimized without surfacing helper-level feedback.",
91
+ ),
92
+ context,
93
+ terms: [
94
+ { var: diffVar, coeff: 1 },
95
+ { var: var1, coeff: -1 },
96
+ { var: var2, coeff: 1 },
97
+ ],
98
+ comparator: ">=",
99
+ targetValue: 0,
100
+ }),
101
+ hardConstraint({
102
+ description: `${diffVar} captures ${var2} without ${var1}`,
103
+ validation: skipValidation(
104
+ "scaffolding",
105
+ "This helper variable measures pair mismatch symmetrically so the objective can penalize either member being assigned alone.",
106
+ ),
107
+ context,
108
+ terms: [
109
+ { var: diffVar, coeff: 1 },
110
+ { var: var1, coeff: 1 },
111
+ { var: var2, coeff: -1 },
112
+ ],
113
+ comparator: ">=",
114
+ targetValue: 0,
115
+ }),
116
+ {
117
+ kind: "objective" as const,
118
+ terms: [{ var: diffVar, coeff: Math.max(1, priorityToPenalty(priority) * 5) }],
119
+ validation: skipValidation(
120
+ "no-meaningful-feedback",
121
+ "Pairing preferences influence the objective but do not emit validation feedback.",
122
+ ),
123
+ },
124
+ ];
125
+ });
126
+ });
127
+ });
128
+
129
+ return {
130
+ rule: "assign-together",
131
+ artifacts,
132
+ };
133
+ },
134
+ });