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,26 +1,30 @@
1
1
  import * as z from "zod";
2
2
  import type { TimeOfDay } from "../../types.js";
3
- import type { CompilationRule, RuleValidationContext } from "../model-builder.js";
3
+ import { defineRuleDescriptor } from "../rule-descriptor.js";
4
+ import type { RuleArtifact, RuleCompileContext } from "../rule-descriptor.js";
5
+ import type { ValidationReporter } from "../validation-reporter.js";
4
6
  import type { ResolvedShiftAssignment } from "../response.js";
5
7
  import { normalizeEndMinutes, priorityToPenalty, timeOfDayToMinutes } from "../utils.js";
6
- import type { ValidationReporter } from "../validation-reporter.js";
7
8
  import {
8
9
  PrioritySchema,
9
10
  entityScope,
10
- requiredTimeScope,
11
11
  parseEntityScope,
12
12
  parseTimeScope,
13
- resolveMembersFromScope,
13
+ requiredTimeScope,
14
14
  resolveActiveDaysFromScope,
15
+ resolveMembersFromScope,
15
16
  ruleGroup,
16
17
  } from "./scope.types.js";
18
+ import { assignmentVar } from "./variables.js";
19
+ import { hardConstraint, reportValidation, softConstraint } from "./artifacts.js";
20
+ import { canAssignMemberToPattern, isPatternAvailableOnDay } from "./pattern-eligibility.js";
17
21
 
18
22
  const timeOfDaySchema = z.object({
19
23
  hours: z.number().int().min(0).max(23),
20
24
  minutes: z.number().int().min(0).max(59),
21
25
  });
22
26
 
23
- const TimeOffSchema = z
27
+ export const TimeOffSchema = z
24
28
  .object({
25
29
  priority: PrioritySchema,
26
30
  startTime: timeOfDaySchema.optional(),
@@ -40,173 +44,143 @@ const TimeOffSchema = z
40
44
  );
41
45
 
42
46
  /**
43
- * Configuration for {@link createTimeOffRule}.
44
- *
45
- * - `priority` (required): how strictly the solver enforces this rule
46
- * - `startTime` (optional): start of the time-off window within each day; must be paired with `endTime`
47
- * - `endTime` (optional): end of the time-off window within each day; must be paired with `startTime`
48
- *
49
- * Entity scoping (at most one):
50
- * - `memberIds`: restrict to specific members
51
- * - `roleIds`: restrict to members with matching roles
52
- * - `skillIds`: restrict to members with matching skills
53
- *
54
- * Time scoping (exactly one required):
55
- * - `dateRange`: contiguous date range
56
- * - `specificDates`: specific dates
57
- * - `dayOfWeek`: days of the week
58
- * - `recurringPeriods`: recurring calendar periods
47
+ * Configuration for {@link timeOffRuleDescriptor}.
59
48
  */
60
49
  export type TimeOffConfig = z.infer<typeof TimeOffSchema>;
61
50
 
62
51
  /**
63
- * Blocks or penalizes assignments during specified time periods.
52
+ * Low-level descriptor for the `time-off` rule.
64
53
  *
65
- * Supports entity scoping (people, roles, skills) and time scoping
66
- * (date ranges, specific dates, days of week, recurring periods).
67
- * Optionally supports partial-day time-off with startTime/endTime.
68
- *
69
- * @param config - See {@link TimeOffConfig}
70
- * @example Full day vacation
71
- * ```ts
72
- * createTimeOffRule({
73
- * memberIds: ["alice"],
74
- * dateRange: { start: "2024-02-01", end: "2024-02-05" },
75
- * priority: "MANDATORY",
76
- * });
77
- * ```
78
- *
79
- * @example Every Wednesday afternoon off for students
80
- * ```ts
81
- * createTimeOffRule({
82
- * roleIds: ["student"],
83
- * dayOfWeek: ["wednesday"],
84
- * startTime: { hours: 14, minutes: 0 },
85
- * endTime: { hours: 23, minutes: 59 },
86
- * priority: "MANDATORY",
87
- * });
88
- * ```
89
- *
90
- * @example Specific date, partial day
91
- * ```ts
92
- * createTimeOffRule({
93
- * memberIds: ["bob"],
94
- * specificDates: ["2024-03-15"],
95
- * startTime: { hours: 16, minutes: 0 },
96
- * endTime: { hours: 23, minutes: 59 },
97
- * priority: "MANDATORY",
98
- * });
99
- * ```
54
+ * @category Rules
100
55
  */
101
- export function createTimeOffRule(config: TimeOffConfig): CompilationRule {
102
- const parsed = TimeOffSchema.parse(config);
103
- const { priority, startTime, endTime } = parsed;
104
-
105
- const fullDayStart: TimeOfDay = { hours: 0, minutes: 0 };
106
- const fullDayEnd: TimeOfDay = { hours: 23, minutes: 59 };
107
- const timeWindowStart = startTime ?? fullDayStart;
108
- const timeWindowEnd = endTime ?? fullDayEnd;
109
-
110
- const entityScopeValue = parseEntityScope(parsed);
111
- const timeScopeValue = parseTimeScope(parsed);
112
- const group = ruleGroup("time-off", "Time off", entityScopeValue, timeScopeValue);
113
-
114
- return {
115
- compile(builder) {
116
- const targetMembers = resolveMembersFromScope(entityScopeValue, builder.members);
117
- if (targetMembers.length === 0) return;
118
-
119
- const activeDays = resolveActiveDaysFromScope(timeScopeValue, builder.days);
120
- if (activeDays.length === 0) return;
121
-
122
- for (const emp of targetMembers) {
123
- for (const day of activeDays) {
124
- // Report exclusion for coverage analysis (MANDATORY only)
125
- if (priority === "MANDATORY") {
126
- builder.reporter.excludeFromCoverage({
127
- memberId: emp.id,
128
- day,
129
- startTime: timeWindowStart,
130
- endTime: timeWindowEnd,
131
- });
132
- }
133
-
134
- for (const pattern of builder.shiftPatterns) {
135
- if (!builder.canAssign(emp, pattern)) continue;
136
- if (!builder.patternAvailableOnDay(pattern, day)) continue;
137
-
138
- if (startTime && endTime) {
139
- if (!shiftOverlapsTimeWindow(pattern, startTime, endTime)) {
140
- continue;
141
- }
142
- }
143
-
144
- const assignVar = builder.assignment(emp.id, pattern.id, day);
145
-
146
- if (priority === "MANDATORY") {
147
- builder.addLinear([{ var: assignVar, coeff: 1 }], "==", 0);
148
- } else {
149
- builder.addSoftLinear(
150
- [{ var: assignVar, coeff: 1 }],
151
- "<=",
152
- 0,
153
- priorityToPenalty(priority),
154
- );
155
- }
156
- }
157
- }
158
- }
159
- },
160
-
161
- validate(
162
- assignments: ResolvedShiftAssignment[],
163
- reporter: ValidationReporter,
164
- context: RuleValidationContext,
165
- ) {
166
- // MANDATORY time-off is a hard constraint - can't be violated
167
- if (priority === "MANDATORY") return;
168
-
169
- const targetMembers = resolveMembersFromScope(entityScopeValue, context.members);
170
- if (targetMembers.length === 0) return;
171
-
172
- const activeDays = resolveActiveDaysFromScope(timeScopeValue, context.days);
173
- if (activeDays.length === 0) return;
174
-
175
- for (const emp of targetMembers) {
176
- for (const day of activeDays) {
177
- const violated = assignments.some(
178
- (a) =>
179
- a.memberId === emp.id &&
180
- a.day === day &&
181
- assignmentOverlapsTimeWindow(a, timeWindowStart, timeWindowEnd),
182
- );
183
-
184
- if (violated) {
185
- reporter.reportRuleViolation({
186
- rule: "time-off",
187
- message: `Time-off request for ${emp.id} on ${day} could not be honored`,
188
- context: {
189
- memberIds: [emp.id],
190
- days: [day],
191
- },
56
+ export const timeOffRuleDescriptor = defineRuleDescriptor({
57
+ name: "time-off",
58
+ schema: TimeOffSchema,
59
+ compile(config, ctx) {
60
+ const { priority, startTime, endTime } = config;
61
+ const fullDayStart: TimeOfDay = { hours: 0, minutes: 0 };
62
+ const fullDayEnd: TimeOfDay = { hours: 23, minutes: 59 };
63
+ const timeWindowStart = startTime ?? fullDayStart;
64
+ const timeWindowEnd = endTime ?? fullDayEnd;
65
+
66
+ const entityScopeValue = parseEntityScope(config);
67
+ const timeScopeValue = parseTimeScope(config);
68
+ const group = ruleGroup("time-off", "Time off", entityScopeValue, timeScopeValue);
69
+ const targetMembers = resolveMembersFromScope(entityScopeValue, [...ctx.members]);
70
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, [...ctx.days]);
71
+ const timeWindowKey = formatTimeWindowKey(timeWindowStart, timeWindowEnd);
72
+
73
+ const constraintArtifacts: RuleArtifact[] = [];
74
+ for (const member of targetMembers) {
75
+ for (const day of activeDays) {
76
+ const overlappingVars = ctx.shiftPatterns
77
+ .filter(
78
+ (pattern) =>
79
+ canAssignMemberToPattern(member, pattern) &&
80
+ isPatternAvailableOnDay(pattern, day) &&
81
+ shiftOverlapsTimeWindow(pattern, timeWindowStart, timeWindowEnd),
82
+ )
83
+ .map((pattern) => assignmentVar(member.id, pattern.id, day.iso));
84
+
85
+ if (overlappingVars.length === 0) continue;
86
+
87
+ const description = buildTimeOffDescription(
88
+ member.id,
89
+ day.iso,
90
+ timeWindowStart,
91
+ timeWindowEnd,
92
+ );
93
+ const context = { memberIds: [member.id], days: [day.iso] };
94
+ const constraintId = `time-off:${group.key}:${member.id}:${day.iso}:${timeWindowKey}`;
95
+ const terms = overlappingVars.map((varName) => ({ var: varName, coeff: 1 }));
96
+
97
+ if (priority === "MANDATORY") {
98
+ constraintArtifacts.push({
99
+ kind: "coverage-exclusion",
100
+ group,
101
+ memberId: member.id,
102
+ day: day.iso,
103
+ startTime: timeWindowStart,
104
+ endTime: timeWindowEnd,
105
+ });
106
+ constraintArtifacts.push(
107
+ hardConstraint({
192
108
  group,
193
- });
194
- } else {
195
- reporter.reportRulePassed({
196
- rule: "time-off",
197
- message: `Time-off honored for ${emp.id} on ${day}`,
198
- context: {
199
- memberIds: [emp.id],
200
- days: [day],
201
- },
109
+ description,
110
+ context,
111
+ validation: reportValidation(constraintId),
112
+ terms,
113
+ comparator: "<=",
114
+ targetValue: 0,
115
+ }),
116
+ );
117
+ } else {
118
+ constraintArtifacts.push(
119
+ softConstraint({
202
120
  group,
203
- });
204
- }
121
+ description,
122
+ context,
123
+ validation: reportValidation(),
124
+ terms,
125
+ comparator: "<=",
126
+ targetValue: 0,
127
+ penalty: priorityToPenalty(priority),
128
+ constraintId,
129
+ }),
130
+ );
205
131
  }
206
132
  }
207
- },
208
- };
209
- }
133
+ }
134
+
135
+ const postSolveValidator =
136
+ priority === "MANDATORY"
137
+ ? []
138
+ : [
139
+ {
140
+ kind: "post-solve-feedback" as const,
141
+ run(
142
+ assignments: readonly ResolvedShiftAssignment[],
143
+ reporter: ValidationReporter,
144
+ validationContext: RuleCompileContext,
145
+ ) {
146
+ const validatorMembers = resolveMembersFromScope(entityScopeValue, [
147
+ ...validationContext.members,
148
+ ]);
149
+ const validatorDays = resolveActiveDaysFromScope(timeScopeValue, [
150
+ ...validationContext.days,
151
+ ]);
152
+
153
+ for (const member of validatorMembers) {
154
+ for (const day of validatorDays) {
155
+ const violated = assignments.some(
156
+ (assignment) =>
157
+ assignment.memberId === member.id &&
158
+ assignment.day === day.iso &&
159
+ assignmentOverlapsTimeWindow(assignment, timeWindowStart, timeWindowEnd),
160
+ );
161
+
162
+ if (violated) {
163
+ continue;
164
+ }
165
+
166
+ reporter.reportRulePassed({
167
+ rule: "time-off",
168
+ message: `Time-off honored for ${member.id} on ${day.iso}`,
169
+ context: { memberIds: [member.id], days: [day.iso] },
170
+ group,
171
+ });
172
+ }
173
+ }
174
+ },
175
+ },
176
+ ];
177
+
178
+ return {
179
+ rule: "time-off",
180
+ artifacts: [...constraintArtifacts, ...postSolveValidator],
181
+ };
182
+ },
183
+ });
210
184
 
211
185
  function shiftOverlapsTimeWindow(
212
186
  pattern: { startTime: TimeOfDay; endTime: TimeOfDay },
@@ -223,7 +197,7 @@ function shiftOverlapsTimeWindow(
223
197
  }
224
198
 
225
199
  function assignmentOverlapsTimeWindow(
226
- assignment: ResolvedShiftAssignment,
200
+ assignment: { startTime: TimeOfDay; endTime: TimeOfDay },
227
201
  windowStart: TimeOfDay,
228
202
  windowEnd: TimeOfDay,
229
203
  ): boolean {
@@ -235,3 +209,20 @@ function assignmentOverlapsTimeWindow(
235
209
 
236
210
  return Math.max(assignStart, winStart) < Math.min(assignEnd, winEnd);
237
211
  }
212
+
213
+ function buildTimeOffDescription(
214
+ memberId: string,
215
+ dayIso: string,
216
+ windowStart: TimeOfDay,
217
+ windowEnd: TimeOfDay,
218
+ ): string {
219
+ return `Time off for ${memberId} on ${dayIso} (${formatTimeWindowKey(windowStart, windowEnd)})`;
220
+ }
221
+
222
+ function formatTimeWindowKey(windowStart: TimeOfDay, windowEnd: TimeOfDay): string {
223
+ return `${formatTimeOfDay(windowStart)}-${formatTimeOfDay(windowEnd)}`;
224
+ }
225
+
226
+ function formatTimeOfDay(time: TimeOfDay): string {
227
+ return `${time.hours.toString().padStart(2, "0")}:${time.minutes.toString().padStart(2, "0")}`;
228
+ }
@@ -0,0 +1,11 @@
1
+ export function assignmentVar(memberId: string, patternId: string, dayIso: string): string {
2
+ return `assign:${memberId}:${patternId}:${dayIso}`;
3
+ }
4
+
5
+ export function assignedDayVariableName(memberId: string, dayIso: string): string {
6
+ return `assigned_day_${memberId}_${dayIso}`;
7
+ }
8
+
9
+ export function assignedDayStartVariableName(memberId: string, dayIso: string): string {
10
+ return `assigned_day_start_${memberId}_${dayIso}`;
11
+ }
@@ -1,27 +1,53 @@
1
+ /**
2
+ * Low-level CP-SAT rule descriptor and registry APIs.
3
+ *
4
+ * @remarks
5
+ * These exports are intended for advanced consumers building custom rule
6
+ * registries or integrating directly with the descriptor layer underneath the
7
+ * high-level {@link schedule} helpers.
8
+ *
9
+ * Rules are the feedback unit. Validation groups summarize rule outcomes for
10
+ * presentation, while individual solver constraints remain implementation
11
+ * details. Descriptor compilation therefore emits artifacts that make the
12
+ * validation story explicit: reported constraints, pre-solve feedback,
13
+ * post-solve validators, cost contributions, and explicit skip strategies for
14
+ * scaffolding or artifacts with no meaningful validation feedback.
15
+ *
16
+ * @module
17
+ */
18
+
1
19
  export {
2
- createAssignTogetherRule,
3
- createAssignmentPriorityRule,
4
- createLocationPreferenceRule,
5
- createMaxConsecutiveDaysRule,
6
- createMaxHoursDayRule,
7
- createMaxHoursWeekRule,
8
- createMinConsecutiveDaysRule,
9
- createMinHoursDayRule,
10
- createMinHoursWeekRule,
11
- createMinRestBetweenShiftsRule,
12
- createTimeOffRule,
20
+ assignTogetherRuleDescriptor,
21
+ assignmentPriorityRuleDescriptor,
22
+ locationPreferenceRuleDescriptor,
23
+ maxConcurrentAssignmentsRuleDescriptor,
24
+ maxConsecutiveDaysRuleDescriptor,
25
+ maxHoursDayRuleDescriptor,
26
+ maxHoursWeekRuleDescriptor,
27
+ minConsecutiveDaysRuleDescriptor,
28
+ minHoursDayRuleDescriptor,
29
+ minHoursWeekRuleDescriptor,
30
+ minRestBetweenShiftsRuleDescriptor,
31
+ targetDaysWeekRuleDescriptor,
32
+ targetPeakConcurrentAssignmentsRuleDescriptor,
33
+ timeOffRuleDescriptor,
13
34
  } from "./rules/index.js";
14
35
 
15
- export { builtInCpsatRuleFactories, createCpsatRuleFactory } from "./rules/registry.js";
36
+ export {
37
+ assertValidCpsatRuleRegistry,
38
+ builtInCpsatRuleRegistry,
39
+ createCpsatRuleRegistry,
40
+ } from "./rules/registry.js";
16
41
 
17
42
  export type {
43
+ AnyCpsatRuleConfigEntry,
44
+ BuiltInCpsatRuleConfigRegistry,
45
+ BuiltInCpsatRuleRegistry,
46
+ CpsatRuleConfigByName,
47
+ CpsatRuleConfigEntry,
48
+ CpsatRuleConfigEntryFor,
18
49
  CpsatRuleName,
19
50
  CpsatRuleRegistry,
20
- CpsatRuleFactories,
21
- BuiltInCpsatRuleFactories,
22
- CpsatRuleRegistryFromFactories,
23
- CreateCpsatRuleFunction,
24
- CpsatRuleConfigEntry,
25
51
  } from "./rules/rules.types.js";
26
52
 
27
53
  export { buildCpsatRules, getMemberIdsForScope, resolveRuleScopes } from "./rules/resolver.js";
@@ -1,6 +1,4 @@
1
- import type { DayOfWeek, TimeOfDay } from "../types.js";
2
- import { toDayOfWeekUTC } from "../datetime.utils.js";
3
- import { parseDayString } from "./utils.js";
1
+ import type { DateString, DayOfWeek, SchedulingDay, TimeOfDay } from "../types.js";
4
2
  import type { CoverageRequirement, Priority } from "./types.js";
5
3
  import { assertSafeKeySegment, type ValidationGroup } from "./validation.types.js";
6
4
 
@@ -40,7 +38,7 @@ export interface SemanticTimeVariant extends SemanticTimeDef {
40
38
  /** Restrict this entry to specific days of the week. */
41
39
  dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
42
40
  /** Restrict this entry to specific dates (YYYY-MM-DD). */
43
- dates?: string[];
41
+ dates?: DateString[];
44
42
  }
45
43
 
46
44
  /**
@@ -59,7 +57,7 @@ interface SemanticCoverageRequirementBase<S extends string> {
59
57
  /** Scope this requirement to specific days of the week */
60
58
  dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
61
59
  /** Scope this requirement to specific dates (YYYY-MM-DD) */
62
- dates?: string[];
60
+ dates?: DateString[];
63
61
  /**
64
62
  * Override the auto-generated group for validation reporting.
65
63
  * If not provided, a group is auto-generated from the semantic time name,
@@ -141,7 +139,7 @@ export type SemanticCoverageRequirement<S extends string> =
141
139
  * Base fields shared by concrete coverage requirement variants.
142
140
  */
143
141
  interface ConcreteCoverageRequirementBase {
144
- day: string;
142
+ day: DateString;
145
143
  startTime: TimeOfDay;
146
144
  endTime: TimeOfDay;
147
145
  targetCount: number;
@@ -199,6 +197,7 @@ export type ConcreteCoverageRequirement =
199
197
  /**
200
198
  * A day-specific count within a variant {@link cover} call.
201
199
  *
200
+ * @remarks
202
201
  * Each variant specifies a count and optional day/date scope. During
203
202
  * resolution, the most specific matching variant wins for each day
204
203
  * (`dates` > `dayOfWeek` > default), mirroring {@link SemanticTimeVariant}.
@@ -221,7 +220,7 @@ export interface CoverageVariant {
221
220
  /** Restrict this variant to specific days of the week. */
222
221
  dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
223
222
  /** Restrict this variant to specific dates (YYYY-MM-DD). */
224
- dates?: string[];
223
+ dates?: DateString[];
225
224
  /** Defaults to `"MANDATORY"`. */
226
225
  priority?: Priority;
227
226
  }
@@ -337,7 +336,7 @@ export interface SemanticTimeContext<S extends string> {
337
336
  * Resolve all coverage requirements to concrete CoverageRequirement[]
338
337
  * for the given days in the scheduling horizon.
339
338
  */
340
- resolve(reqs: MixedCoverageRequirement<S>[], days: string[]): CoverageRequirement[];
339
+ resolve(reqs: MixedCoverageRequirement<S>[], days: SchedulingDay[]): CoverageRequirement[];
341
340
  }
342
341
 
343
342
  /**
@@ -394,7 +393,7 @@ export function defineSemanticTimes<const T extends Record<string, SemanticTimeE
394
393
  return reqs;
395
394
  },
396
395
 
397
- resolve(reqs: MixedCoverageRequirement<S>[], days: string[]): CoverageRequirement[] {
396
+ resolve(reqs: MixedCoverageRequirement<S>[], days: SchedulingDay[]): CoverageRequirement[] {
398
397
  return resolveSemanticCoverage(defs, reqs, days);
399
398
  },
400
399
  };
@@ -405,7 +404,7 @@ export function defineSemanticTimes<const T extends Record<string, SemanticTimeE
405
404
  * Handles the discriminated union by checking which variant to construct.
406
405
  */
407
406
  function buildCoverageRequirement(
408
- day: string,
407
+ day: DateString,
409
408
  startTime: TimeOfDay,
410
409
  endTime: TimeOfDay,
411
410
  roleIds: [string, ...string[]] | undefined,
@@ -417,14 +416,11 @@ function buildCoverageRequirement(
417
416
  const base = { day, startTime, endTime, targetCount, priority, group };
418
417
 
419
418
  if (roleIds && roleIds.length > 0) {
420
- // Role-based (with optional skills)
421
419
  return skillIds && skillIds.length > 0 ? { ...base, roleIds, skillIds } : { ...base, roleIds };
422
420
  } else if (skillIds && skillIds.length > 0) {
423
- // Skill-only
424
421
  return { ...base, skillIds };
425
422
  }
426
423
 
427
- // This shouldn't happen if input types are correct, but handle gracefully
428
424
  throw new Error(
429
425
  `Coverage requirement for day "${day}" must have at least one of roleIds or skillIds`,
430
426
  );
@@ -437,15 +433,15 @@ function buildCoverageRequirement(
437
433
  function resolveSemanticCoverage<S extends string>(
438
434
  defs: Record<S, SemanticTimeEntry>,
439
435
  reqs: MixedCoverageRequirement<S>[],
440
- days: string[],
436
+ days: SchedulingDay[],
441
437
  ): CoverageRequirement[] {
442
438
  const result: CoverageRequirement[] = [];
443
- const daySet = new Set(days);
439
+ const dayIsoSet = new Set(days.map((d) => d.iso));
444
440
 
445
441
  for (const req of reqs) {
446
442
  if (isConcreteCoverage(req)) {
447
443
  // Concrete requirement - pass through if day is in horizon
448
- if (daySet.has(req.day)) {
444
+ if (dayIsoSet.has(req.day)) {
449
445
  const defaultGroup = concreteCoverageGroup(req);
450
446
  result.push(
451
447
  buildCoverageRequirement(
@@ -478,7 +474,7 @@ function resolveSemanticCoverage<S extends string>(
478
474
 
479
475
  result.push(
480
476
  buildCoverageRequirement(
481
- day,
477
+ day.iso,
482
478
  resolved.startTime,
483
479
  resolved.endTime,
484
480
  req.roleIds,
@@ -504,7 +500,7 @@ function resolveSemanticCoverage<S extends string>(
504
500
  if (resolved) {
505
501
  result.push(
506
502
  buildCoverageRequirement(
507
- day,
503
+ day.iso,
508
504
  resolved.startTime,
509
505
  resolved.endTime,
510
506
  req.roleIds,
@@ -568,21 +564,18 @@ function variantCoverageGroup<S extends string>(
568
564
  */
569
565
  function resolveVariantForDay(
570
566
  variants: readonly CoverageVariant[],
571
- day: string,
567
+ day: SchedulingDay,
572
568
  ): CoverageVariant | null {
573
- const date = parseDayString(day);
574
- const dayOfWeek = toDayOfWeekUTC(date);
575
-
576
569
  let dateMatch: CoverageVariant | null = null;
577
570
  let dowMatch: CoverageVariant | null = null;
578
571
  let defaultMatch: CoverageVariant | null = null;
579
572
 
580
573
  for (const variant of variants) {
581
- if (variant.dates && variant.dates.includes(day)) {
574
+ if (variant.dates && variant.dates.includes(day.iso)) {
582
575
  dateMatch = variant;
583
576
  break;
584
577
  }
585
- if (variant.dayOfWeek && variant.dayOfWeek.includes(dayOfWeek)) {
578
+ if (variant.dayOfWeek && variant.dayOfWeek.includes(day.dayOfWeek)) {
586
579
  dowMatch = variant;
587
580
  }
588
581
  if (!variant.dates && !variant.dayOfWeek) {
@@ -607,8 +600,9 @@ function concreteCoverageGroup(req: ConcreteCoverageRequirement): ValidationGrou
607
600
  * Formats days of week for display in group keys.
608
601
  */
609
602
  function formatDaysScope(days: DayOfWeek[]): string {
610
- if (days.length === 0) return "";
611
- if (days.length === 1) return days[0]!;
603
+ const first = days[0];
604
+ if (!first) return "";
605
+ if (days.length === 1) return first;
612
606
 
613
607
  // Check for common patterns
614
608
  const weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
@@ -640,20 +634,18 @@ function formatTime(time: TimeOfDay): string {
640
634
  * Filter days based on optional day-of-week and specific date constraints.
641
635
  */
642
636
  function filterDays(
643
- allDays: string[],
637
+ allDays: SchedulingDay[],
644
638
  dayOfWeek?: DayOfWeek[],
645
639
  specificDates?: string[],
646
- ): string[] {
640
+ ): SchedulingDay[] {
647
641
  let result = allDays;
648
642
 
649
643
  if (specificDates && specificDates.length > 0) {
650
- // If specific dates are provided, use only those (intersection with allDays)
651
644
  const dateSet = new Set(specificDates);
652
- result = result.filter((d) => dateSet.has(d));
645
+ result = result.filter((d) => dateSet.has(d.iso));
653
646
  } else if (dayOfWeek && dayOfWeek.length > 0) {
654
- // Filter by day of week
655
647
  const daySet = new Set(dayOfWeek);
656
- result = result.filter((d) => daySet.has(toDayOfWeekUTC(parseDayString(d))));
648
+ result = result.filter((d) => daySet.has(d.dayOfWeek));
657
649
  }
658
650
 
659
651
  return result;
@@ -663,26 +655,21 @@ function filterDays(
663
655
  * Resolve the time definition for a specific day.
664
656
  * Returns the most specific match: date-specific > day-of-week > default.
665
657
  */
666
- function resolveTimeForDay(entry: SemanticTimeEntry, day: string): SemanticTimeDef | null {
667
- // Simple definition - applies to all days
658
+ function resolveTimeForDay(entry: SemanticTimeEntry, day: SchedulingDay): SemanticTimeDef | null {
668
659
  if (!Array.isArray(entry)) {
669
660
  return entry;
670
661
  }
671
662
 
672
- const date = parseDayString(day);
673
- const dayOfWeek = toDayOfWeekUTC(date);
674
-
675
- // Find best match: date-specific first, then day-of-week, then default
676
663
  let dateMatch: SemanticTimeVariant | null = null;
677
664
  let dowMatch: SemanticTimeVariant | null = null;
678
665
  let defaultMatch: SemanticTimeVariant | null = null;
679
666
 
680
667
  for (const variant of entry) {
681
- if (variant.dates && variant.dates.includes(day)) {
668
+ if (variant.dates && variant.dates.includes(day.iso)) {
682
669
  dateMatch = variant;
683
- break; // Date-specific is most specific
670
+ break;
684
671
  }
685
- if (variant.dayOfWeek && variant.dayOfWeek.includes(dayOfWeek)) {
672
+ if (variant.dayOfWeek && variant.dayOfWeek.includes(day.dayOfWeek)) {
686
673
  dowMatch = variant;
687
674
  }
688
675
  if (!variant.dates && !variant.dayOfWeek) {
@@ -690,6 +677,5 @@ function resolveTimeForDay(entry: SemanticTimeEntry, day: string): SemanticTimeD
690
677
  }
691
678
  }
692
679
 
693
- const match = dateMatch ?? dowMatch ?? defaultMatch;
694
- return match ?? null;
680
+ return dateMatch ?? dowMatch ?? defaultMatch ?? null;
695
681
  }