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,101 +1,122 @@
1
1
  import * as z from "zod";
2
- import type { CompilationRule, CostContribution } from "../model-builder.js";
3
- import type { ShiftPattern, SchedulingMember } from "../types.js";
4
- import type { ShiftAssignment } from "../response.js";
2
+ import { defineRuleDescriptor } from "../rule-descriptor.js";
3
+ import type { CostArtifact } from "../rule-descriptor.js";
5
4
  import { COST_CATEGORY } from "../cost.js";
6
5
  import {
7
6
  entityScope,
8
- timeScope,
9
7
  parseEntityScope,
10
8
  parseTimeScope,
11
- resolveMembersFromScope,
12
9
  resolveActiveDaysFromScope,
10
+ resolveMembersFromScope,
11
+ timeScope,
13
12
  } from "./scope.types.js";
14
- import { patternDurationMinutes } from "./cost-utils.js";
13
+ import { patternDurationMinutes } from "./pattern-time.js";
15
14
 
16
- const DayCostSurchargeSchema = z
15
+ export const DayCostSurchargeSchema = z
17
16
  .object({
18
17
  amountPerHour: z.number().min(0),
19
18
  })
20
19
  .and(entityScope(["members", "roles", "skills"]))
21
20
  .and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]));
22
21
 
23
- /** Configuration for {@link createDayCostSurchargeRule}. */
22
+ /** Configuration for {@link dayCostSurchargeRuleDescriptor}. */
24
23
  export type DayCostSurchargeConfig = z.infer<typeof DayCostSurchargeSchema>;
25
24
 
26
25
  /**
27
- * Creates a day-based flat surcharge rule.
26
+ * Low-level descriptor for the `day-cost-surcharge` rule.
28
27
  *
29
- * Adds a flat extra amount per hour for assignments on matching days,
30
- * independent of the member's base rate.
28
+ * @remarks
29
+ * This artifact always contributes to post-solve cost calculation. It only adds
30
+ * solver objective terms when `minimize-cost` is active.
31
31
  *
32
- * When `minimizeCost()` is not present, no solver terms are emitted,
33
- * but the `cost()` method still contributes to post-solve calculation.
32
+ * @category Rules
34
33
  */
35
- export function createDayCostSurchargeRule(config: DayCostSurchargeConfig): CompilationRule {
36
- const parsed = DayCostSurchargeSchema.parse(config);
37
- const { amountPerHour } = parsed;
38
- const entityScopeValue = parseEntityScope(parsed);
39
- const timeScopeValue = parseTimeScope(parsed);
34
+ export const dayCostSurchargeRuleDescriptor = defineRuleDescriptor({
35
+ name: "day-cost-surcharge",
36
+ schema: DayCostSurchargeSchema,
37
+ compile(config) {
38
+ const { amountPerHour } = config;
39
+ const entityScopeValue = parseEntityScope(config);
40
+ const timeScopeValue = parseTimeScope(config);
40
41
 
41
- return {
42
- compile(b) {
43
- if (!b.costContext?.active) return;
44
- if (amountPerHour <= 0) return;
42
+ const validation = {
43
+ strategy: "skip" as const,
44
+ category: "no-meaningful-feedback" as const,
45
+ rationale:
46
+ amountPerHour <= 0
47
+ ? "A zero surcharge does not change optimization or cost accounting."
48
+ : "Day cost surcharges only affect optimization when minimize-cost is active.",
49
+ };
45
50
 
46
- const targetMembers = resolveMembersFromScope(entityScopeValue, b.members);
47
- const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
51
+ const costArtifact: CostArtifact = {
52
+ kind: "cost",
53
+ validation,
54
+ compileObjective(builder) {
55
+ if (!builder.costContext?.active || amountPerHour <= 0) return;
48
56
 
49
- if (targetMembers.length === 0 || activeDays.length === 0) return;
57
+ const targetMembers = resolveMembersFromScope(entityScopeValue, builder.members);
58
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, builder.days);
59
+ if (targetMembers.length === 0 || activeDays.length === 0) return;
50
60
 
51
- const { normalizationFactor } = b.costContext;
52
-
53
- for (const emp of targetMembers) {
54
- for (const pattern of b.shiftPatterns) {
55
- if (!b.canAssign(emp, pattern)) continue;
56
- for (const day of activeDays) {
57
- if (!b.patternAvailableOnDay(pattern, day)) continue;
58
- const duration = patternDurationMinutes(pattern);
59
- const surcharge = (amountPerHour * duration) / 60;
60
- const normalizedPenalty = surcharge / normalizationFactor;
61
- b.addPenalty(b.assignment(emp.id, pattern.id, day), Math.max(1, normalizedPenalty));
61
+ const { normalizationFactor } = builder.costContext;
62
+ for (const member of targetMembers) {
63
+ for (const pattern of builder.shiftPatterns) {
64
+ if (!builder.canAssign(member, pattern)) continue;
65
+ for (const day of activeDays) {
66
+ if (!builder.patternAvailableOnDay(pattern, day)) continue;
67
+ const duration = patternDurationMinutes(pattern);
68
+ const surcharge = (amountPerHour * duration) / 60;
69
+ const normalizedPenalty = surcharge / normalizationFactor;
70
+ builder.addPenalty(
71
+ builder.assignment(member.id, pattern.id, day),
72
+ Math.max(1, normalizedPenalty),
73
+ );
74
+ }
62
75
  }
63
76
  }
64
- }
65
- },
66
-
67
- cost(
68
- assignments: ShiftAssignment[],
69
- members: ReadonlyArray<SchedulingMember>,
70
- shiftPatterns: ReadonlyArray<ShiftPattern>,
71
- ): CostContribution {
72
- if (amountPerHour <= 0) return { entries: [] };
77
+ },
78
+ calculateCost(assignments, costContext) {
79
+ if (amountPerHour <= 0) return { entries: [] };
73
80
 
74
- const patternMap = new Map(shiftPatterns.map((p) => [p.id, p]));
75
- const allDays = [...new Set(assignments.map((a) => a.day))].toSorted();
76
- const activeDays = new Set(resolveActiveDaysFromScope(timeScopeValue, allDays));
77
- const targetEmpIds = new Set(
78
- resolveMembersFromScope(entityScopeValue, [...members]).map((e) => e.id),
79
- );
81
+ const patternMap = new Map(
82
+ costContext.shiftPatterns.map((pattern) => [pattern.id, pattern]),
83
+ );
84
+ const activeDays = new Set(
85
+ resolveActiveDaysFromScope(timeScopeValue, [...costContext.days]).map((day) => day.iso),
86
+ );
87
+ const targetMemberIds = new Set(
88
+ resolveMembersFromScope(entityScopeValue, [...costContext.members]).map(
89
+ (member) => member.id,
90
+ ),
91
+ );
80
92
 
81
- const entries: CostContribution["entries"] = [];
93
+ const entries = [] as Array<{
94
+ memberId: string;
95
+ day: string;
96
+ category: string;
97
+ amount: number;
98
+ }>;
99
+ for (const assignment of assignments) {
100
+ if (!activeDays.has(assignment.day) || !targetMemberIds.has(assignment.memberId))
101
+ continue;
102
+ const pattern = patternMap.get(assignment.shiftPatternId);
103
+ if (!pattern) continue;
104
+ const duration = patternDurationMinutes(pattern);
105
+ entries.push({
106
+ memberId: assignment.memberId,
107
+ day: assignment.day,
108
+ category: COST_CATEGORY.PREMIUM,
109
+ amount: (amountPerHour * duration) / 60,
110
+ });
111
+ }
82
112
 
83
- for (const a of assignments) {
84
- if (!activeDays.has(a.day)) continue;
85
- if (!targetEmpIds.has(a.memberId)) continue;
86
- const pattern = patternMap.get(a.shiftPatternId);
87
- if (!pattern) continue;
88
- const duration = patternDurationMinutes(pattern);
89
- const amount = (amountPerHour * duration) / 60;
90
- entries.push({
91
- memberId: a.memberId,
92
- day: a.day,
93
- category: COST_CATEGORY.PREMIUM,
94
- amount,
95
- });
96
- }
113
+ return { entries };
114
+ },
115
+ };
97
116
 
98
- return { entries };
99
- },
100
- };
101
- }
117
+ return {
118
+ rule: "day-cost-surcharge",
119
+ artifacts: [costArtifact],
120
+ };
121
+ },
122
+ });
@@ -1,24 +1,30 @@
1
- export { createAssignTogetherRule } from "./assign-together.js";
2
- export { createAssignmentPriorityRule } from "./assignment-priority.js";
3
- export { createLocationPreferenceRule } from "./location-preference.js";
4
- export { createMaxConsecutiveDaysRule } from "./max-consecutive-days.js";
5
- export { createMaxDaysWeekRule } from "./max-days-week.js";
6
- export { createMaxHoursDayRule } from "./max-hours-day.js";
7
- export { createMaxHoursWeekRule } from "./max-hours-week.js";
8
- export { createMaxShiftsDayRule } from "./max-shifts-day.js";
9
- export { createMinConsecutiveDaysRule } from "./min-consecutive-days.js";
10
- export { createMinDaysWeekRule } from "./min-days-week.js";
11
- export { createMinHoursDayRule } from "./min-hours-day.js";
12
- export { createMinHoursWeekRule } from "./min-hours-week.js";
13
- export { createMinRestBetweenShiftsRule } from "./min-rest-between-shifts.js";
14
- export { createMustAssignRule } from "./must-assign.js";
15
- export { createTimeOffRule } from "./time-off.js";
16
- export { createMinimizeCostRule } from "./minimize-cost.js";
17
- export { createDayCostMultiplierRule } from "./day-cost-multiplier.js";
18
- export { createDayCostSurchargeRule } from "./day-cost-surcharge.js";
19
- export { createTimeCostSurchargeRule } from "./time-cost-surcharge.js";
20
- export { createOvertimeWeeklyMultiplierRule } from "./overtime-weekly-multiplier.js";
21
- export { createOvertimeWeeklySurchargeRule } from "./overtime-weekly-surcharge.js";
22
- export { createOvertimeDailyMultiplierRule } from "./overtime-daily-multiplier.js";
23
- export { createOvertimeDailySurchargeRule } from "./overtime-daily-surcharge.js";
24
- export { createOvertimeTieredMultiplierRule } from "./overtime-tiered-multiplier.js";
1
+ export { assignTogetherRuleDescriptor } from "./assign-together.js";
2
+ export { assignmentPriorityRuleDescriptor } from "./assignment-priority.js";
3
+ export { locationPreferenceRuleDescriptor } from "./location-preference.js";
4
+ export { rolePreferenceRuleDescriptor } from "./role-preference.js";
5
+ export { maxConcurrentAssignmentsRuleDescriptor } from "./max-concurrent-assignments.js";
6
+ export { targetPeakConcurrentAssignmentsRuleDescriptor } from "./target-peak-concurrent-assignments.js";
7
+ export { targetDaysWeekRuleDescriptor } from "./target-days-week.js";
8
+ export { maxConsecutiveDaysRuleDescriptor } from "./max-consecutive-days.js";
9
+ export { maxDaysWeekRuleDescriptor } from "./max-days-week.js";
10
+ export { maxHoursDayRuleDescriptor } from "./max-hours-day.js";
11
+ export { maxHoursWeekRuleDescriptor } from "./max-hours-week.js";
12
+ export { maxShiftsDayRuleDescriptor } from "./max-shifts-day.js";
13
+ export { minConsecutiveDaysRuleDescriptor } from "./min-consecutive-days.js";
14
+ export { minDaysWeekRuleDescriptor } from "./min-days-week.js";
15
+ export { minHoursDayRuleDescriptor } from "./min-hours-day.js";
16
+ export { minHoursWeekRuleDescriptor } from "./min-hours-week.js";
17
+ export { minRestBetweenShiftsRuleDescriptor } from "./min-rest-between-shifts.js";
18
+ export { mustAssignRuleDescriptor } from "./must-assign.js";
19
+ export { timeOffRuleDescriptor } from "./time-off.js";
20
+ export { minimizeCostRuleDescriptor } from "./minimize-cost.js";
21
+ export { dayCostMultiplierRuleDescriptor } from "./day-cost-multiplier.js";
22
+ export { dayCostSurchargeRuleDescriptor } from "./day-cost-surcharge.js";
23
+ export { timeCostSurchargeRuleDescriptor } from "./time-cost-surcharge.js";
24
+ export { overtimeWeeklyMultiplierRuleDescriptor } from "./overtime-weekly-multiplier.js";
25
+ export { overtimeWeeklySurchargeRuleDescriptor } from "./overtime-weekly-surcharge.js";
26
+ export { overtimeDailyMultiplierRuleDescriptor } from "./overtime-daily-multiplier.js";
27
+ export { overtimeDailySurchargeRuleDescriptor } from "./overtime-daily-surcharge.js";
28
+ export { overtimeTieredMultiplierRuleDescriptor } from "./overtime-tiered-multiplier.js";
29
+ export { maxDaysOfWeekPerPeriodRuleDescriptor } from "./max-days-of-week-per-period.js";
30
+ export { minDaysOfWeekPerPeriodRuleDescriptor } from "./min-days-of-week-per-period.js";
@@ -1,15 +1,16 @@
1
1
  import * as z from "zod";
2
- import type { CompilationRule } from "../model-builder.js";
3
- import type { Priority } from "../types.js";
4
- import { OBJECTIVE_WEIGHTS } from "../utils.js";
2
+ import { defineRuleDescriptor } from "../rule-descriptor.js";
3
+ import { PREFERENCE_WEIGHTS } from "../utils.js";
5
4
  import {
6
5
  PrioritySchema,
7
6
  entityScope,
8
7
  parseEntityScope,
9
8
  resolveMembersFromScope,
10
9
  } from "./scope.types.js";
10
+ import { assignmentVar } from "./variables.js";
11
+ import { canAssignMemberToPattern, isPatternAvailableOnDay } from "./pattern-eligibility.js";
11
12
 
12
- const LocationPreferenceSchema = z
13
+ export const LocationPreferenceSchema = z
13
14
  .object({
14
15
  locationId: z.string(),
15
16
  priority: PrioritySchema,
@@ -17,59 +18,53 @@ const LocationPreferenceSchema = z
17
18
  .and(entityScope(["members", "roles", "skills"]));
18
19
 
19
20
  /**
20
- * Configuration for {@link createLocationPreferenceRule}.
21
- *
22
- * - `locationId` (required): the location ID to prefer for matching shift patterns
23
- * - `priority` (required): how strongly to prefer this location
24
- *
25
- * Entity scoping (at most one): `memberIds`, `roleIds`, `skillIds`
21
+ * Configuration for {@link locationPreferenceRuleDescriptor}.
26
22
  */
27
23
  export type LocationPreferenceConfig = z.infer<typeof LocationPreferenceSchema>;
28
24
 
29
- const PRIORITY_WEIGHTS: Record<Priority, number> = {
30
- LOW: OBJECTIVE_WEIGHTS.FAIRNESS,
31
- MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE,
32
- HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5,
33
- MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5,
34
- };
35
-
36
25
  /**
37
- * Prefers assigning a person to shift patterns matching a specific location.
26
+ * Low-level descriptor for location preference rules.
38
27
  *
39
- * @param config - See {@link LocationPreferenceConfig}
40
- * @example
41
- * ```ts
42
- * createLocationPreferenceRule({
43
- * locationId: "terrace",
44
- * priority: "HIGH",
45
- * memberIds: ["alice"],
46
- * });
47
- * ```
28
+ * @category Rules
48
29
  */
49
- export function createLocationPreferenceRule(config: LocationPreferenceConfig): CompilationRule {
50
- const parsed = LocationPreferenceSchema.parse(config);
51
- const scope = parseEntityScope(parsed);
52
- const { locationId } = parsed;
53
- const weight = PRIORITY_WEIGHTS[parsed.priority] ?? 0;
54
-
55
- return {
56
- compile(b) {
57
- if (weight === 0) return;
58
-
59
- const members = resolveMembersFromScope(scope, b.members);
30
+ export const locationPreferenceRuleDescriptor = defineRuleDescriptor({
31
+ name: "location-preference",
32
+ schema: LocationPreferenceSchema,
33
+ compile(config, ctx) {
34
+ const scope = parseEntityScope(config);
35
+ const { locationId } = config;
36
+ const weight = PREFERENCE_WEIGHTS[config.priority as keyof typeof PREFERENCE_WEIGHTS] ?? 0;
37
+ if (weight === 0) {
38
+ return { rule: "location-preference", artifacts: [] };
39
+ }
60
40
 
61
- for (const emp of members) {
62
- for (const pattern of b.shiftPatterns) {
63
- if (!b.canAssign(emp, pattern)) continue;
64
- const isPreferred = pattern.locationId === locationId;
65
- if (isPreferred) continue;
41
+ const members = resolveMembersFromScope(scope, [...ctx.members]);
42
+ const terms = members.flatMap((member) =>
43
+ ctx.shiftPatterns.flatMap((pattern) => {
44
+ if (!canAssignMemberToPattern(member, pattern)) return [];
45
+ if (pattern.locationId === locationId) return [];
46
+ return ctx.days
47
+ .filter((day) => isPatternAvailableOnDay(pattern, day))
48
+ .map((day) => ({ var: assignmentVar(member.id, pattern.id, day.iso), coeff: weight }));
49
+ }),
50
+ );
66
51
 
67
- for (const day of b.days) {
68
- if (!b.patternAvailableOnDay(pattern, day)) continue;
69
- b.addPenalty(b.assignment(emp.id, pattern.id, day), weight);
70
- }
71
- }
72
- }
73
- },
74
- };
75
- }
52
+ return {
53
+ rule: "location-preference",
54
+ artifacts: [
55
+ {
56
+ kind: "objective",
57
+ terms,
58
+ validation: {
59
+ strategy: "skip",
60
+ category: "no-meaningful-feedback",
61
+ rationale:
62
+ terms.length === 0
63
+ ? `Location preference for "${locationId}" produced no objective terms.`
64
+ : "Preference rules influence the objective but do not emit validation feedback.",
65
+ },
66
+ },
67
+ ],
68
+ };
69
+ },
70
+ });
@@ -0,0 +1,170 @@
1
+ import * as z from "zod";
2
+ import type { TimeOfDay } from "../../types.js";
3
+ import { defineRuleDescriptor } from "../rule-descriptor.js";
4
+ import {
5
+ MINUTES_PER_DAY,
6
+ normalizeEndMinutes,
7
+ priorityToPenalty,
8
+ timeOfDayToMinutes,
9
+ } from "../utils.js";
10
+ import {
11
+ PrioritySchema,
12
+ entityScope,
13
+ parseEntityScope,
14
+ parseTimeScope,
15
+ resolveActiveDaysFromScope,
16
+ resolveMembersFromScope,
17
+ ruleGroup,
18
+ timeScope,
19
+ } from "./scope.types.js";
20
+ import { hardConstraint, reportValidation, softConstraint } from "./artifacts.js";
21
+ import {
22
+ buildConcurrentAssignmentSegments,
23
+ collectConcurrentAssignmentIntervals,
24
+ resolveConcurrentWindow,
25
+ } from "./concurrent-intervals.js";
26
+
27
+ const timeOfDaySchema = z.object({
28
+ hours: z.number().int().min(0).max(23),
29
+ minutes: z.number().int().min(0).max(59),
30
+ });
31
+
32
+ export const MaxConcurrentAssignmentsSchema = z
33
+ .object({
34
+ assignments: z.number().int().min(0),
35
+ priority: PrioritySchema,
36
+ startTime: timeOfDaySchema.optional(),
37
+ endTime: timeOfDaySchema.optional(),
38
+ })
39
+ .and(entityScope(["members", "roles", "skills"]))
40
+ .and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]))
41
+ .refine(
42
+ (config) => {
43
+ const hasStartTime = config.startTime !== undefined;
44
+ const hasEndTime = config.endTime !== undefined;
45
+ return hasStartTime === hasEndTime;
46
+ },
47
+ {
48
+ message:
49
+ "Both startTime and endTime must be provided together for partial day concurrent-assignment caps",
50
+ },
51
+ );
52
+
53
+ /**
54
+ * Configuration for {@link maxConcurrentAssignmentsRuleDescriptor}.
55
+ */
56
+ export type MaxConcurrentAssignmentsConfig = z.infer<typeof MaxConcurrentAssignmentsSchema>;
57
+
58
+ /**
59
+ * Low-level descriptor for the `max-concurrent-assignments` rule.
60
+ *
61
+ * @remarks
62
+ * Caps how many targeted members may be assigned at the same time. Use it for
63
+ * physical capacity constraints that should remain independent from coverage
64
+ * floors. Unlike {@link import("./max-shifts-day.js").maxShiftsDayRuleDescriptor},
65
+ * this counts concurrent overlap, not total assignments on a day.
66
+ *
67
+ * @category Rules
68
+ */
69
+ export const maxConcurrentAssignmentsRuleDescriptor = defineRuleDescriptor({
70
+ name: "max-concurrent-assignments",
71
+ schema: MaxConcurrentAssignmentsSchema,
72
+ compile(config, ctx) {
73
+ const { assignments, priority, startTime, endTime } = config;
74
+ const entityScopeValue = parseEntityScope(config);
75
+ const timeScopeValue = parseTimeScope(config);
76
+ const targetMembers = resolveMembersFromScope(entityScopeValue, [...ctx.members]);
77
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, [...ctx.days]);
78
+
79
+ if (targetMembers.length === 0 || activeDays.length === 0) {
80
+ return { rule: "max-concurrent-assignments", artifacts: [] };
81
+ }
82
+
83
+ const windowStart = startTime ? timeOfDayToMinutes(startTime) : undefined;
84
+ const windowEnd =
85
+ startTime && endTime
86
+ ? normalizeEndMinutes(windowStart ?? 0, timeOfDayToMinutes(endTime))
87
+ : undefined;
88
+ const windowKey = buildWindowKey(startTime, endTime);
89
+ const windowLabel = buildWindowLabel(startTime, endTime);
90
+ const group = ruleGroup(
91
+ `max-concurrent-assignments:${assignments}:${windowKey}`,
92
+ `Max ${assignments} concurrent assignment${assignments === 1 ? "" : "s"}${windowLabel}`,
93
+ entityScopeValue,
94
+ timeScopeValue,
95
+ );
96
+
97
+ const artifacts = activeDays.flatMap((day) => {
98
+ const dayContext = { days: [day.iso], memberIds: targetMembers.map((member) => member.id) };
99
+ const intervals = collectConcurrentAssignmentIntervals(day, targetMembers, ctx.shiftPatterns);
100
+ const window = resolveConcurrentWindow(intervals, windowStart, windowEnd);
101
+ const segments = buildConcurrentAssignmentSegments(intervals, window.start, window.end);
102
+
103
+ return segments.flatMap((segment) => {
104
+ const description = `Max ${assignments} concurrent assignment${assignments === 1 ? "" : "s"} on ${day.iso} (${formatTimeRange(segment.start, segment.end)})`;
105
+ const constraintId = `${group.key}:${day.iso}:${segment.start}`;
106
+ const linearTerms = segment.varNames.map((varName) => ({ var: varName, coeff: 1 }));
107
+
108
+ return [
109
+ priority === "MANDATORY"
110
+ ? hardConstraint({
111
+ group,
112
+ description,
113
+ context: dayContext,
114
+ validation: reportValidation(constraintId),
115
+ terms: linearTerms,
116
+ comparator: "<=",
117
+ targetValue: assignments,
118
+ })
119
+ : softConstraint({
120
+ group,
121
+ description,
122
+ context: dayContext,
123
+ validation: reportValidation(),
124
+ terms: linearTerms,
125
+ comparator: "<=",
126
+ targetValue: assignments,
127
+ penalty: priorityToPenalty(priority),
128
+ constraintId,
129
+ }),
130
+ ];
131
+ });
132
+ });
133
+
134
+ return {
135
+ rule: "max-concurrent-assignments",
136
+ artifacts,
137
+ };
138
+ },
139
+ });
140
+
141
+ function buildWindowKey(startTime?: TimeOfDay, endTime?: TimeOfDay): string {
142
+ if (!startTime || !endTime) {
143
+ return "all-day";
144
+ }
145
+
146
+ return `${formatTimeKey(startTime)}-${formatTimeKey(endTime)}`;
147
+ }
148
+
149
+ function buildWindowLabel(startTime?: TimeOfDay, endTime?: TimeOfDay): string {
150
+ if (!startTime || !endTime) {
151
+ return "";
152
+ }
153
+
154
+ return ` (${formatTimeKey(startTime)}-${formatTimeKey(endTime)})`;
155
+ }
156
+
157
+ function formatTimeRange(startMinutes: number, endMinutes: number): string {
158
+ return `${formatClockMinutes(startMinutes)}-${formatClockMinutes(endMinutes)}`;
159
+ }
160
+
161
+ function formatTimeKey(time: TimeOfDay): string {
162
+ return `${String(time.hours).padStart(2, "0")}:${String(time.minutes ?? 0).padStart(2, "0")}`;
163
+ }
164
+
165
+ function formatClockMinutes(totalMinutes: number): string {
166
+ const normalized = ((totalMinutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
167
+ const hours = Math.floor(normalized / 60);
168
+ const minutes = normalized % 60;
169
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
170
+ }